├── .eslintignore ├── .husky ├── pre-commit ├── commit-msg ├── pre-push └── README.md ├── .DS_Store ├── .dockerignore ├── public └── images │ ├── code.png │ ├── cover.png │ ├── bmc_qr.png │ ├── picpay-qr.jpeg │ ├── qrcode-pix.png │ ├── atendai-logo.png │ ├── picpay-image.png │ ├── video-cover.png │ ├── evolution-logo.png │ └── evolution-pro.png ├── prisma ├── postgresql-migrations │ ├── 20240712155950_adjusts_in_templates_table │ │ └── migration.sql │ ├── 20240725221646_modify_token_instance_table │ │ └── migration.sql │ ├── 20240722173518_add_name_column_to_openai_creds │ │ └── migration.sql │ ├── 20240817110155_add_trigger_type_advanced │ │ └── migration.sql │ ├── 20241001180457_add_message_status │ │ └── migration.sql │ ├── 20240811021156_add_chat_name_column │ │ └── migration.sql │ ├── 20250516012152_remove_unique_atribute_for_file_name_in_media │ │ └── migration.sql │ ├── 20240729115127_modify_trigger_type_openai_typebot_table │ │ └── migration.sql │ ├── 20240814173033_add_ignore_jids_chatwoot │ │ └── migration.sql │ ├── 20240906202019_add_headers_on_webhook_config │ │ └── migration.sql │ ├── 20240723200254_add_webhookurl_on_message │ │ └── migration.sql │ ├── 20250613143000_add_lid_column_to_is_onwhatsapp │ │ └── migration.sql │ ├── 20240725202651_add_webhook_url_template_table │ │ └── migration.sql │ ├── 20240610144159_create_column_profile_name_instance │ │ └── migration.sql │ ├── 20240712144948_add_business_id_column_to_instances │ │ └── migration.sql │ ├── 20240718123923_adjusts_openai_tables │ │ └── migration.sql │ ├── 20240819154941_add_context_to_integration_session │ │ └── migration.sql │ ├── 20240824161333_add_type_on_integration_sessions │ │ └── migration.sql │ ├── migration_lock.toml │ ├── 20241007164026_add_unread_messages_on_chat_table │ │ └── migration.sql │ ├── 20240801193907_add_column_speech_to_text_openai_setting_table │ │ └── migration.sql │ ├── 20240611125754_create_columns_whitelabel_chatwoot │ │ └── migration.sql │ ├── 20241006130306_alter_status_on_message_table │ │ └── migration.sql │ ├── 20240712230631_column_ignore_jids_typebot │ │ └── migration.sql │ ├── 20240611202817_create_columns_debounce_time_typebot │ │ └── migration.sql │ ├── 20240828141556_remove_name_column_from_on_whatsapp_table │ │ └── migration.sql │ ├── 20240803163908_add_column_description_on_integrations_table │ │ └── migration.sql │ ├── 20240808210239_add_column_function_url_openaibot_table │ │ └── migration.sql │ ├── 20240712162206_remove_templates_table │ │ └── migration.sql │ ├── 20250612155048_add_coluns_trypebot_tables │ │ └── migration.sql │ ├── 20250116001415_add_wavoip_token_to_settings_table │ │ └── migration.sql │ ├── 20240723152648_adjusts_in_column_openai_creds │ │ └── migration.sql │ ├── 20240712223655_column_fallback_typebot │ │ └── migration.sql │ ├── 20240813003116_make_label_unique_for_instance │ │ └── migration.sql │ ├── 20240722173259_add_name_column_to_openai_creds │ │ └── migration.sql │ ├── 20240828140837_add_is_on_whatsapp_table │ │ └── migration.sql │ ├── 20240811183328_add_unique_index_for_remoted_jid_and_instance_in_contacts │ │ └── migration.sql │ ├── 20251122003044_add_chat_instance_remotejid_unique │ │ └── migration.sql │ ├── 20250225180031_add_nats_integration │ │ └── migration.sql │ ├── 20250918182355_add_kafka_integration │ │ └── migration.sql │ ├── 20240830193533_changed_table_case │ │ └── migration.sql │ ├── 20240725184147_create_template_table │ │ └── migration.sql │ ├── 20240712150256_create_templates_table │ │ └── migration.sql │ ├── 20241011085129_create_pusher_table │ │ └── migration.sql │ ├── 20241017144950_create_index │ │ └── migration.sql │ ├── 20240713184337_add_media_table │ │ └── migration.sql │ ├── 20240821120816_bot_id_integration_session │ │ └── migration.sql │ ├── 20240729180347_modify_typebot_session_status_openai_typebot_table │ │ └── migration.sql │ ├── 20241011100803_split_messages_and_time_per_char_integrations │ │ └── migration.sql │ ├── 20240821194524_add_flowise_table │ │ └── migration.sql │ ├── 20240821171327_add_generic_bot_table │ │ └── migration.sql │ ├── 20250515211815_add_evoai_table │ │ └── migration.sql │ └── 20250514232744_add_n8n_table │ │ └── migration.sql └── mysql-migrations │ ├── 20250613143000_add_lid_column_to_is_onwhatsapp │ └── migration.sql │ ├── 20250516012152_remove_unique_atribute_for_file_name_in_media │ └── migration.sql │ ├── migration_lock.toml │ ├── 20250612155048_add_coluns_trypebot_tables │ └── migration.sql │ ├── 20250225180031_add_nats_integration │ └── migration.sql │ ├── 20250510035200_add_wavoip_token_to_settings_table │ └── migration.sql │ ├── 20250515211815_add_evoai_table │ └── migration.sql │ └── 20250514232744_add_n8n_table │ └── migration.sql ├── .gitmodules ├── manager └── dist │ ├── assets │ └── images │ │ └── evolution-logo.png │ └── index.html ├── src ├── api │ ├── integrations │ │ ├── storage │ │ │ ├── s3 │ │ │ │ ├── dto │ │ │ │ │ └── media.dto.ts │ │ │ │ ├── controllers │ │ │ │ │ └── s3.controller.ts │ │ │ │ ├── validate │ │ │ │ │ └── s3.schema.ts │ │ │ │ ├── routes │ │ │ │ │ └── s3.router.ts │ │ │ │ └── services │ │ │ │ │ └── s3.service.ts │ │ │ └── storage.router.ts │ │ ├── chatbot │ │ │ ├── evoai │ │ │ │ └── dto │ │ │ │ │ └── evoai.dto.ts │ │ │ ├── flowise │ │ │ │ └── dto │ │ │ │ │ └── flowise.dto.ts │ │ │ ├── evolutionBot │ │ │ │ └── dto │ │ │ │ │ └── evolutionBot.dto.ts │ │ │ ├── dify │ │ │ │ └── dto │ │ │ │ │ └── dify.dto.ts │ │ │ ├── n8n │ │ │ │ └── dto │ │ │ │ │ └── n8n.dto.ts │ │ │ ├── typebot │ │ │ │ └── dto │ │ │ │ │ └── typebot.dto.ts │ │ │ ├── chatbot.schema.ts │ │ │ ├── openai │ │ │ │ └── dto │ │ │ │ │ └── openai.dto.ts │ │ │ ├── base-chatbot.dto.ts │ │ │ ├── chatwoot │ │ │ │ ├── libs │ │ │ │ │ └── postgres.client.ts │ │ │ │ ├── dto │ │ │ │ │ └── chatwoot.dto.ts │ │ │ │ ├── validate │ │ │ │ │ └── chatwoot.schema.ts │ │ │ │ └── routes │ │ │ │ │ └── chatwoot.router.ts │ │ │ └── chatbot.router.ts │ │ ├── integration.dto.ts │ │ ├── event │ │ │ ├── kafka │ │ │ │ ├── kafka.schema.ts │ │ │ │ └── kafka.router.ts │ │ │ ├── event.schema.ts │ │ │ ├── event.router.ts │ │ │ ├── webhook │ │ │ │ ├── webhook.schema.ts │ │ │ │ └── webhook.router.ts │ │ │ ├── pusher │ │ │ │ ├── pusher.router.ts │ │ │ │ └── pusher.schema.ts │ │ │ ├── sqs │ │ │ │ └── sqs.router.ts │ │ │ ├── nats │ │ │ │ └── nats.router.ts │ │ │ ├── rabbitmq │ │ │ │ └── rabbitmq.router.ts │ │ │ ├── websocket │ │ │ │ └── websocket.router.ts │ │ │ └── event.dto.ts │ │ └── channel │ │ │ ├── channel.router.ts │ │ │ ├── evolution │ │ │ ├── evolution.router.ts │ │ │ └── evolution.controller.ts │ │ │ ├── meta │ │ │ ├── meta.router.ts │ │ │ └── meta.controller.ts │ │ │ └── whatsapp │ │ │ ├── baileys.controller.ts │ │ │ ├── baileysMessage.processor.ts │ │ │ └── voiceCalls │ │ │ └── transport.type.ts │ ├── dto │ │ ├── call.dto.ts │ │ ├── proxy.dto.ts │ │ ├── label.dto.ts │ │ ├── chatbot.dto.ts │ │ ├── business.dto.ts │ │ ├── settings.dto.ts │ │ ├── template.dto.ts │ │ ├── group.dto.ts │ │ ├── instance.dto.ts │ │ └── chat.dto.ts │ ├── guards │ │ ├── telemetry.guard.ts │ │ ├── auth.guard.ts │ │ └── instance.guard.ts │ ├── controllers │ │ ├── call.controller.ts │ │ ├── settings.controller.ts │ │ ├── label.controller.ts │ │ ├── business.controller.ts │ │ ├── template.controller.ts │ │ └── proxy.controller.ts │ ├── abstract │ │ ├── abstract.cache.ts │ │ └── abstract.repository.ts │ ├── routes │ │ ├── view.router.ts │ │ ├── call.router.ts │ │ ├── label.router.ts │ │ ├── proxy.router.ts │ │ ├── settings.router.ts │ │ └── business.router.ts │ ├── services │ │ ├── auth.service.ts │ │ ├── proxy.service.ts │ │ ├── settings.service.ts │ │ └── cache.service.ts │ └── repository │ │ └── repository.service.ts ├── @types │ └── express.d.ts ├── exceptions │ ├── index.ts │ ├── 403.exception.ts │ ├── 404.exception.ts │ ├── 400.exception.ts │ ├── 401.exception.ts │ └── 500.exception.ts ├── utils │ ├── renderStatus.ts │ ├── instrumentSentry.ts │ ├── server-up.ts │ ├── i18n.ts │ ├── sendTelemetry.ts │ ├── fetchLatestWaWebVersion.ts │ ├── advancedOperatorsSearch.ts │ ├── errorResponse.ts │ ├── translations │ │ ├── en.json │ │ ├── pt-BR.json │ │ └── es.json │ ├── createJid.ts │ └── use-multi-file-auth-state-redis-db.ts ├── config │ ├── path.config.ts │ ├── event.config.ts │ └── error.config.ts ├── validate │ ├── business.schema.ts │ ├── validate.schema.ts │ ├── templateDelete.schema.ts │ ├── proxy.schema.ts │ ├── templateEdit.schema.ts │ ├── label.schema.ts │ ├── template.schema.ts │ └── settings.schema.ts ├── railway.json └── cache │ ├── cacheengine.ts │ └── rediscache.client.ts ├── manager_install.sh ├── .prettierrc.js ├── Docker ├── redis │ └── docker-compose.yaml ├── mysql │ └── docker-compose.yaml ├── scripts │ ├── env_functions.sh │ ├── generate_database.sh │ └── deploy_database.sh ├── rabbitmq │ └── docker-compose.yaml ├── minio │ └── docker-compose.yaml ├── postgres │ └── docker-compose.yaml └── kafka │ └── docker-compose.yaml ├── tsup.config.ts ├── .vscode └── settings.json ├── Dockerfile.metrics ├── .gitignore ├── docker-compose.dev.yaml ├── .github ├── workflows │ ├── check_code_quality.yml │ ├── publish_docker_image_latest.yml │ ├── publish_docker_image_homolog.yml │ ├── publish_docker_image.yml │ └── security.yml ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── tsconfig.json ├── commitlint.config.js ├── .eslintrc.js ├── LICENSE ├── Dockerfile ├── runWithProvider.js ├── docker-compose.yaml └── prometheus.yml.example /.eslintignore: -------------------------------------------------------------------------------- 1 | /node-modules 2 | /dist -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run build 2 | npm run lint:check 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *Dockerfile* 3 | *docker-compose* 4 | .env 5 | node_modules 6 | dist -------------------------------------------------------------------------------- /public/images/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/code.png -------------------------------------------------------------------------------- /public/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/cover.png -------------------------------------------------------------------------------- /public/images/bmc_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/bmc_qr.png -------------------------------------------------------------------------------- /public/images/picpay-qr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/picpay-qr.jpeg -------------------------------------------------------------------------------- /public/images/qrcode-pix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/qrcode-pix.png -------------------------------------------------------------------------------- /public/images/atendai-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/atendai-logo.png -------------------------------------------------------------------------------- /public/images/picpay-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/picpay-image.png -------------------------------------------------------------------------------- /public/images/video-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/video-cover.png -------------------------------------------------------------------------------- /public/images/evolution-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/evolution-logo.png -------------------------------------------------------------------------------- /public/images/evolution-pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/public/images/evolution-pro.png -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240712155950_adjusts_in_templates_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Template_instanceId_key"; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240725221646_modify_token_instance_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Instance_token_key"; 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "evolution-manager-v2"] 2 | path = evolution-manager-v2 3 | url = https://github.com/EvolutionAPI/evolution-manager-v2.git 4 | -------------------------------------------------------------------------------- /manager/dist/assets/images/evolution-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/HEAD/manager/dist/assets/images/evolution-logo.png -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240722173518_add_name_column_to_openai_creds/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "OpenaiCreds_instanceId_key"; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240817110155_add_trigger_type_advanced/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "TriggerType" ADD VALUE 'advanced'; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20241001180457_add_message_status/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Message" ADD COLUMN "status" INTEGER; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240811021156_add_chat_name_column/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chat" ADD COLUMN "name" VARCHAR(100); 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20250516012152_remove_unique_atribute_for_file_name_in_media/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Media_fileName_key"; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240729115127_modify_trigger_type_openai_typebot_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "TriggerType" ADD VALUE 'none'; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240814173033_add_ignore_jids_chatwoot/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chatwoot" ADD COLUMN "ignoreJids" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240906202019_add_headers_on_webhook_config/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Webhook" ADD COLUMN "headers" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/mysql-migrations/20250613143000_add_lid_column_to_is_onwhatsapp/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `IsOnWhatsapp` ADD COLUMN `lid` VARCHAR(100); 3 | -------------------------------------------------------------------------------- /prisma/mysql-migrations/20250516012152_remove_unique_atribute_for_file_name_in_media/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | ALTER TABLE `Media` DROP INDEX `Media_fileName_key`; 3 | -------------------------------------------------------------------------------- /prisma/mysql-migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240723200254_add_webhookurl_on_message/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Message" ADD COLUMN "webhookUrl" VARCHAR(500); 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20250613143000_add_lid_column_to_is_onwhatsapp/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "IsOnWhatsapp" ADD COLUMN "lid" VARCHAR(100); 3 | -------------------------------------------------------------------------------- /src/api/integrations/storage/s3/dto/media.dto.ts: -------------------------------------------------------------------------------- 1 | export class MediaDto { 2 | id?: string; 3 | type?: string; 4 | messageId?: number; 5 | expiry?: number; 6 | } 7 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240725202651_add_webhook_url_template_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Template" ADD COLUMN "webhookUrl" VARCHAR(500); 3 | -------------------------------------------------------------------------------- /manager_install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | cd evolution-manager-v2 4 | npm install 5 | npm run build 6 | cd .. 7 | rm -rf manager/dist 8 | cp -r evolution-manager-v2/dist manager/dist -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240610144159_create_column_profile_name_instance/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Instance" ADD COLUMN "profileName" VARCHAR(100); 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240712144948_add_business_id_column_to_instances/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Instance" ADD COLUMN "businessId" VARCHAR(100); 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240718123923_adjusts_openai_tables/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "OpenaiBot" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240819154941_add_context_to_integration_session/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "IntegrationSession" ADD COLUMN "context" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240824161333_add_type_on_integration_sessions/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "IntegrationSession" ADD COLUMN "type" VARCHAR(100); 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20241007164026_add_unread_messages_on_chat_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chat" ADD COLUMN "unreadMessages" INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /src/api/dto/call.dto.ts: -------------------------------------------------------------------------------- 1 | export class Metadata { 2 | number: string; 3 | } 4 | 5 | export class OfferCallDto extends Metadata { 6 | isVideo?: boolean; 7 | callDuration?: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/api/dto/proxy.dto.ts: -------------------------------------------------------------------------------- 1 | export class ProxyDto { 2 | enabled?: boolean; 3 | host: string; 4 | port: string; 5 | protocol: string; 6 | username?: string; 7 | password?: string; 8 | } 9 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240801193907_add_column_speech_to_text_openai_setting_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "OpenaiSetting" ADD COLUMN "speechToText" BOOLEAN DEFAULT false; 3 | -------------------------------------------------------------------------------- /src/@types/express.d.ts: -------------------------------------------------------------------------------- 1 | import { Multer } from 'multer'; 2 | 3 | declare global { 4 | namespace Express { 5 | interface Request { 6 | file?: Multer.File; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './400.exception'; 2 | export * from './401.exception'; 3 | export * from './403.exception'; 4 | export * from './404.exception'; 5 | export * from './500.exception'; 6 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240611125754_create_columns_whitelabel_chatwoot/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chatwoot" ADD COLUMN "logo" VARCHAR(500), 3 | ADD COLUMN "organization" VARCHAR(100); 4 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20241006130306_alter_status_on_message_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Message" 3 | ALTER COLUMN "status" 4 | SET 5 | DATA TYPE VARCHAR(30); 6 | 7 | UPDATE "Message" SET "status" = 'PENDING'; -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240712230631_column_ignore_jids_typebot/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Typebot" ADD COLUMN "ignoreJids" JSONB; 3 | 4 | -- AlterTable 5 | ALTER TABLE "TypebotSetting" ADD COLUMN "ignoreJids" JSONB; 6 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240611202817_create_columns_debounce_time_typebot/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Typebot" ADD COLUMN "debounceTime" INTEGER; 3 | 4 | -- AlterTable 5 | ALTER TABLE "TypebotSetting" ADD COLUMN "debounceTime" INTEGER; 6 | -------------------------------------------------------------------------------- /src/utils/renderStatus.ts: -------------------------------------------------------------------------------- 1 | import { wa } from '@api/types/wa.types'; 2 | 3 | export const status: Record = { 4 | 0: 'ERROR', 5 | 1: 'PENDING', 6 | 2: 'SERVER_ACK', 7 | 3: 'DELIVERY_ACK', 8 | 4: 'READ', 9 | 5: 'PLAYED', 10 | }; 11 | -------------------------------------------------------------------------------- /src/api/dto/label.dto.ts: -------------------------------------------------------------------------------- 1 | export class LabelDto { 2 | id?: string; 3 | name: string; 4 | color: string; 5 | predefinedId?: string; 6 | } 7 | 8 | export class HandleLabelDto { 9 | number: string; 10 | labelId: string; 11 | action: 'add' | 'remove'; 12 | } 13 | -------------------------------------------------------------------------------- /src/api/dto/chatbot.dto.ts: -------------------------------------------------------------------------------- 1 | export class Session { 2 | remoteJid?: string; 3 | sessionId?: string; 4 | status?: string; 5 | createdAt?: number; 6 | updateAt?: number; 7 | } 8 | 9 | export class IgnoreJidDto { 10 | remoteJid?: string; 11 | action?: string; 12 | } 13 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | arrowParens: 'always', 7 | tabWidth: 2, 8 | useTabs: false, 9 | bracketSameLine: false, 10 | bracketSpacing: true, 11 | parser: 'typescript' 12 | } -------------------------------------------------------------------------------- /src/api/dto/business.dto.ts: -------------------------------------------------------------------------------- 1 | export class NumberDto { 2 | number: string; 3 | } 4 | 5 | export class getCatalogDto { 6 | number?: string; 7 | limit?: number; 8 | cursor?: string; 9 | } 10 | 11 | export class getCollectionsDto { 12 | number?: string; 13 | limit?: number; 14 | } 15 | -------------------------------------------------------------------------------- /src/api/dto/settings.dto.ts: -------------------------------------------------------------------------------- 1 | export class SettingsDto { 2 | rejectCall?: boolean; 3 | msgCall?: string; 4 | groupsIgnore?: boolean; 5 | alwaysOnline?: boolean; 6 | readMessages?: boolean; 7 | readStatus?: boolean; 8 | syncFullHistory?: boolean; 9 | wavoipToken?: string; 10 | } 11 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240828141556_remove_name_column_from_on_whatsapp_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `name` on the `is_on_whatsapp` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "is_on_whatsapp" DROP COLUMN "name"; 9 | -------------------------------------------------------------------------------- /src/config/path.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export const ROOT_DIR = process.cwd(); 4 | export const INSTANCE_DIR = join(ROOT_DIR, 'instances'); 5 | export const SRC_DIR = join(ROOT_DIR, 'src'); 6 | export const AUTH_DIR = join(ROOT_DIR, 'store', 'auth'); 7 | export const STORE_DIR = join(ROOT_DIR, 'store'); 8 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/evoai/dto/evoai.dto.ts: -------------------------------------------------------------------------------- 1 | import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; 2 | 3 | export class EvoaiDto extends BaseChatbotDto { 4 | agentUrl?: string; 5 | apiKey?: string; 6 | } 7 | 8 | export class EvoaiSettingDto extends BaseChatbotSettingDto { 9 | evoaiIdFallback?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/exceptions/403.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@api/routes/index.router'; 2 | 3 | export class ForbiddenException { 4 | constructor(...objectError: any[]) { 5 | throw { 6 | status: HttpStatus.FORBIDDEN, 7 | error: 'Forbidden', 8 | message: objectError.length > 0 ? objectError : undefined, 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/exceptions/404.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@api/routes/index.router'; 2 | 3 | export class NotFoundException { 4 | constructor(...objectError: any[]) { 5 | throw { 6 | status: HttpStatus.NOT_FOUND, 7 | error: 'Not Found', 8 | message: objectError.length > 0 ? objectError : undefined, 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240803163908_add_column_description_on_integrations_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Dify" ADD COLUMN "description" VARCHAR(255); 3 | 4 | -- AlterTable 5 | ALTER TABLE "OpenaiBot" ADD COLUMN "description" VARCHAR(255); 6 | 7 | -- AlterTable 8 | ALTER TABLE "Typebot" ADD COLUMN "description" VARCHAR(255); 9 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/flowise/dto/flowise.dto.ts: -------------------------------------------------------------------------------- 1 | import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; 2 | 3 | export class FlowiseDto extends BaseChatbotDto { 4 | apiUrl: string; 5 | apiKey?: string; 6 | } 7 | 8 | export class FlowiseSettingDto extends BaseChatbotSettingDto { 9 | flowiseIdFallback?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/exceptions/400.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@api/routes/index.router'; 2 | 3 | export class BadRequestException { 4 | constructor(...objectError: any[]) { 5 | throw { 6 | status: HttpStatus.BAD_REQUEST, 7 | error: 'Bad Request', 8 | message: objectError.length > 0 ? objectError : undefined, 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240808210239_add_column_function_url_openaibot_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Instance" ADD COLUMN "disconnectionAt" TIMESTAMP, 3 | ADD COLUMN "disconnectionObject" JSONB, 4 | ADD COLUMN "disconnectionReasonCode" INTEGER; 5 | 6 | -- AlterTable 7 | ALTER TABLE "OpenaiBot" ADD COLUMN "functionUrl" VARCHAR(500); 8 | -------------------------------------------------------------------------------- /src/api/integrations/integration.dto.ts: -------------------------------------------------------------------------------- 1 | import { ChatwootInstanceMixin } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto'; 2 | import { EventInstanceMixin } from '@api/integrations/event/event.dto'; 3 | 4 | export type Constructor = new (...args: any[]) => T; 5 | 6 | export class IntegrationDto extends EventInstanceMixin(ChatwootInstanceMixin(class {})) {} 7 | -------------------------------------------------------------------------------- /src/exceptions/401.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@api/routes/index.router'; 2 | 3 | export class UnauthorizedException { 4 | constructor(...objectError: any[]) { 5 | throw { 6 | status: HttpStatus.UNAUTHORIZED, 7 | error: 'Unauthorized', 8 | message: objectError.length > 0 ? objectError : 'Unauthorized', 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/api/guards/telemetry.guard.ts: -------------------------------------------------------------------------------- 1 | import { sendTelemetry } from '@utils/sendTelemetry'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | 4 | class Telemetry { 5 | public collectTelemetry(req: Request, res: Response, next: NextFunction): void { 6 | sendTelemetry(req.path); 7 | 8 | next(); 9 | } 10 | } 11 | 12 | export default Telemetry; 13 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/evolutionBot/dto/evolutionBot.dto.ts: -------------------------------------------------------------------------------- 1 | import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; 2 | 3 | export class EvolutionBotDto extends BaseChatbotDto { 4 | apiUrl: string; 5 | apiKey: string; 6 | } 7 | 8 | export class EvolutionBotSettingDto extends BaseChatbotSettingDto { 9 | botIdFallback?: string; 10 | } 11 | -------------------------------------------------------------------------------- /prisma/mysql-migrations/20250612155048_add_coluns_trypebot_tables/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Typebot` ADD COLUMN `splitMessages` BOOLEAN DEFAULT false, 3 | ADD COLUMN `timePerChar` INTEGER DEFAULT 50; 4 | 5 | -- AlterTable 6 | ALTER TABLE `TypebotSetting` ADD COLUMN `splitMessages` BOOLEAN DEFAULT false, 7 | ADD COLUMN `timePerChar` INTEGER DEFAULT 50; 8 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240712162206_remove_templates_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Template` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Template" DROP CONSTRAINT "Template_instanceId_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "Template"; 12 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20250612155048_add_coluns_trypebot_tables/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Typebot" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 3 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 4 | 5 | -- AlterTable 6 | ALTER TABLE "TypebotSetting" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 7 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 8 | -------------------------------------------------------------------------------- /src/exceptions/500.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@api/routes/index.router'; 2 | 3 | export class InternalServerErrorException { 4 | constructor(...objectError: any[]) { 5 | throw { 6 | status: HttpStatus.INTERNAL_SERVER_ERROR, 7 | error: 'Internal Server Error', 8 | message: objectError.length > 0 ? objectError : undefined, 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20250116001415_add_wavoip_token_to_settings_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | 8 | -- AlterTable 9 | ALTER TABLE "Setting" ADD COLUMN IF NOT EXISTS "wavoipToken" VARCHAR(100); 10 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240723152648_adjusts_in_column_openai_creds/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[openaiCredsId]` on the table `OpenaiSetting` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "OpenaiSetting_openaiCredsId_key" ON "OpenaiSetting"("openaiCredsId"); 9 | -------------------------------------------------------------------------------- /src/api/integrations/storage/storage.router.ts: -------------------------------------------------------------------------------- 1 | import { S3Router } from '@api/integrations/storage/s3/routes/s3.router'; 2 | import { Router } from 'express'; 3 | 4 | export class StorageRouter { 5 | public readonly router: Router; 6 | 7 | constructor(...guards: any[]) { 8 | this.router = Router(); 9 | 10 | this.router.use('/s3', new S3Router(...guards).router); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/dify/dto/dify.dto.ts: -------------------------------------------------------------------------------- 1 | import { $Enums } from '@prisma/client'; 2 | 3 | import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; 4 | 5 | export class DifyDto extends BaseChatbotDto { 6 | botType?: $Enums.DifyBotType; 7 | apiUrl?: string; 8 | apiKey?: string; 9 | } 10 | 11 | export class DifySettingDto extends BaseChatbotSettingDto { 12 | difyIdFallback?: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/config/event.config.ts: -------------------------------------------------------------------------------- 1 | import { configService, EventEmitter as EventEmitterConfig } from '@config/env.config'; 2 | import EventEmitter2 from 'eventemitter2'; 3 | 4 | const eventEmitterConfig = configService.get('EVENT_EMITTER'); 5 | 6 | export const eventEmitter = new EventEmitter2({ 7 | delimiter: '.', 8 | newListener: false, 9 | ignoreErrors: false, 10 | maxListeners: eventEmitterConfig.MAX_LISTENERS, 11 | }); 12 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240712223655_column_fallback_typebot/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "TriggerOperator" ADD VALUE 'regex'; 3 | 4 | -- AlterTable 5 | ALTER TABLE "TypebotSetting" ADD COLUMN "typebotIdFallback" VARCHAR(100); 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "TypebotSetting" ADD CONSTRAINT "TypebotSetting_typebotIdFallback_fkey" FOREIGN KEY ("typebotIdFallback") REFERENCES "Typebot"("id") ON DELETE SET NULL ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /src/validate/business.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | 3 | export const catalogSchema: JSONSchema7 = { 4 | type: 'object', 5 | properties: { 6 | number: { type: 'string' }, 7 | limit: { type: 'number' }, 8 | }, 9 | }; 10 | 11 | export const collectionsSchema: JSONSchema7 = { 12 | type: 'object', 13 | properties: { 14 | number: { type: 'string' }, 15 | limit: { type: 'number' }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240813003116_make_label_unique_for_instance/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[labelId,instanceId]` on the table `Label` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "Label_labelId_key"; 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "Label_labelId_instanceId_key" ON "Label"("labelId", "instanceId"); 12 | -------------------------------------------------------------------------------- /src/utils/instrumentSentry.ts: -------------------------------------------------------------------------------- 1 | import { configService, Sentry as SentryConfig } from '@config/env.config'; 2 | import * as Sentry from '@sentry/node'; 3 | 4 | const sentryConfig = configService.get('SENTRY'); 5 | 6 | if (sentryConfig.DSN) { 7 | Sentry.init({ 8 | dsn: sentryConfig.DSN, 9 | environment: process.env.NODE_ENV || 'development', 10 | tracesSampleRate: 1.0, 11 | profilesSampleRate: 1.0, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /Docker/redis/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | redis: 5 | image: redis:latest 6 | networks: 7 | - evolution-net 8 | container_name: redis 9 | command: > 10 | redis-server --port 6379 --appendonly yes 11 | volumes: 12 | - evolution_redis:/data 13 | ports: 14 | - 6379:6379 15 | 16 | volumes: 17 | evolution_redis: 18 | 19 | 20 | networks: 21 | evolution-net: 22 | name: evolution-net 23 | driver: bridge 24 | -------------------------------------------------------------------------------- /src/railway.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://railway.com/railway.schema.json", 3 | "build": { 4 | "builder": "DOCKERFILE", 5 | "dockerfilePath": "Dockerfile" 6 | }, 7 | "deploy": { 8 | "runtime": "V2", 9 | "numReplicas": 1, 10 | "sleepApplication": false, 11 | "multiRegionConfig": { 12 | "us-east4-eqdc4a": { 13 | "numReplicas": 1 14 | } 15 | }, 16 | "restartPolicyType": "ON_FAILURE", 17 | "restartPolicyMaxRetries": 10 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240722173259_add_name_column_to_openai_creds/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[name]` on the table `OpenaiCreds` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "OpenaiCreds" ADD COLUMN "name" VARCHAR(255), 9 | ALTER COLUMN "apiKey" DROP NOT NULL; 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "OpenaiCreds_name_key" ON "OpenaiCreds"("name"); 13 | -------------------------------------------------------------------------------- /src/api/controllers/call.controller.ts: -------------------------------------------------------------------------------- 1 | import { OfferCallDto } from '@api/dto/call.dto'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { WAMonitoringService } from '@api/services/monitor.service'; 4 | 5 | export class CallController { 6 | constructor(private readonly waMonitor: WAMonitoringService) {} 7 | 8 | public async offerCall({ instanceName }: InstanceDto, data: OfferCallDto) { 9 | return await this.waMonitor.waInstances[instanceName].offerCall(data); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { cpSync } from 'node:fs'; 2 | 3 | import { defineConfig } from 'tsup'; 4 | 5 | export default defineConfig({ 6 | entry: ['src'], 7 | outDir: 'dist', 8 | splitting: false, 9 | sourcemap: true, 10 | clean: true, 11 | minify: true, 12 | format: ['cjs', 'esm'], 13 | onSuccess: async () => { 14 | cpSync('src/utils/translations', 'dist/translations', { recursive: true }); 15 | }, 16 | loader: { 17 | '.json': 'file', 18 | '.yml': 'file', 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/n8n/dto/n8n.dto.ts: -------------------------------------------------------------------------------- 1 | import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; 2 | 3 | export class N8nDto extends BaseChatbotDto { 4 | // N8n specific fields 5 | webhookUrl?: string; 6 | basicAuthUser?: string; 7 | basicAuthPass?: string; 8 | } 9 | 10 | export class N8nSettingDto extends BaseChatbotSettingDto { 11 | // N8n has no specific fields 12 | } 13 | 14 | export class N8nMessageDto { 15 | chatInput: string; 16 | sessionId: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/api/dto/template.dto.ts: -------------------------------------------------------------------------------- 1 | export class TemplateDto { 2 | name: string; 3 | category: string; 4 | allowCategoryChange: boolean; 5 | language: string; 6 | components: any; 7 | webhookUrl?: string; 8 | } 9 | 10 | export class TemplateEditDto { 11 | templateId: string; 12 | category?: 'AUTHENTICATION' | 'MARKETING' | 'UTILITY'; 13 | allowCategoryChange?: boolean; 14 | ttl?: number; 15 | components?: any; 16 | } 17 | 18 | export class TemplateDeleteDto { 19 | name: string; 20 | hsmId?: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/typebot/dto/typebot.dto.ts: -------------------------------------------------------------------------------- 1 | import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; 2 | 3 | export class PrefilledVariables { 4 | remoteJid?: string; 5 | pushName?: string; 6 | messageType?: string; 7 | additionalData?: { [key: string]: any }; 8 | } 9 | 10 | export class TypebotDto extends BaseChatbotDto { 11 | url: string; 12 | typebot: string; 13 | } 14 | 15 | export class TypebotSettingDto extends BaseChatbotSettingDto { 16 | typebotIdFallback?: string; 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.fontSize": 13, 3 | "editor.fontLigatures": true, 4 | "editor.letterSpacing": 0.5, 5 | "editor.smoothScrolling": true, 6 | "editor.tabSize": 2, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.fixAll": "explicit" 10 | }, 11 | "prisma-smart-formatter.typescript.defaultFormatter": "esbenp.prettier-vscode", 12 | "prisma-smart-formatter.prisma.defaultFormatter": "Prisma.prisma", 13 | "i18n-ally.localesPaths": [ 14 | "store/messages" 15 | ] 16 | } -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240828140837_add_is_on_whatsapp_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "is_on_whatsapp" ( 3 | "id" TEXT NOT NULL, 4 | "remote_jid" VARCHAR(100) NOT NULL, 5 | "name" TEXT, 6 | "jid_options" TEXT NOT NULL, 7 | "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updated_at" TIMESTAMP NOT NULL, 9 | 10 | CONSTRAINT "is_on_whatsapp_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "is_on_whatsapp_remote_jid_key" ON "is_on_whatsapp"("remote_jid"); 15 | -------------------------------------------------------------------------------- /manager/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Evolution Manager 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /Docker/mysql/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | mysql: 5 | container_name: mysql 6 | image: percona/percona-server:8.0 7 | networks: 8 | - evolution-net 9 | restart: always 10 | ports: 11 | - 3306:3306 12 | environment: 13 | - MYSQL_ROOT_PASSWORD=root 14 | - TZ=America/Bahia 15 | volumes: 16 | - mysql_data:/var/lib/mysql 17 | expose: 18 | - 3306 19 | 20 | volumes: 21 | mysql_data: 22 | 23 | 24 | networks: 25 | evolution-net: 26 | name: evolution-net 27 | driver: bridge 28 | -------------------------------------------------------------------------------- /src/api/integrations/event/kafka/kafka.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | import { EventController } from '../event.controller'; 5 | 6 | export const kafkaSchema: JSONSchema7 = { 7 | $id: v4(), 8 | type: 'object', 9 | properties: { 10 | enabled: { type: 'boolean', enum: [true, false] }, 11 | events: { 12 | type: 'array', 13 | minItems: 0, 14 | items: { 15 | type: 'string', 16 | enum: EventController.events, 17 | }, 18 | }, 19 | }, 20 | required: ['enabled'], 21 | }; 22 | -------------------------------------------------------------------------------- /Docker/scripts/env_functions.sh: -------------------------------------------------------------------------------- 1 | export_env_vars() { 2 | if [ -f .env ]; then 3 | while IFS='=' read -r key value; do 4 | if [[ -z "$key" || "$key" =~ ^\s*# || -z "$value" ]]; then 5 | continue 6 | fi 7 | 8 | key=$(echo "$key" | tr -d '[:space:]') 9 | value=$(echo "$value" | tr -d '[:space:]') 10 | value=$(echo "$value" | tr -d "'" | tr -d "\"") 11 | 12 | export "$key=$value" 13 | done < .env 14 | else 15 | echo ".env file not found" 16 | exit 1 17 | fi 18 | } 19 | -------------------------------------------------------------------------------- /src/api/abstract/abstract.cache.ts: -------------------------------------------------------------------------------- 1 | export interface ICache { 2 | get(key: string): Promise; 3 | 4 | hGet(key: string, field: string): Promise; 5 | 6 | set(key: string, value: any, ttl?: number): void; 7 | 8 | hSet(key: string, field: string, value: any): Promise; 9 | 10 | has(key: string): Promise; 11 | 12 | keys(appendCriteria?: string): Promise; 13 | 14 | delete(key: string | string[]): Promise; 15 | 16 | hDelete(key: string, field: string): Promise; 17 | 18 | deleteAll(appendCriteria?: string): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /src/validate/validate.schema.ts: -------------------------------------------------------------------------------- 1 | // Integrations Schema 2 | export * from './business.schema'; 3 | export * from './chat.schema'; 4 | export * from './group.schema'; 5 | export * from './instance.schema'; 6 | export * from './label.schema'; 7 | export * from './message.schema'; 8 | export * from './proxy.schema'; 9 | export * from './settings.schema'; 10 | export * from './template.schema'; 11 | export * from './templateDelete.schema'; 12 | export * from './templateEdit.schema'; 13 | export * from '@api/integrations/chatbot/chatbot.schema'; 14 | export * from '@api/integrations/event/event.schema'; 15 | -------------------------------------------------------------------------------- /src/config/error.config.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from './logger.config'; 2 | 3 | export function onUnexpectedError() { 4 | process.on('uncaughtException', (error, origin) => { 5 | const logger = new Logger('uncaughtException'); 6 | logger.error({ 7 | origin, 8 | stderr: process.stderr.fd, 9 | error, 10 | }); 11 | }); 12 | 13 | process.on('unhandledRejection', (error, origin) => { 14 | const logger = new Logger('unhandledRejection'); 15 | logger.error({ 16 | origin, 17 | stderr: process.stderr.fd, 18 | }); 19 | logger.error(error); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240811183328_add_unique_index_for_remoted_jid_and_instance_in_contacts/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Contact` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- Remove the duplicates 8 | DELETE FROM "Contact" 9 | WHERE ctid NOT IN ( 10 | SELECT min(ctid) 11 | FROM "Contact" 12 | GROUP BY "remoteJid", "instanceId" 13 | ); 14 | 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "Contact_remoteJid_instanceId_key" ON "Contact"("remoteJid", "instanceId"); 18 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20251122003044_add_chat_instance_remotejid_unique/migration.sql: -------------------------------------------------------------------------------- 1 | -- 1. Cleanup: Remove duplicate chats, keeping the most recently updated one 2 | DELETE FROM "Chat" 3 | WHERE id IN ( 4 | SELECT id FROM ( 5 | SELECT id, 6 | ROW_NUMBER() OVER ( 7 | PARTITION BY "instanceId", "remoteJid" 8 | ORDER BY "updatedAt" DESC 9 | ) as row_num 10 | FROM "Chat" 11 | ) t 12 | WHERE t.row_num > 1 13 | ); 14 | 15 | -- 2. Create the unique index (Constraint) 16 | CREATE UNIQUE INDEX "Chat_instanceId_remoteJid_key" ON "Chat"("instanceId", "remoteJid"); 17 | -------------------------------------------------------------------------------- /Docker/rabbitmq/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | rabbitmq: 5 | container_name: rabbitmq 6 | image: rabbitmq:management 7 | environment: 8 | - RABBITMQ_ERLANG_COOKIE=33H2CdkzF5WrnJ4ud6nkUdRTKXvbCHeFjvVL71p 9 | - RABBITMQ_DEFAULT_VHOST=default 10 | - RABBITMQ_DEFAULT_USER=USER 11 | - RABBITMQ_DEFAULT_PASS=PASSWORD 12 | volumes: 13 | - rabbitmq_data:/var/lib/rabbitmq/ 14 | ports: 15 | - 5672:5672 16 | - 15672:15672 17 | 18 | volumes: 19 | rabbitmq_data: 20 | 21 | 22 | networks: 23 | evolution-net: 24 | name: evolution-net 25 | driver: bridge 26 | -------------------------------------------------------------------------------- /src/api/routes/view.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import express, { Router } from 'express'; 3 | import path from 'path'; 4 | 5 | export class ViewsRouter extends RouterBroker { 6 | public readonly router: Router; 7 | 8 | constructor() { 9 | super(); 10 | this.router = Router(); 11 | 12 | const basePath = path.join(process.cwd(), 'manager', 'dist'); 13 | const indexPath = path.join(basePath, 'index.html'); 14 | 15 | this.router.use(express.static(basePath)); 16 | 17 | this.router.get('*', (req, res) => { 18 | res.sendFile(indexPath); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/api/controllers/settings.controller.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { SettingsDto } from '@api/dto/settings.dto'; 3 | import { SettingsService } from '@api/services/settings.service'; 4 | 5 | export class SettingsController { 6 | constructor(private readonly settingsService: SettingsService) {} 7 | 8 | public async createSettings(instance: InstanceDto, data: SettingsDto) { 9 | return this.settingsService.create(instance, data); 10 | } 11 | 12 | public async findSettings(instance: InstanceDto) { 13 | const settings = this.settingsService.find(instance); 14 | return settings; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/chatbot.schema.ts: -------------------------------------------------------------------------------- 1 | export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema'; 2 | export * from '@api/integrations/chatbot/dify/validate/dify.schema'; 3 | export * from '@api/integrations/chatbot/evoai/validate/evoai.schema'; 4 | export * from '@api/integrations/chatbot/evolutionBot/validate/evolutionBot.schema'; 5 | export * from '@api/integrations/chatbot/flowise/validate/flowise.schema'; 6 | export * from '@api/integrations/chatbot/n8n/validate/n8n.schema'; 7 | export * from '@api/integrations/chatbot/openai/validate/openai.schema'; 8 | export * from '@api/integrations/chatbot/typebot/validate/typebot.schema'; 9 | -------------------------------------------------------------------------------- /Dockerfile.metrics: -------------------------------------------------------------------------------- 1 | FROM evoapicloud/evolution-api:latest AS base 2 | WORKDIR /evolution 3 | 4 | # Copiamos apenas o necessário para recompilar o dist com as mudanças locais 5 | COPY tsconfig.json tsup.config.ts package.json ./ 6 | COPY src ./src 7 | 8 | # Recompila usando os node_modules já presentes na imagem base 9 | RUN npm run build 10 | 11 | # Runtime final: reaproveita a imagem oficial e apenas sobrepõe o dist 12 | FROM evoapicloud/evolution-api:latest AS final 13 | WORKDIR /evolution 14 | COPY --from=base /evolution/dist ./dist 15 | 16 | ENV PROMETHEUS_METRICS=true 17 | 18 | # Entrada original da imagem oficial já sobe o app em /evolution 19 | 20 | -------------------------------------------------------------------------------- /src/api/integrations/storage/s3/controllers/s3.controller.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { MediaDto } from '@api/integrations/storage/s3/dto/media.dto'; 3 | import { S3Service } from '@api/integrations/storage/s3/services/s3.service'; 4 | 5 | export class S3Controller { 6 | constructor(private readonly s3Service: S3Service) {} 7 | 8 | public async getMedia(instance: InstanceDto, data: MediaDto) { 9 | return this.s3Service.getMedia(instance, data); 10 | } 11 | 12 | public async getMediaUrl(instance: InstanceDto, data: MediaDto) { 13 | return this.s3Service.getMediaUrl(instance, data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/api/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { PrismaRepository } from '@api/repository/repository.service'; 2 | import { BadRequestException } from '@exceptions'; 3 | 4 | export class AuthService { 5 | constructor(private readonly prismaRepository: PrismaRepository) {} 6 | 7 | public async checkDuplicateToken(token: string) { 8 | if (!token) { 9 | return true; 10 | } 11 | 12 | const instances = await this.prismaRepository.instance.findMany({ 13 | where: { token }, 14 | }); 15 | 16 | if (instances.length > 0) { 17 | throw new BadRequestException('Token already exists'); 18 | } 19 | 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/api/controllers/label.controller.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { HandleLabelDto } from '@api/dto/label.dto'; 3 | import { WAMonitoringService } from '@api/services/monitor.service'; 4 | 5 | export class LabelController { 6 | constructor(private readonly waMonitor: WAMonitoringService) {} 7 | 8 | public async fetchLabels({ instanceName }: InstanceDto) { 9 | return await this.waMonitor.waInstances[instanceName].fetchLabels(); 10 | } 11 | 12 | public async handleLabel({ instanceName }: InstanceDto, data: HandleLabelDto) { 13 | return await this.waMonitor.waInstances[instanceName].handleLabel(data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/api/integrations/channel/channel.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { EvolutionRouter } from './evolution/evolution.router'; 4 | import { MetaRouter } from './meta/meta.router'; 5 | import { BaileysRouter } from './whatsapp/baileys.router'; 6 | 7 | export class ChannelRouter { 8 | public readonly router: Router; 9 | 10 | constructor(configService: any, ...guards: any[]) { 11 | this.router = Router(); 12 | 13 | this.router.use('/', new EvolutionRouter(configService).router); 14 | this.router.use('/', new MetaRouter(configService).router); 15 | this.router.use('/baileys', new BaileysRouter(...guards).router); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20250225180031_add_nats_integration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Nats" ( 3 | "id" TEXT NOT NULL, 4 | "enabled" BOOLEAN NOT NULL DEFAULT false, 5 | "events" JSONB NOT NULL, 6 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP NOT NULL, 8 | "instanceId" TEXT NOT NULL, 9 | 10 | CONSTRAINT "Nats_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "Nats_instanceId_key" ON "Nats"("instanceId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Nats" ADD CONSTRAINT "Nats_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20250918182355_add_kafka_integration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Kafka" ( 3 | "id" TEXT NOT NULL, 4 | "enabled" BOOLEAN NOT NULL DEFAULT false, 5 | "events" JSONB NOT NULL, 6 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP NOT NULL, 8 | "instanceId" TEXT NOT NULL, 9 | 10 | CONSTRAINT "Kafka_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "Kafka_instanceId_key" ON "Kafka"("instanceId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Kafka" ADD CONSTRAINT "Kafka_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/openai/dto/openai.dto.ts: -------------------------------------------------------------------------------- 1 | import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; 2 | 3 | export class OpenaiCredsDto { 4 | name: string; 5 | apiKey: string; 6 | } 7 | 8 | export class OpenaiDto extends BaseChatbotDto { 9 | openaiCredsId: string; 10 | botType: string; 11 | assistantId?: string; 12 | functionUrl?: string; 13 | model?: string; 14 | systemMessages?: string[]; 15 | assistantMessages?: string[]; 16 | userMessages?: string[]; 17 | maxTokens?: number; 18 | } 19 | 20 | export class OpenaiSettingDto extends BaseChatbotSettingDto { 21 | openaiCredsId?: string; 22 | openaiIdFallback?: string; 23 | speechToText?: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /Docker/scripts/generate_database.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./Docker/scripts/env_functions.sh 4 | 5 | if [ "$DOCKER_ENV" != "true" ]; then 6 | export_env_vars 7 | fi 8 | 9 | if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then 10 | export DATABASE_URL 11 | echo "Generating database for $DATABASE_PROVIDER" 12 | echo "Database URL: $DATABASE_URL" 13 | npm run db:generate 14 | if [ $? -ne 0 ]; then 15 | echo "Prisma generate failed" 16 | exit 1 17 | else 18 | echo "Prisma generate succeeded" 19 | fi 20 | else 21 | echo "Error: Database provider $DATABASE_PROVIDER invalid." 22 | exit 1 23 | fi -------------------------------------------------------------------------------- /prisma/mysql-migrations/20250225180031_add_nats_integration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `Nats` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `enabled` BOOLEAN NOT NULL DEFAULT false, 5 | `events` JSON NOT NULL, 6 | `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 7 | `updatedAt` TIMESTAMP NOT NULL, 8 | `instanceId` VARCHAR(191) NOT NULL, 9 | 10 | PRIMARY KEY (`id`) 11 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX `Nats_instanceId_key` ON `Nats`(`instanceId`); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE `Nats` ADD CONSTRAINT `Nats_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240830193533_changed_table_case/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `is_on_whatsapp` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "is_on_whatsapp"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "IsOnWhatsapp" ( 12 | "id" TEXT NOT NULL, 13 | "remoteJid" VARCHAR(100) NOT NULL, 14 | "jidOptions" TEXT NOT NULL, 15 | "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updatedAt" TIMESTAMP NOT NULL, 17 | 18 | CONSTRAINT "IsOnWhatsapp_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "IsOnWhatsapp_remoteJid_key" ON "IsOnWhatsapp"("remoteJid"); 23 | -------------------------------------------------------------------------------- /src/api/integrations/channel/evolution/evolution.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { evolutionController } from '@api/server.module'; 3 | import { ConfigService } from '@config/env.config'; 4 | import { Router } from 'express'; 5 | 6 | export class EvolutionRouter extends RouterBroker { 7 | constructor(readonly configService: ConfigService) { 8 | super(); 9 | this.router.post(this.routerPath('webhook/evolution', false), async (req, res) => { 10 | const { body } = req; 11 | const response = await evolutionController.receiveWebhook(body); 12 | 13 | return res.status(200).json(response); 14 | }); 15 | } 16 | 17 | public readonly router: Router = Router(); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Repo 2 | Baileys 3 | # compiled output 4 | /dist 5 | /node_modules 6 | 7 | /Docker/.env 8 | 9 | # Logs 10 | logs/**.json 11 | *.log 12 | npm-debug.log* 13 | pnpm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | 18 | /docker-compose-data 19 | /docker-data 20 | 21 | # Package 22 | /yarn.lock 23 | /pnpm-lock.yaml 24 | 25 | # IDEs 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .nova/* 32 | .idea/* 33 | 34 | # Project related 35 | /instances/* 36 | !/instances/.gitkeep 37 | /test/ 38 | /src/env.yml 39 | /store 40 | *.env 41 | 42 | /temp/* 43 | 44 | .DS_Store 45 | *.DS_Store 46 | .tool-versions 47 | 48 | /prisma/migrations/* 49 | -------------------------------------------------------------------------------- /Docker/minio/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | minio: 5 | container_name: minio 6 | image: quay.io/minio/minio 7 | networks: 8 | - evolution-net 9 | command: server /data --console-address ":9001" 10 | restart: always 11 | ports: 12 | - 5432:5432 13 | environment: 14 | - MINIO_ROOT_USER=USER 15 | - MINIO_ROOT_PASSWORD=PASSWORD 16 | - MINIO_BROWSER_REDIRECT_URL=http:/localhost:9001 17 | - MINIO_SERVER_URL=http://localhost:9000 18 | volumes: 19 | - minio_data:/data 20 | expose: 21 | - 9000 22 | - 9001 23 | 24 | volumes: 25 | minio_data: 26 | 27 | 28 | networks: 29 | evolution-net: 30 | name: evolution-net 31 | driver: bridge 32 | -------------------------------------------------------------------------------- /src/api/controllers/business.controller.ts: -------------------------------------------------------------------------------- 1 | import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { WAMonitoringService } from '@api/services/monitor.service'; 4 | 5 | export class BusinessController { 6 | constructor(private readonly waMonitor: WAMonitoringService) {} 7 | 8 | public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) { 9 | return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data); 10 | } 11 | 12 | public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) { 13 | return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/server-up.ts: -------------------------------------------------------------------------------- 1 | import { configService, SslConf } from '@config/env.config'; 2 | import { Express } from 'express'; 3 | import { readFileSync } from 'fs'; 4 | import * as http from 'http'; 5 | import * as https from 'https'; 6 | 7 | export class ServerUP { 8 | static #app: Express; 9 | 10 | static set app(e: Express) { 11 | this.#app = e; 12 | } 13 | 14 | static get https() { 15 | const { FULLCHAIN, PRIVKEY } = configService.get('SSL_CONF'); 16 | return https.createServer( 17 | { 18 | cert: readFileSync(FULLCHAIN), 19 | key: readFileSync(PRIVKEY), 20 | }, 21 | ServerUP.#app, 22 | ); 23 | } 24 | 25 | static get http() { 26 | return http.createServer(ServerUP.#app); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docker-compose.dev.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | container_name: evolution_api 4 | image: evolution/api:local 5 | build: . 6 | restart: always 7 | ports: 8 | - 8080:8080 9 | volumes: 10 | - evolution_instances:/evolution/instances 11 | networks: 12 | - evolution-net 13 | env_file: 14 | - .env 15 | expose: 16 | - 8080 17 | 18 | frontend: 19 | container_name: evolution_frontend 20 | image: evolution/manager:local 21 | build: ./evolution-manager-v2 22 | restart: always 23 | ports: 24 | - "3000:80" 25 | networks: 26 | - evolution-net 27 | 28 | volumes: 29 | evolution_instances: 30 | 31 | 32 | networks: 33 | evolution-net: 34 | name: evolution-net 35 | driver: bridge 36 | -------------------------------------------------------------------------------- /src/api/repository/repository.service.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@config/env.config'; 2 | import { Logger } from '@config/logger.config'; 3 | import { PrismaClient } from '@prisma/client'; 4 | 5 | export class Query { 6 | where?: T; 7 | sort?: 'asc' | 'desc'; 8 | page?: number; 9 | offset?: number; 10 | } 11 | 12 | export class PrismaRepository extends PrismaClient { 13 | constructor(private readonly configService: ConfigService) { 14 | super(); 15 | } 16 | 17 | private readonly logger = new Logger('PrismaRepository'); 18 | 19 | public async onModuleInit() { 20 | await this.$connect(); 21 | this.logger.info('Repository:Prisma - ON'); 22 | } 23 | 24 | public async onModuleDestroy() { 25 | await this.$disconnect(); 26 | this.logger.warn('Repository:Prisma - OFF'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240725184147_create_template_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Template" ( 3 | "id" TEXT NOT NULL, 4 | "templateId" VARCHAR(255) NOT NULL, 5 | "name" VARCHAR(255) NOT NULL, 6 | "template" JSONB NOT NULL, 7 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP NOT NULL, 9 | "instanceId" TEXT NOT NULL, 10 | 11 | CONSTRAINT "Template_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "Template_templateId_key" ON "Template"("templateId"); 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "Template_name_key" ON "Template"("name"); 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "Template" ADD CONSTRAINT "Template_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 22 | -------------------------------------------------------------------------------- /prisma/mysql-migrations/20250510035200_add_wavoip_token_to_settings_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | 8 | -- AlterTable 9 | SET @column_exists := ( 10 | SELECT COUNT(*) 11 | FROM information_schema.columns 12 | WHERE table_schema = DATABASE() 13 | AND table_name = 'Setting' 14 | AND column_name = 'wavoipToken' 15 | ); 16 | 17 | SET @sql := IF(@column_exists = 0, 18 | 'ALTER TABLE Setting ADD COLUMN wavoipToken VARCHAR(100);', 19 | 'SELECT "Column already exists";' 20 | ); 21 | 22 | PREPARE stmt FROM @sql; 23 | EXECUTE stmt; 24 | DEALLOCATE PREPARE stmt; 25 | 26 | ALTER TABLE Chat ADD CONSTRAINT unique_remote_instance UNIQUE (remoteJid, instanceId); 27 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240712150256_create_templates_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Template" ( 3 | "id" TEXT NOT NULL, 4 | "name" VARCHAR(255) NOT NULL, 5 | "language" VARCHAR(255) NOT NULL, 6 | "templateId" VARCHAR(255) NOT NULL, 7 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP NOT NULL, 9 | "instanceId" TEXT NOT NULL, 10 | 11 | CONSTRAINT "Template_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "Template_templateId_key" ON "Template"("templateId"); 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "Template_instanceId_key" ON "Template"("instanceId"); 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "Template" ADD CONSTRAINT "Template_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 22 | -------------------------------------------------------------------------------- /src/validate/templateDelete.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 5 | const properties: Record = {}; 6 | propertyNames.forEach( 7 | (property) => 8 | (properties[property] = { 9 | minLength: 1, 10 | description: `The "${property}" cannot be empty`, 11 | }), 12 | ); 13 | return { 14 | if: { 15 | propertyNames: { 16 | enum: [...propertyNames], 17 | }, 18 | }, 19 | then: { properties }, 20 | } as JSONSchema7; 21 | }; 22 | 23 | export const templateDeleteSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | name: { type: 'string' }, 28 | hsmId: { type: 'string' }, 29 | }, 30 | required: ['name'], 31 | ...isNotEmpty('name'), 32 | }; 33 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20241011085129_create_pusher_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Pusher" ( 3 | "id" TEXT NOT NULL, 4 | "enabled" BOOLEAN NOT NULL DEFAULT false, 5 | "appId" VARCHAR(100) NOT NULL, 6 | "key" VARCHAR(100) NOT NULL, 7 | "secret" VARCHAR(100) NOT NULL, 8 | "cluster" VARCHAR(100) NOT NULL, 9 | "useTLS" BOOLEAN NOT NULL DEFAULT false, 10 | "events" JSONB NOT NULL, 11 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 12 | "updatedAt" TIMESTAMP NOT NULL, 13 | "instanceId" TEXT NOT NULL, 14 | 15 | CONSTRAINT "Pusher_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- CreateIndex 19 | CREATE UNIQUE INDEX "Pusher_instanceId_key" ON "Pusher"("instanceId"); 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "Pusher" ADD CONSTRAINT "Pusher_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20241017144950_create_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "Chat_instanceId_idx" ON "Chat"("instanceId"); 3 | 4 | -- CreateIndex 5 | CREATE INDEX "Chat_remoteJid_idx" ON "Chat"("remoteJid"); 6 | 7 | -- CreateIndex 8 | CREATE INDEX "Contact_remoteJid_idx" ON "Contact"("remoteJid"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "Contact_instanceId_idx" ON "Contact"("instanceId"); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "Message_instanceId_idx" ON "Message"("instanceId"); 15 | 16 | -- CreateIndex 17 | CREATE INDEX "MessageUpdate_instanceId_idx" ON "MessageUpdate"("instanceId"); 18 | 19 | -- CreateIndex 20 | CREATE INDEX "MessageUpdate_messageId_idx" ON "MessageUpdate"("messageId"); 21 | 22 | -- CreateIndex 23 | CREATE INDEX "Setting_instanceId_idx" ON "Setting"("instanceId"); 24 | 25 | -- CreateIndex 26 | CREATE INDEX "Webhook_instanceId_idx" ON "Webhook"("instanceId"); 27 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService, Language } from '@config/env.config'; 2 | import fs from 'fs'; 3 | import i18next from 'i18next'; 4 | import path from 'path'; 5 | 6 | const languages = ['en', 'pt-BR', 'es']; 7 | const translationsPath = path.join(__dirname, 'translations'); 8 | const configService: ConfigService = new ConfigService(); 9 | 10 | const resources: any = {}; 11 | 12 | languages.forEach((language) => { 13 | const languagePath = path.join(translationsPath, `${language}.json`); 14 | if (fs.existsSync(languagePath)) { 15 | const translationContent = fs.readFileSync(languagePath, 'utf8'); 16 | resources[language] = { 17 | translation: JSON.parse(translationContent), 18 | }; 19 | } 20 | }); 21 | 22 | i18next.init({ 23 | resources, 24 | fallbackLng: 'en', 25 | lng: configService.get('LANGUAGE'), 26 | debug: false, 27 | 28 | interpolation: { 29 | escapeValue: false, 30 | }, 31 | }); 32 | export default i18next; 33 | -------------------------------------------------------------------------------- /Docker/postgres/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | postgres: 5 | container_name: postgres 6 | image: postgres:15 7 | networks: 8 | - evolution-net 9 | command: ["postgres", "-c", "max_connections=1000"] 10 | restart: always 11 | ports: 12 | - 5432:5432 13 | environment: 14 | - POSTGRES_PASSWORD=PASSWORD 15 | volumes: 16 | - postgres_data:/var/lib/postgresql/data 17 | expose: 18 | - 5432 19 | 20 | pgadmin: 21 | image: dpage/pgadmin4:latest 22 | networks: 23 | - evolution-net 24 | environment: 25 | - PGADMIN_DEFAULT_EMAIL=EMAIL 26 | - PGADMIN_DEFAULT_PASSWORD=PASSWORD 27 | volumes: 28 | - pgadmin_data:/var/lib/pgadmin 29 | ports: 30 | - 4000:80 31 | links: 32 | - postgres 33 | 34 | volumes: 35 | postgres_data: 36 | pgadmin_data: 37 | 38 | 39 | networks: 40 | evolution-net: 41 | name: evolution-net 42 | driver: bridge 43 | -------------------------------------------------------------------------------- /src/api/routes/call.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { OfferCallDto } from '@api/dto/call.dto'; 3 | import { callController } from '@api/server.module'; 4 | import { offerCallSchema } from '@validate/validate.schema'; 5 | import { RequestHandler, Router } from 'express'; 6 | 7 | import { HttpStatus } from './index.router'; 8 | 9 | export class CallRouter extends RouterBroker { 10 | constructor(...guards: RequestHandler[]) { 11 | super(); 12 | this.router.post(this.routerPath('offer'), ...guards, async (req, res) => { 13 | const response = await this.dataValidate({ 14 | request: req, 15 | schema: offerCallSchema, 16 | ClassRef: OfferCallDto, 17 | execute: (instance, data) => callController.offerCall(instance, data), 18 | }); 19 | 20 | return res.status(HttpStatus.CREATED).json(response); 21 | }); 22 | } 23 | 24 | public readonly router: Router = Router(); 25 | } 26 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240713184337_add_media_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Media" ( 3 | "id" TEXT NOT NULL, 4 | "fileName" VARCHAR(500) NOT NULL, 5 | "type" VARCHAR(100) NOT NULL, 6 | "mimetype" VARCHAR(100) NOT NULL, 7 | "createdAt" DATE DEFAULT CURRENT_TIMESTAMP, 8 | "messageId" TEXT NOT NULL, 9 | "instanceId" TEXT NOT NULL, 10 | 11 | CONSTRAINT "Media_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "Media_fileName_key" ON "Media"("fileName"); 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "Media_messageId_key" ON "Media"("messageId"); 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "Media" ADD CONSTRAINT "Media_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "Media" ADD CONSTRAINT "Media_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /Docker/scripts/deploy_database.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./Docker/scripts/env_functions.sh 4 | 5 | if [ "$DOCKER_ENV" != "true" ]; then 6 | export_env_vars 7 | fi 8 | 9 | if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then 10 | export DATABASE_URL 11 | echo "Deploying migrations for $DATABASE_PROVIDER" 12 | echo "Database URL: $DATABASE_URL" 13 | # rm -rf ./prisma/migrations 14 | # cp -r ./prisma/$DATABASE_PROVIDER-migrations ./prisma/migrations 15 | npm run db:deploy 16 | if [ $? -ne 0 ]; then 17 | echo "Migration failed" 18 | exit 1 19 | else 20 | echo "Migration succeeded" 21 | fi 22 | npm run db:generate 23 | if [ $? -ne 0 ]; then 24 | echo "Prisma generate failed" 25 | exit 1 26 | else 27 | echo "Prisma generate succeeded" 28 | fi 29 | else 30 | echo "Error: Database provider $DATABASE_PROVIDER invalid." 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240821120816_bot_id_integration_session/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `difyId` on the `IntegrationSession` table. All the data in the column will be lost. 5 | - You are about to drop the column `openaiBotId` on the `IntegrationSession` table. All the data in the column will be lost. 6 | - You are about to drop the column `typebotId` on the `IntegrationSession` table. All the data in the column will be lost. 7 | 8 | */ 9 | -- DropForeignKey 10 | ALTER TABLE "IntegrationSession" DROP CONSTRAINT "IntegrationSession_difyId_fkey"; 11 | 12 | -- DropForeignKey 13 | ALTER TABLE "IntegrationSession" DROP CONSTRAINT "IntegrationSession_openaiBotId_fkey"; 14 | 15 | -- DropForeignKey 16 | ALTER TABLE "IntegrationSession" DROP CONSTRAINT "IntegrationSession_typebotId_fkey"; 17 | 18 | -- AlterTable 19 | ALTER TABLE "IntegrationSession" DROP COLUMN "difyId", 20 | DROP COLUMN "openaiBotId", 21 | DROP COLUMN "typebotId", 22 | ADD COLUMN "botId" TEXT; 23 | -------------------------------------------------------------------------------- /src/utils/sendTelemetry.ts: -------------------------------------------------------------------------------- 1 | import { configService, Telemetry } from '@config/env.config'; 2 | import axios from 'axios'; 3 | import fs from 'fs'; 4 | 5 | const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 6 | 7 | export interface TelemetryData { 8 | route: string; 9 | apiVersion: string; 10 | timestamp: Date; 11 | } 12 | 13 | export const sendTelemetry = async (route: string): Promise => { 14 | const telemetryConfig = configService.get('TELEMETRY'); 15 | 16 | if (!telemetryConfig.ENABLED) { 17 | return; 18 | } 19 | 20 | if (route === '/') { 21 | return; 22 | } 23 | 24 | const telemetry: TelemetryData = { 25 | route, 26 | apiVersion: `${packageJson.version}`, 27 | timestamp: new Date(), 28 | }; 29 | 30 | const url = 31 | telemetryConfig.URL && telemetryConfig.URL !== '' ? telemetryConfig.URL : 'https://log.evolution-api.com/telemetry'; 32 | 33 | axios 34 | .post(url, telemetry) 35 | .then(() => {}) 36 | .catch(() => {}); 37 | }; 38 | -------------------------------------------------------------------------------- /.github/workflows/check_code_quality.yml: -------------------------------------------------------------------------------- 1 | name: Check Code Quality 2 | 3 | on: 4 | pull_request: 5 | branches: [ main, develop ] 6 | push: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | check-lint-and-build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | 14 | steps: 15 | - uses: actions/checkout@v5 16 | with: 17 | submodules: recursive 18 | 19 | - name: Install Node 20 | uses: actions/setup-node@v5 21 | with: 22 | node-version: 20.x 23 | 24 | - name: Cache node modules 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.npm 28 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 29 | restore-keys: | 30 | ${{ runner.os }}-node- 31 | 32 | - name: Install packages 33 | run: npm ci 34 | 35 | - name: Check linting 36 | run: npm run lint:check 37 | 38 | - name: Generate Prisma client 39 | run: npm run db:generate 40 | 41 | - name: Check build 42 | run: npm run build -------------------------------------------------------------------------------- /src/api/controllers/template.controller.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { TemplateDto } from '@api/dto/template.dto'; 3 | import { TemplateService } from '@api/services/template.service'; 4 | 5 | export class TemplateController { 6 | constructor(private readonly templateService: TemplateService) {} 7 | 8 | public async createTemplate(instance: InstanceDto, data: TemplateDto) { 9 | return this.templateService.create(instance, data); 10 | } 11 | 12 | public async findTemplate(instance: InstanceDto) { 13 | return this.templateService.find(instance); 14 | } 15 | 16 | public async editTemplate( 17 | instance: InstanceDto, 18 | data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number }, 19 | ) { 20 | return this.templateService.edit(instance, data); 21 | } 22 | 23 | public async deleteTemplate(instance: InstanceDto, data: { name: string; hsmId?: string }) { 24 | return this.templateService.delete(instance, data); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/api/integrations/channel/meta/meta.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { metaController } from '@api/server.module'; 3 | import { ConfigService, WaBusiness } from '@config/env.config'; 4 | import { Router } from 'express'; 5 | 6 | export class MetaRouter extends RouterBroker { 7 | constructor(readonly configService: ConfigService) { 8 | super(); 9 | this.router 10 | .get(this.routerPath('webhook/meta', false), async (req, res) => { 11 | if (req.query['hub.verify_token'] === configService.get('WA_BUSINESS').TOKEN_WEBHOOK) 12 | res.send(req.query['hub.challenge']); 13 | else res.send('Error, wrong validation token'); 14 | }) 15 | .post(this.routerPath('webhook/meta', false), async (req, res) => { 16 | const { body } = req; 17 | const response = await metaController.receiveWebhook(body); 18 | 19 | return res.status(200).json(response); 20 | }); 21 | } 22 | 23 | public readonly router: Router = Router(); 24 | } 25 | -------------------------------------------------------------------------------- /src/validate/proxy.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 5 | const properties = {}; 6 | propertyNames.forEach( 7 | (property) => 8 | (properties[property] = { 9 | minLength: 1, 10 | description: `The "${property}" cannot be empty`, 11 | }), 12 | ); 13 | return { 14 | if: { 15 | propertyNames: { 16 | enum: [...propertyNames], 17 | }, 18 | }, 19 | then: { properties }, 20 | }; 21 | }; 22 | 23 | export const proxySchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | enabled: { type: 'boolean', enum: [true, false] }, 28 | host: { type: 'string' }, 29 | port: { type: 'string' }, 30 | protocol: { type: 'string' }, 31 | username: { type: 'string' }, 32 | password: { type: 'string' }, 33 | }, 34 | required: ['enabled', 'host', 'port', 'protocol'], 35 | ...isNotEmpty('enabled', 'host', 'port', 'protocol'), 36 | }; 37 | -------------------------------------------------------------------------------- /src/api/services/proxy.service.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { ProxyDto } from '@api/dto/proxy.dto'; 3 | import { Logger } from '@config/logger.config'; 4 | import { Proxy } from '@prisma/client'; 5 | 6 | import { WAMonitoringService } from './monitor.service'; 7 | 8 | export class ProxyService { 9 | constructor(private readonly waMonitor: WAMonitoringService) {} 10 | 11 | private readonly logger = new Logger('ProxyService'); 12 | 13 | public create(instance: InstanceDto, data: ProxyDto) { 14 | this.waMonitor.waInstances[instance.instanceName].setProxy(data); 15 | 16 | return { proxy: { ...instance, proxy: data } }; 17 | } 18 | 19 | public async find(instance: InstanceDto): Promise { 20 | try { 21 | const result = await this.waMonitor.waInstances[instance.instanceName].findProxy(); 22 | 23 | if (Object.keys(result).length === 0) { 24 | throw new Error('Proxy not found'); 25 | } 26 | 27 | return result; 28 | } catch { 29 | return null; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/cache/cacheengine.ts: -------------------------------------------------------------------------------- 1 | import { ICache } from '@api/abstract/abstract.cache'; 2 | import { CacheConf, ConfigService } from '@config/env.config'; 3 | import { Logger } from '@config/logger.config'; 4 | 5 | import { LocalCache } from './localcache'; 6 | import { RedisCache } from './rediscache'; 7 | 8 | const logger = new Logger('CacheEngine'); 9 | 10 | export class CacheEngine { 11 | private engine: ICache; 12 | 13 | constructor( 14 | private readonly configService: ConfigService, 15 | module: string, 16 | ) { 17 | const cacheConf = configService.get('CACHE'); 18 | 19 | if (cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') { 20 | logger.verbose(`RedisCache initialized for ${module}`); 21 | this.engine = new RedisCache(configService, module); 22 | } else if (cacheConf?.LOCAL?.ENABLED) { 23 | logger.verbose(`LocalCache initialized for ${module}`); 24 | this.engine = new LocalCache(configService, module); 25 | } 26 | } 27 | 28 | public getEngine() { 29 | return this.engine; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/validate/templateEdit.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 5 | const properties: Record = {}; 6 | propertyNames.forEach( 7 | (property) => 8 | (properties[property] = { 9 | minLength: 1, 10 | description: `The "${property}" cannot be empty`, 11 | }), 12 | ); 13 | return { 14 | if: { 15 | propertyNames: { 16 | enum: [...propertyNames], 17 | }, 18 | }, 19 | then: { properties }, 20 | } as JSONSchema7; 21 | }; 22 | 23 | export const templateEditSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | templateId: { type: 'string' }, 28 | category: { type: 'string', enum: ['AUTHENTICATION', 'MARKETING', 'UTILITY'] }, 29 | allowCategoryChange: { type: 'boolean' }, 30 | ttl: { type: 'number' }, 31 | components: { type: 'array' }, 32 | }, 33 | required: ['templateId'], 34 | ...isNotEmpty('templateId'), 35 | }; 36 | -------------------------------------------------------------------------------- /src/api/services/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { SettingsDto } from '@api/dto/settings.dto'; 3 | import { Logger } from '@config/logger.config'; 4 | 5 | import { WAMonitoringService } from './monitor.service'; 6 | 7 | export class SettingsService { 8 | constructor(private readonly waMonitor: WAMonitoringService) {} 9 | 10 | private readonly logger = new Logger('SettingsService'); 11 | 12 | public async create(instance: InstanceDto, data: SettingsDto) { 13 | await this.waMonitor.waInstances[instance.instanceName].setSettings(data); 14 | 15 | return { settings: { ...instance, settings: data } }; 16 | } 17 | 18 | public async find(instance: InstanceDto): Promise { 19 | try { 20 | const result = await this.waMonitor.waInstances[instance.instanceName].findSettings(); 21 | 22 | if (Object.keys(result).length === 0) { 23 | throw new Error('Settings not found'); 24 | } 25 | 26 | return result; 27 | } catch { 28 | return null; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/validate/label.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7, JSONSchema7Definition } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 5 | const properties = {}; 6 | propertyNames.forEach( 7 | (property) => 8 | (properties[property] = { 9 | minLength: 1, 10 | description: `The "${property}" cannot be empty`, 11 | }), 12 | ); 13 | return { 14 | if: { 15 | propertyNames: { 16 | enum: [...propertyNames], 17 | }, 18 | }, 19 | then: { properties }, 20 | }; 21 | }; 22 | 23 | const numberDefinition: JSONSchema7Definition = { 24 | type: 'string', 25 | description: 'Invalid format', 26 | }; 27 | 28 | export const handleLabelSchema: JSONSchema7 = { 29 | $id: v4(), 30 | type: 'object', 31 | properties: { 32 | number: { ...numberDefinition }, 33 | labelId: { type: 'string' }, 34 | action: { type: 'string', enum: ['add', 'remove'] }, 35 | }, 36 | required: ['number', 'labelId', 'action'], 37 | ...isNotEmpty('number', 'labelId', 'action'), 38 | }; 39 | -------------------------------------------------------------------------------- /src/validate/template.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 5 | const properties = {}; 6 | propertyNames.forEach( 7 | (property) => 8 | (properties[property] = { 9 | minLength: 1, 10 | description: `The "${property}" cannot be empty`, 11 | }), 12 | ); 13 | return { 14 | if: { 15 | propertyNames: { 16 | enum: [...propertyNames], 17 | }, 18 | }, 19 | then: { properties }, 20 | }; 21 | }; 22 | 23 | export const templateSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | name: { type: 'string' }, 28 | category: { type: 'string', enum: ['AUTHENTICATION', 'MARKETING', 'UTILITY'] }, 29 | allowCategoryChange: { type: 'boolean' }, 30 | language: { type: 'string' }, 31 | components: { type: 'array' }, 32 | webhookUrl: { type: 'string' }, 33 | }, 34 | required: ['name', 'category', 'language', 'components'], 35 | ...isNotEmpty('name', 'category', 'language', 'components'), 36 | }; 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "emitDecoratorMetadata": true, 5 | "declaration": true, 6 | "target": "es2020", 7 | "module": "CommonJS", 8 | "rootDir": "./", 9 | "resolveJsonModule": true, 10 | "removeComments": true, 11 | "outDir": "./dist", 12 | "noEmitOnError": true, 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": false, 16 | "skipLibCheck": true, 17 | "strictNullChecks": false, 18 | "incremental": true, 19 | "noImplicitAny": false, 20 | "baseUrl": ".", 21 | "paths": { 22 | "@api/*": ["./src/api/*"], 23 | "@cache/*": ["./src/cache/*"], 24 | "@config/*": ["./src/config/*"], 25 | "@exceptions": ["./src/exceptions"], 26 | "@libs/*": ["./src/libs/*"], 27 | "@utils/*": ["./src/utils/*"], 28 | "@validate/*": ["./src/validate/*"] 29 | }, 30 | "moduleResolution": "Node" 31 | }, 32 | "exclude": ["node_modules", "./test", "./dist", "./prisma"], 33 | "include": [ 34 | "src/**/*", 35 | "src/**/*.json" 36 | ] 37 | } -------------------------------------------------------------------------------- /src/utils/fetchLatestWaWebVersion.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios'; 2 | import { fetchLatestBaileysVersion, WAVersion } from 'baileys'; 3 | 4 | export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => { 5 | try { 6 | const { data } = await axios.get('https://web.whatsapp.com/sw.js', { 7 | ...options, 8 | responseType: 'json', 9 | }); 10 | 11 | const regex = /\\?"client_revision\\?":\s*(\d+)/; 12 | const match = data.match(regex); 13 | 14 | if (!match?.[1]) { 15 | return { 16 | version: (await fetchLatestBaileysVersion()).version as WAVersion, 17 | isLatest: false, 18 | error: { 19 | message: 'Could not find client revision in the fetched content', 20 | }, 21 | }; 22 | } 23 | 24 | const clientRevision = match[1]; 25 | 26 | return { 27 | version: [2, 3000, +clientRevision] as WAVersion, 28 | isLatest: true, 29 | }; 30 | } catch (error) { 31 | return { 32 | version: (await fetchLatestBaileysVersion()).version as WAVersion, 33 | isLatest: false, 34 | error, 35 | }; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/api/integrations/event/event.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | import { EventController } from './event.controller'; 5 | 6 | export * from '@api/integrations/event/pusher/pusher.schema'; 7 | export * from '@api/integrations/event/webhook/webhook.schema'; 8 | 9 | export const eventSchema: JSONSchema7 = { 10 | $id: v4(), 11 | type: 'object', 12 | properties: { 13 | websocket: { 14 | $ref: '#/$defs/event', 15 | }, 16 | rabbitmq: { 17 | $ref: '#/$defs/event', 18 | }, 19 | nats: { 20 | $ref: '#/$defs/event', 21 | }, 22 | sqs: { 23 | $ref: '#/$defs/event', 24 | }, 25 | kafka: { 26 | $ref: '#/$defs/event', 27 | }, 28 | }, 29 | $defs: { 30 | event: { 31 | type: 'object', 32 | properties: { 33 | enabled: { type: 'boolean', enum: [true, false] }, 34 | events: { 35 | type: 'array', 36 | minItems: 0, 37 | items: { 38 | type: 'string', 39 | enum: EventController.events, 40 | }, 41 | }, 42 | }, 43 | required: ['enabled'], 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/api/integrations/storage/s3/validate/s3.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 5 | const properties = {}; 6 | propertyNames.forEach( 7 | (property) => 8 | (properties[property] = { 9 | minLength: 1, 10 | description: `The "${property}" cannot be empty`, 11 | }), 12 | ); 13 | return { 14 | if: { 15 | propertyNames: { 16 | enum: [...propertyNames], 17 | }, 18 | }, 19 | then: { properties }, 20 | }; 21 | }; 22 | 23 | export const s3Schema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | id: { type: 'string' }, 28 | type: { type: 'string' }, 29 | messageId: { type: 'integer' }, 30 | }, 31 | ...isNotEmpty('id', 'type', 'messageId'), 32 | }; 33 | 34 | export const s3UrlSchema: JSONSchema7 = { 35 | $id: v4(), 36 | type: 'object', 37 | properties: { 38 | id: { type: 'string', pattern: '\\d+', minLength: 1 }, 39 | expiry: { type: 'string', pattern: '\\d+', minLength: 1 }, 40 | }, 41 | ...isNotEmpty('id'), 42 | required: ['id'], 43 | }; 44 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240729180347_modify_typebot_session_status_openai_typebot_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The values [open] on the enum `TypebotSessionStatus` will be removed. If these variants are still used in the database, this will fail. 5 | - Changed the type of `status` on the `TypebotSession` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. 6 | 7 | */ 8 | -- AlterEnum 9 | BEGIN; 10 | CREATE TYPE "TypebotSessionStatus_new" AS ENUM ('opened', 'closed', 'paused'); 11 | ALTER TABLE "TypebotSession" ALTER COLUMN "status" TYPE "TypebotSessionStatus_new" USING ("status"::text::"TypebotSessionStatus_new"); 12 | ALTER TABLE "OpenaiSession" ALTER COLUMN "status" TYPE "TypebotSessionStatus_new" USING ("status"::text::"TypebotSessionStatus_new"); 13 | ALTER TYPE "TypebotSessionStatus" RENAME TO "TypebotSessionStatus_old"; 14 | ALTER TYPE "TypebotSessionStatus_new" RENAME TO "TypebotSessionStatus"; 15 | DROP TYPE "TypebotSessionStatus_old"; 16 | COMMIT; 17 | 18 | -- AlterTable 19 | ALTER TABLE "TypebotSession" DROP COLUMN "status", 20 | ADD COLUMN "status" "TypebotSessionStatus" NOT NULL; 21 | -------------------------------------------------------------------------------- /src/validate/settings.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 5 | const properties = {}; 6 | propertyNames.forEach( 7 | (property) => 8 | (properties[property] = { 9 | minLength: 1, 10 | description: `The "${property}" cannot be empty`, 11 | }), 12 | ); 13 | return { 14 | if: { 15 | propertyNames: { 16 | enum: [...propertyNames], 17 | }, 18 | }, 19 | then: { properties }, 20 | }; 21 | }; 22 | 23 | export const settingsSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | rejectCall: { type: 'boolean' }, 28 | msgCall: { type: 'string' }, 29 | groupsIgnore: { type: 'boolean' }, 30 | alwaysOnline: { type: 'boolean' }, 31 | readMessages: { type: 'boolean' }, 32 | readStatus: { type: 'boolean' }, 33 | syncFullHistory: { type: 'boolean' }, 34 | wavoipToken: { type: 'string' }, 35 | }, 36 | required: ['rejectCall', 'groupsIgnore', 'alwaysOnline', 'readMessages', 'readStatus', 'syncFullHistory'], 37 | ...isNotEmpty('rejectCall', 'groupsIgnore', 'alwaysOnline', 'readMessages', 'readStatus', 'syncFullHistory'), 38 | }; 39 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/base-chatbot.dto.ts: -------------------------------------------------------------------------------- 1 | import { TriggerOperator, TriggerType } from '@prisma/client'; 2 | 3 | /** 4 | * Base DTO for all chatbot integrations 5 | * Contains common properties shared by all chatbot types 6 | */ 7 | export class BaseChatbotDto { 8 | enabled?: boolean; 9 | description: string; 10 | expire?: number; 11 | keywordFinish?: string; 12 | delayMessage?: number; 13 | unknownMessage?: string; 14 | listeningFromMe?: boolean; 15 | stopBotFromMe?: boolean; 16 | keepOpen?: boolean; 17 | debounceTime?: number; 18 | triggerType: TriggerType; 19 | triggerOperator?: TriggerOperator; 20 | triggerValue?: string; 21 | ignoreJids?: string[]; 22 | splitMessages?: boolean; 23 | timePerChar?: number; 24 | } 25 | 26 | /** 27 | * Base settings DTO for all chatbot integrations 28 | */ 29 | export class BaseChatbotSettingDto { 30 | expire?: number; 31 | keywordFinish?: string; 32 | delayMessage?: number; 33 | unknownMessage?: string; 34 | listeningFromMe?: boolean; 35 | stopBotFromMe?: boolean; 36 | keepOpen?: boolean; 37 | debounceTime?: number; 38 | ignoreJids?: any; 39 | splitMessages?: boolean; 40 | timePerChar?: number; 41 | fallbackId?: string; // Unified fallback ID field for all integrations 42 | } 43 | -------------------------------------------------------------------------------- /src/api/dto/group.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateGroupDto { 2 | subject: string; 3 | participants: string[]; 4 | description?: string; 5 | promoteParticipants?: boolean; 6 | } 7 | 8 | export class GroupPictureDto { 9 | groupJid: string; 10 | image: string; 11 | } 12 | 13 | export class GroupSubjectDto { 14 | groupJid: string; 15 | subject: string; 16 | } 17 | 18 | export class GroupDescriptionDto { 19 | groupJid: string; 20 | description: string; 21 | } 22 | 23 | export class GroupJid { 24 | groupJid: string; 25 | } 26 | 27 | export class GetParticipant { 28 | getParticipants: string; 29 | } 30 | 31 | export class GroupInvite { 32 | inviteCode: string; 33 | } 34 | 35 | export class AcceptGroupInvite { 36 | inviteCode: string; 37 | } 38 | 39 | export class GroupSendInvite { 40 | groupJid: string; 41 | description: string; 42 | numbers: string[]; 43 | } 44 | 45 | export class GroupUpdateParticipantDto extends GroupJid { 46 | action: 'add' | 'remove' | 'promote' | 'demote'; 47 | participants: string[]; 48 | } 49 | 50 | export class GroupUpdateSettingDto extends GroupJid { 51 | action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; 52 | } 53 | 54 | export class GroupToggleEphemeralDto extends GroupJid { 55 | expiration: 0 | 86400 | 604800 | 7776000; 56 | } 57 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/chatwoot/libs/postgres.client.ts: -------------------------------------------------------------------------------- 1 | import { Chatwoot, configService } from '@config/env.config'; 2 | import { Logger } from '@config/logger.config'; 3 | import postgresql from 'pg'; 4 | 5 | const { Pool } = postgresql; 6 | 7 | class Postgres { 8 | private logger = new Logger('Postgres'); 9 | private pool; 10 | private connected = false; 11 | 12 | getConnection(connectionString: string) { 13 | if (this.connected) { 14 | return this.pool; 15 | } else { 16 | this.pool = new Pool({ 17 | connectionString, 18 | ssl: { 19 | rejectUnauthorized: false, 20 | }, 21 | }); 22 | 23 | this.pool.on('error', () => { 24 | this.logger.error('postgres disconnected'); 25 | this.connected = false; 26 | }); 27 | 28 | try { 29 | this.connected = true; 30 | } catch (e) { 31 | this.connected = false; 32 | this.logger.error('postgres connect exception caught: ' + e); 33 | return null; 34 | } 35 | 36 | return this.pool; 37 | } 38 | } 39 | 40 | getChatwootConnection() { 41 | const uri = configService.get('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI; 42 | 43 | return this.getConnection(uri); 44 | } 45 | } 46 | 47 | export const postgresClient = new Postgres(); 48 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'feat', // New feature 9 | 'fix', // Bug fix 10 | 'docs', // Documentation changes 11 | 'style', // Code style changes (formatting, etc) 12 | 'refactor', // Code refactoring 13 | 'perf', // Performance improvements 14 | 'test', // Adding or updating tests 15 | 'chore', // Maintenance tasks 16 | 'ci', // CI/CD changes 17 | 'build', // Build system changes 18 | 'revert', // Reverting changes 19 | 'security', // Security fixes 20 | ], 21 | ], 22 | 'type-case': [2, 'always', 'lower-case'], 23 | 'type-empty': [2, 'never'], 24 | 'scope-case': [2, 'always', 'lower-case'], 25 | 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], 26 | 'subject-empty': [2, 'never'], 27 | 'subject-full-stop': [2, 'never', '.'], 28 | 'header-max-length': [2, 'always', 100], 29 | 'body-leading-blank': [1, 'always'], 30 | 'body-max-line-length': [0, 'always', 150], 31 | 'footer-leading-blank': [1, 'always'], 32 | 'footer-max-line-length': [0, 'always', 150], 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/api/integrations/event/event.router.ts: -------------------------------------------------------------------------------- 1 | import { KafkaRouter } from '@api/integrations/event/kafka/kafka.router'; 2 | import { NatsRouter } from '@api/integrations/event/nats/nats.router'; 3 | import { PusherRouter } from '@api/integrations/event/pusher/pusher.router'; 4 | import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router'; 5 | import { SqsRouter } from '@api/integrations/event/sqs/sqs.router'; 6 | import { WebhookRouter } from '@api/integrations/event/webhook/webhook.router'; 7 | import { WebsocketRouter } from '@api/integrations/event/websocket/websocket.router'; 8 | import { Router } from 'express'; 9 | 10 | export class EventRouter { 11 | public readonly router: Router; 12 | 13 | constructor(configService: any, ...guards: any[]) { 14 | this.router = Router(); 15 | 16 | this.router.use('/webhook', new WebhookRouter(configService, ...guards).router); 17 | this.router.use('/websocket', new WebsocketRouter(...guards).router); 18 | this.router.use('/rabbitmq', new RabbitmqRouter(...guards).router); 19 | this.router.use('/nats', new NatsRouter(...guards).router); 20 | this.router.use('/pusher', new PusherRouter(...guards).router); 21 | this.router.use('/sqs', new SqsRouter(...guards).router); 22 | this.router.use('/kafka', new KafkaRouter(...guards).router); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.husky/README.md: -------------------------------------------------------------------------------- 1 | # Git Hooks Configuration 2 | 3 | Este projeto usa [Husky](https://typicode.github.io/husky/) para automatizar verificações de qualidade de código. 4 | 5 | ## Hooks Configurados 6 | 7 | ### Pre-commit 8 | - **Arquivo**: `.husky/pre-commit` 9 | - **Executa**: `npx lint-staged` 10 | - **Função**: Executa lint e correções automáticas apenas nos arquivos modificados 11 | 12 | ### Pre-push 13 | - **Arquivo**: `.husky/pre-push` 14 | - **Executa**: `npm run build` + `npm run lint:check` 15 | - **Função**: Verifica se o projeto compila e não tem erros de lint antes do push 16 | 17 | ## Lint-staged Configuration 18 | 19 | Configurado no `package.json`: 20 | 21 | ```json 22 | "lint-staged": { 23 | "src/**/*.{ts,js}": [ 24 | "eslint --fix", 25 | "git add" 26 | ], 27 | "src/**/*.ts": [ 28 | "npm run build" 29 | ] 30 | } 31 | ``` 32 | 33 | ## Como funciona 34 | 35 | 1. **Ao fazer commit**: Executa lint apenas nos arquivos modificados 36 | 2. **Ao fazer push**: Executa build completo e verificação de lint 37 | 3. **Se houver erros**: O commit/push é bloqueado até correção 38 | 39 | ## Comandos úteis 40 | 41 | ```bash 42 | # Pular hooks (não recomendado) 43 | git commit --no-verify 44 | git push --no-verify 45 | 46 | # Executar lint manualmente 47 | npm run lint 48 | 49 | # Executar build manualmente 50 | npm run build 51 | ``` 52 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20241011100803_split_messages_and_time_per_char_integrations/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Dify" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 3 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 4 | 5 | -- AlterTable 6 | ALTER TABLE "DifySetting" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 7 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 8 | 9 | -- AlterTable 10 | ALTER TABLE "EvolutionBot" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 11 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 12 | 13 | -- AlterTable 14 | ALTER TABLE "EvolutionBotSetting" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 15 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 16 | 17 | -- AlterTable 18 | ALTER TABLE "Flowise" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 19 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 20 | 21 | -- AlterTable 22 | ALTER TABLE "FlowiseSetting" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 23 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 24 | 25 | -- AlterTable 26 | ALTER TABLE "OpenaiBot" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 27 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 28 | 29 | -- AlterTable 30 | ALTER TABLE "OpenaiSetting" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false, 31 | ADD COLUMN "timePerChar" INTEGER DEFAULT 50; 32 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/chatwoot/dto/chatwoot.dto.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '@api/integrations/integration.dto'; 2 | 3 | export class ChatwootDto { 4 | enabled?: boolean; 5 | accountId?: string; 6 | token?: string; 7 | url?: string; 8 | nameInbox?: string; 9 | signMsg?: boolean; 10 | signDelimiter?: string; 11 | number?: string; 12 | reopenConversation?: boolean; 13 | conversationPending?: boolean; 14 | mergeBrazilContacts?: boolean; 15 | importContacts?: boolean; 16 | importMessages?: boolean; 17 | daysLimitImportMessages?: number; 18 | autoCreate?: boolean; 19 | organization?: string; 20 | logo?: string; 21 | ignoreJids?: string[]; 22 | } 23 | 24 | export function ChatwootInstanceMixin(Base: TBase) { 25 | return class extends Base { 26 | chatwootAccountId?: string; 27 | chatwootToken?: string; 28 | chatwootUrl?: string; 29 | chatwootSignMsg?: boolean; 30 | chatwootReopenConversation?: boolean; 31 | chatwootConversationPending?: boolean; 32 | chatwootMergeBrazilContacts?: boolean; 33 | chatwootImportContacts?: boolean; 34 | chatwootImportMessages?: boolean; 35 | chatwootDaysLimitImportMessages?: number; 36 | chatwootNameInbox?: string; 37 | chatwootOrganization?: string; 38 | chatwootLogo?: string; 39 | chatwootAutoCreate?: boolean; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/api/integrations/channel/evolution/evolution.controller.ts: -------------------------------------------------------------------------------- 1 | import { PrismaRepository } from '@api/repository/repository.service'; 2 | import { WAMonitoringService } from '@api/services/monitor.service'; 3 | import { Logger } from '@config/logger.config'; 4 | 5 | import { ChannelController, ChannelControllerInterface } from '../channel.controller'; 6 | 7 | export class EvolutionController extends ChannelController implements ChannelControllerInterface { 8 | private readonly logger = new Logger('EvolutionController'); 9 | 10 | constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { 11 | super(prismaRepository, waMonitor); 12 | } 13 | 14 | integrationEnabled: boolean; 15 | 16 | public async receiveWebhook(data: any) { 17 | const numberId = data.numberId; 18 | 19 | if (!numberId) { 20 | this.logger.error('WebhookService -> receiveWebhookEvolution -> numberId not found'); 21 | return; 22 | } 23 | 24 | const instance = await this.prismaRepository.instance.findFirst({ 25 | where: { number: numberId }, 26 | }); 27 | 28 | if (!instance) { 29 | this.logger.error('WebhookService -> receiveWebhook -> instance not found'); 30 | return; 31 | } 32 | 33 | await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data); 34 | 35 | return { 36 | status: 'success', 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/api/integrations/event/webhook/webhook.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | import { EventController } from '../event.controller'; 5 | 6 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 7 | const properties = {}; 8 | propertyNames.forEach( 9 | (property) => 10 | (properties[property] = { 11 | minLength: 1, 12 | description: `The "${property}" cannot be empty`, 13 | }), 14 | ); 15 | return { 16 | if: { 17 | propertyNames: { 18 | enum: [...propertyNames], 19 | }, 20 | }, 21 | then: { properties }, 22 | }; 23 | }; 24 | 25 | export const webhookSchema: JSONSchema7 = { 26 | $id: v4(), 27 | type: 'object', 28 | properties: { 29 | webhook: { 30 | type: 'object', 31 | properties: { 32 | enabled: { type: 'boolean' }, 33 | url: { type: 'string' }, 34 | headers: { type: 'object' }, 35 | byEvents: { type: 'boolean' }, 36 | base64: { type: 'boolean' }, 37 | events: { 38 | type: 'array', 39 | minItems: 0, 40 | items: { 41 | type: 'string', 42 | enum: EventController.events, 43 | }, 44 | }, 45 | }, 46 | required: ['enabled', 'url'], 47 | ...isNotEmpty('enabled', 'url'), 48 | }, 49 | }, 50 | required: ['webhook'], 51 | }; 52 | -------------------------------------------------------------------------------- /.github/workflows/publish_docker_image_latest.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build_deploy: 10 | name: Build and Deploy 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v5 18 | with: 19 | submodules: recursive 20 | 21 | - name: Docker meta 22 | id: meta 23 | uses: docker/metadata-action@v5 24 | with: 25 | images: evoapicloud/evolution-api 26 | tags: latest 27 | 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v3 33 | 34 | - name: Login to Docker Hub 35 | uses: docker/login-action@v3 36 | with: 37 | username: ${{ secrets.DOCKER_USERNAME }} 38 | password: ${{ secrets.DOCKER_PASSWORD }} 39 | 40 | - name: Build and push 41 | id: docker_build 42 | uses: docker/build-push-action@v6 43 | with: 44 | platforms: linux/amd64,linux/arm64 45 | push: true 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | 49 | - name: Image digest 50 | run: echo ${{ steps.docker_build.outputs.digest }} 51 | -------------------------------------------------------------------------------- /.github/workflows/publish_docker_image_homolog.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | build_deploy: 10 | name: Build and Deploy 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v5 18 | with: 19 | submodules: recursive 20 | 21 | - name: Docker meta 22 | id: meta 23 | uses: docker/metadata-action@v5 24 | with: 25 | images: evoapicloud/evolution-api 26 | tags: homolog 27 | 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v3 33 | 34 | - name: Login to Docker Hub 35 | uses: docker/login-action@v3 36 | with: 37 | username: ${{ secrets.DOCKER_USERNAME }} 38 | password: ${{ secrets.DOCKER_PASSWORD }} 39 | 40 | - name: Build and push 41 | id: docker_build 42 | uses: docker/build-push-action@v6 43 | with: 44 | platforms: linux/amd64,linux/arm64 45 | push: true 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | 49 | - name: Image digest 50 | run: echo ${{ steps.docker_build.outputs.digest }} 51 | -------------------------------------------------------------------------------- /src/api/routes/label.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { HandleLabelDto, LabelDto } from '@api/dto/label.dto'; 3 | import { labelController } from '@api/server.module'; 4 | import { handleLabelSchema } from '@validate/validate.schema'; 5 | import { RequestHandler, Router } from 'express'; 6 | 7 | import { HttpStatus } from './index.router'; 8 | 9 | export class LabelRouter extends RouterBroker { 10 | constructor(...guards: RequestHandler[]) { 11 | super(); 12 | this.router 13 | .get(this.routerPath('findLabels'), ...guards, async (req, res) => { 14 | const response = await this.dataValidate({ 15 | request: req, 16 | schema: null, 17 | ClassRef: LabelDto, 18 | execute: (instance) => labelController.fetchLabels(instance), 19 | }); 20 | 21 | return res.status(HttpStatus.OK).json(response); 22 | }) 23 | .post(this.routerPath('handleLabel'), ...guards, async (req, res) => { 24 | const response = await this.dataValidate({ 25 | request: req, 26 | schema: handleLabelSchema, 27 | ClassRef: HandleLabelDto, 28 | execute: (instance, data) => labelController.handleLabel(instance, data), 29 | }); 30 | 31 | return res.status(HttpStatus.OK).json(response); 32 | }); 33 | } 34 | 35 | public readonly router: Router = Router(); 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/publish_docker_image.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | 8 | jobs: 9 | build_deploy: 10 | name: Build and Deploy 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v5 18 | with: 19 | submodules: recursive 20 | 21 | - name: Docker meta 22 | id: meta 23 | uses: docker/metadata-action@v5 24 | with: 25 | images: evoapicloud/evolution-api 26 | tags: type=semver,pattern=v{{version}} 27 | 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v3 33 | 34 | - name: Login to GitHub Container Registry 35 | uses: docker/login-action@v3 36 | with: 37 | username: ${{ secrets.DOCKER_USERNAME }} 38 | password: ${{ secrets.DOCKER_PASSWORD }} 39 | 40 | - name: Build and push 41 | id: docker_build 42 | uses: docker/build-push-action@v6 43 | with: 44 | platforms: linux/amd64,linux/arm64 45 | push: true 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | 49 | - name: Image digest 50 | run: echo ${{ steps.docker_build.outputs.digest }} -------------------------------------------------------------------------------- /src/api/integrations/chatbot/chatbot.router.ts: -------------------------------------------------------------------------------- 1 | import { ChatwootRouter } from '@api/integrations/chatbot/chatwoot/routes/chatwoot.router'; 2 | import { DifyRouter } from '@api/integrations/chatbot/dify/routes/dify.router'; 3 | import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.router'; 4 | import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router'; 5 | import { Router } from 'express'; 6 | 7 | import { EvoaiRouter } from './evoai/routes/evoai.router'; 8 | import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router'; 9 | import { FlowiseRouter } from './flowise/routes/flowise.router'; 10 | import { N8nRouter } from './n8n/routes/n8n.router'; 11 | 12 | export class ChatbotRouter { 13 | public readonly router: Router; 14 | 15 | constructor(...guards: any[]) { 16 | this.router = Router(); 17 | 18 | this.router.use('/evolutionBot', new EvolutionBotRouter(...guards).router); 19 | this.router.use('/chatwoot', new ChatwootRouter(...guards).router); 20 | this.router.use('/typebot', new TypebotRouter(...guards).router); 21 | this.router.use('/openai', new OpenaiRouter(...guards).router); 22 | this.router.use('/dify', new DifyRouter(...guards).router); 23 | this.router.use('/flowise', new FlowiseRouter(...guards).router); 24 | this.router.use('/n8n', new N8nRouter(...guards).router); 25 | this.router.use('/evoai', new EvoaiRouter(...guards).router); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/api/routes/proxy.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { ProxyDto } from '@api/dto/proxy.dto'; 4 | import { proxyController } from '@api/server.module'; 5 | import { instanceSchema, proxySchema } from '@validate/validate.schema'; 6 | import { RequestHandler, Router } from 'express'; 7 | 8 | import { HttpStatus } from './index.router'; 9 | 10 | export class ProxyRouter extends RouterBroker { 11 | constructor(...guards: RequestHandler[]) { 12 | super(); 13 | this.router 14 | .post(this.routerPath('set'), ...guards, async (req, res) => { 15 | const response = await this.dataValidate({ 16 | request: req, 17 | schema: proxySchema, 18 | ClassRef: ProxyDto, 19 | execute: (instance, data) => proxyController.createProxy(instance, data), 20 | }); 21 | 22 | res.status(HttpStatus.CREATED).json(response); 23 | }) 24 | .get(this.routerPath('find'), ...guards, async (req, res) => { 25 | const response = await this.dataValidate({ 26 | request: req, 27 | schema: instanceSchema, 28 | ClassRef: InstanceDto, 29 | execute: (instance) => proxyController.findProxy(instance), 30 | }); 31 | 32 | res.status(HttpStatus.OK).json(response); 33 | }); 34 | } 35 | 36 | public readonly router: Router = Router(); 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security Scan 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | schedule: 9 | - cron: '0 0 * * 1' # Weekly on Mondays 10 | 11 | jobs: 12 | codeql: 13 | name: CodeQL Analysis 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 15 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'javascript' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v5 29 | with: 30 | submodules: recursive 31 | 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v3 34 | with: 35 | languages: ${{ matrix.language }} 36 | 37 | - name: Autobuild 38 | uses: github/codeql-action/autobuild@v3 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v3 42 | with: 43 | category: "/language:${{matrix.language}}" 44 | 45 | dependency-review: 46 | name: Dependency Review 47 | runs-on: ubuntu-latest 48 | if: github.event_name == 'pull_request' 49 | steps: 50 | - name: Checkout Repository 51 | uses: actions/checkout@v5 52 | with: 53 | submodules: recursive 54 | - name: Dependency Review 55 | uses: actions/dependency-review-action@v4 56 | -------------------------------------------------------------------------------- /src/api/integrations/storage/s3/routes/s3.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { MediaDto } from '@api/integrations/storage/s3/dto/media.dto'; 3 | import { s3Schema, s3UrlSchema } from '@api/integrations/storage/s3/validate/s3.schema'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { s3Controller } from '@api/server.module'; 6 | import { RequestHandler, Router } from 'express'; 7 | 8 | export class S3Router extends RouterBroker { 9 | constructor(...guards: RequestHandler[]) { 10 | super(); 11 | this.router 12 | .post(this.routerPath('getMedia'), ...guards, async (req, res) => { 13 | const response = await this.dataValidate({ 14 | request: req, 15 | schema: s3Schema, 16 | ClassRef: MediaDto, 17 | execute: (instance, data) => s3Controller.getMedia(instance, data), 18 | }); 19 | 20 | res.status(HttpStatus.CREATED).json(response); 21 | }) 22 | .post(this.routerPath('getMediaUrl'), ...guards, async (req, res) => { 23 | const response = await this.dataValidate({ 24 | request: req, 25 | schema: s3UrlSchema, 26 | ClassRef: MediaDto, 27 | execute: (instance, data) => s3Controller.getMediaUrl(instance, data), 28 | }); 29 | 30 | res.status(HttpStatus.OK).json(response); 31 | }); 32 | } 33 | 34 | public readonly router: Router = Router(); 35 | } 36 | -------------------------------------------------------------------------------- /src/api/routes/settings.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { SettingsDto } from '@api/dto/settings.dto'; 4 | import { settingsController } from '@api/server.module'; 5 | import { settingsSchema } from '@validate/validate.schema'; 6 | import { RequestHandler, Router } from 'express'; 7 | 8 | import { HttpStatus } from './index.router'; 9 | 10 | export class SettingsRouter extends RouterBroker { 11 | constructor(...guards: RequestHandler[]) { 12 | super(); 13 | this.router 14 | .post(this.routerPath('set'), ...guards, async (req, res) => { 15 | const response = await this.dataValidate({ 16 | request: req, 17 | schema: settingsSchema, 18 | ClassRef: SettingsDto, 19 | execute: (instance, data) => settingsController.createSettings(instance, data), 20 | }); 21 | 22 | res.status(HttpStatus.CREATED).json(response); 23 | }) 24 | .get(this.routerPath('find'), ...guards, async (req, res) => { 25 | const response = await this.dataValidate({ 26 | request: req, 27 | schema: null, 28 | ClassRef: InstanceDto, 29 | execute: (instance) => settingsController.findSettings(instance), 30 | }); 31 | 32 | res.status(HttpStatus.OK).json(response); 33 | }); 34 | } 35 | 36 | public readonly router: Router = Router(); 37 | } 38 | -------------------------------------------------------------------------------- /src/api/integrations/event/pusher/pusher.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { EventDto } from '@api/integrations/event/event.dto'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { eventManager } from '@api/server.module'; 6 | import { instanceSchema, pusherSchema } from '@validate/validate.schema'; 7 | import { RequestHandler, Router } from 'express'; 8 | export class PusherRouter extends RouterBroker { 9 | constructor(...guards: RequestHandler[]) { 10 | super(); 11 | this.router 12 | .post(this.routerPath('set'), ...guards, async (req, res) => { 13 | const response = await this.dataValidate({ 14 | request: req, 15 | schema: pusherSchema, 16 | ClassRef: EventDto, 17 | execute: (instance, data) => eventManager.pusher.set(instance.instanceName, data), 18 | }); 19 | res.status(HttpStatus.CREATED).json(response); 20 | }) 21 | .get(this.routerPath('find'), ...guards, async (req, res) => { 22 | const response = await this.dataValidate({ 23 | request: req, 24 | schema: instanceSchema, 25 | ClassRef: InstanceDto, 26 | execute: (instance) => eventManager.pusher.get(instance.instanceName), 27 | }); 28 | res.status(HttpStatus.OK).json(response); 29 | }); 30 | } 31 | public readonly router: Router = Router(); 32 | } 33 | -------------------------------------------------------------------------------- /src/api/integrations/event/pusher/pusher.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | import { EventController } from '../event.controller'; 5 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 6 | const properties = {}; 7 | propertyNames.forEach( 8 | (property) => 9 | (properties[property] = { 10 | minLength: 1, 11 | description: `The "${property}" cannot be empty`, 12 | }), 13 | ); 14 | return { 15 | if: { 16 | propertyNames: { 17 | enum: [...propertyNames], 18 | }, 19 | }, 20 | then: { properties }, 21 | }; 22 | }; 23 | export const pusherSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | pusher: { 28 | type: 'object', 29 | properties: { 30 | enabled: { type: 'boolean' }, 31 | appId: { type: 'string' }, 32 | key: { type: 'string' }, 33 | secret: { type: 'string' }, 34 | cluster: { type: 'string' }, 35 | useTLS: { type: 'boolean' }, 36 | events: { 37 | type: 'array', 38 | minItems: 0, 39 | items: { 40 | type: 'string', 41 | enum: EventController.events, 42 | }, 43 | }, 44 | }, 45 | required: ['enabled', 'appId', 'key', 'secret', 'cluster', 'useTLS'], 46 | ...isNotEmpty('enabled', 'appId', 'key', 'secret', 'cluster', 'useTLS'), 47 | }, 48 | }, 49 | required: ['pusher'], 50 | }; 51 | -------------------------------------------------------------------------------- /src/api/integrations/event/sqs/sqs.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { EventDto } from '@api/integrations/event/event.dto'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { eventManager } from '@api/server.module'; 6 | import { eventSchema, instanceSchema } from '@validate/validate.schema'; 7 | import { RequestHandler, Router } from 'express'; 8 | 9 | export class SqsRouter extends RouterBroker { 10 | constructor(...guards: RequestHandler[]) { 11 | super(); 12 | this.router 13 | .post(this.routerPath('set'), ...guards, async (req, res) => { 14 | const response = await this.dataValidate({ 15 | request: req, 16 | schema: eventSchema, 17 | ClassRef: EventDto, 18 | execute: (instance, data) => eventManager.sqs.set(instance.instanceName, data), 19 | }); 20 | 21 | res.status(HttpStatus.CREATED).json(response); 22 | }) 23 | .get(this.routerPath('find'), ...guards, async (req, res) => { 24 | const response = await this.dataValidate({ 25 | request: req, 26 | schema: instanceSchema, 27 | ClassRef: InstanceDto, 28 | execute: (instance) => eventManager.sqs.get(instance.instanceName), 29 | }); 30 | 31 | res.status(HttpStatus.OK).json(response); 32 | }); 33 | } 34 | 35 | public readonly router: Router = Router(); 36 | } 37 | -------------------------------------------------------------------------------- /src/api/integrations/event/nats/nats.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { EventDto } from '@api/integrations/event/event.dto'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { eventManager } from '@api/server.module'; 6 | import { eventSchema, instanceSchema } from '@validate/validate.schema'; 7 | import { RequestHandler, Router } from 'express'; 8 | 9 | export class NatsRouter extends RouterBroker { 10 | constructor(...guards: RequestHandler[]) { 11 | super(); 12 | this.router 13 | .post(this.routerPath('set'), ...guards, async (req, res) => { 14 | const response = await this.dataValidate({ 15 | request: req, 16 | schema: eventSchema, 17 | ClassRef: EventDto, 18 | execute: (instance, data) => eventManager.nats.set(instance.instanceName, data), 19 | }); 20 | 21 | res.status(HttpStatus.CREATED).json(response); 22 | }) 23 | .get(this.routerPath('find'), ...guards, async (req, res) => { 24 | const response = await this.dataValidate({ 25 | request: req, 26 | schema: instanceSchema, 27 | ClassRef: InstanceDto, 28 | execute: (instance) => eventManager.nats.get(instance.instanceName), 29 | }); 30 | 31 | res.status(HttpStatus.OK).json(response); 32 | }); 33 | } 34 | 35 | public readonly router: Router = Router(); 36 | } 37 | -------------------------------------------------------------------------------- /src/api/integrations/event/kafka/kafka.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { EventDto } from '@api/integrations/event/event.dto'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { eventManager } from '@api/server.module'; 6 | import { eventSchema, instanceSchema } from '@validate/validate.schema'; 7 | import { RequestHandler, Router } from 'express'; 8 | 9 | export class KafkaRouter extends RouterBroker { 10 | constructor(...guards: RequestHandler[]) { 11 | super(); 12 | this.router 13 | .post(this.routerPath('set'), ...guards, async (req, res) => { 14 | const response = await this.dataValidate({ 15 | request: req, 16 | schema: eventSchema, 17 | ClassRef: EventDto, 18 | execute: (instance, data) => eventManager.kafka.set(instance.instanceName, data), 19 | }); 20 | 21 | res.status(HttpStatus.CREATED).json(response); 22 | }) 23 | .get(this.routerPath('find'), ...guards, async (req, res) => { 24 | const response = await this.dataValidate({ 25 | request: req, 26 | schema: instanceSchema, 27 | ClassRef: InstanceDto, 28 | execute: (instance) => eventManager.kafka.get(instance.instanceName), 29 | }); 30 | 31 | res.status(HttpStatus.OK).json(response); 32 | }); 33 | } 34 | 35 | public readonly router: Router = Router(); 36 | } 37 | -------------------------------------------------------------------------------- /src/api/integrations/event/rabbitmq/rabbitmq.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { EventDto } from '@api/integrations/event/event.dto'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { eventManager } from '@api/server.module'; 6 | import { eventSchema, instanceSchema } from '@validate/validate.schema'; 7 | import { RequestHandler, Router } from 'express'; 8 | 9 | export class RabbitmqRouter extends RouterBroker { 10 | constructor(...guards: RequestHandler[]) { 11 | super(); 12 | this.router 13 | .post(this.routerPath('set'), ...guards, async (req, res) => { 14 | const response = await this.dataValidate({ 15 | request: req, 16 | schema: eventSchema, 17 | ClassRef: EventDto, 18 | execute: (instance, data) => eventManager.rabbitmq.set(instance.instanceName, data), 19 | }); 20 | 21 | res.status(HttpStatus.CREATED).json(response); 22 | }) 23 | .get(this.routerPath('find'), ...guards, async (req, res) => { 24 | const response = await this.dataValidate({ 25 | request: req, 26 | schema: instanceSchema, 27 | ClassRef: InstanceDto, 28 | execute: (instance) => eventManager.rabbitmq.get(instance.instanceName), 29 | }); 30 | 31 | res.status(HttpStatus.OK).json(response); 32 | }); 33 | } 34 | 35 | public readonly router: Router = Router(); 36 | } 37 | -------------------------------------------------------------------------------- /src/api/integrations/event/websocket/websocket.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { EventDto } from '@api/integrations/event/event.dto'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { eventManager } from '@api/server.module'; 6 | import { eventSchema, instanceSchema } from '@validate/validate.schema'; 7 | import { RequestHandler, Router } from 'express'; 8 | 9 | export class WebsocketRouter extends RouterBroker { 10 | constructor(...guards: RequestHandler[]) { 11 | super(); 12 | this.router 13 | .post(this.routerPath('set'), ...guards, async (req, res) => { 14 | const response = await this.dataValidate({ 15 | request: req, 16 | schema: eventSchema, 17 | ClassRef: EventDto, 18 | execute: (instance, data) => eventManager.websocket.set(instance.instanceName, data), 19 | }); 20 | 21 | res.status(HttpStatus.CREATED).json(response); 22 | }) 23 | .get(this.routerPath('find'), ...guards, async (req, res) => { 24 | const response = await this.dataValidate({ 25 | request: req, 26 | schema: instanceSchema, 27 | ClassRef: InstanceDto, 28 | execute: (instance) => eventManager.websocket.get(instance.instanceName), 29 | }); 30 | 31 | res.status(HttpStatus.OK).json(response); 32 | }); 33 | } 34 | 35 | public readonly router: Router = Router(); 36 | } 37 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | warnOnUnsupportedTypeScriptVersion: false, 8 | EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, 9 | }, 10 | plugins: ['@typescript-eslint', 'simple-import-sort', 'import'], 11 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], 12 | globals: { 13 | Atomics: 'readonly', 14 | SharedArrayBuffer: 'readonly', 15 | }, 16 | root: true, 17 | env: { 18 | node: true, 19 | jest: true, 20 | }, 21 | ignorePatterns: ['.eslintrc.js'], 22 | rules: { 23 | '@typescript-eslint/interface-name-prefix': 'off', 24 | '@typescript-eslint/explicit-function-return-type': 'off', 25 | '@typescript-eslint/explicit-module-boundary-types': 'off', 26 | '@typescript-eslint/no-explicit-any': 'off', 27 | '@typescript-eslint/no-empty-function': 'off', 28 | '@typescript-eslint/no-non-null-assertion': 'off', 29 | '@typescript-eslint/no-unused-vars': 'error', 30 | 'import/first': 'error', 31 | 'import/no-duplicates': 'error', 32 | 'simple-import-sort/imports': 'error', 33 | 'simple-import-sort/exports': 'error', 34 | '@typescript-eslint/no-empty-object-type': 'off', 35 | '@typescript-eslint/no-wrapper-object-types': 'off', 36 | '@typescript-eslint/no-unused-expressions': 'off', 37 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/utils/advancedOperatorsSearch.ts: -------------------------------------------------------------------------------- 1 | function normalizeString(str: string): string { 2 | return str 3 | .normalize('NFD') 4 | .replace(/[\u0300-\u036f]/g, '') 5 | .toLowerCase(); 6 | } 7 | 8 | export function advancedOperatorsSearch(data: string, query: string): boolean { 9 | const filters = query.split(' ').reduce((acc: Record, filter) => { 10 | const [operator, ...values] = filter.split(':'); 11 | const value = values.join(':'); 12 | 13 | if (!acc[operator]) { 14 | acc[operator] = []; 15 | } 16 | acc[operator].push(value); 17 | return acc; 18 | }, {}); 19 | 20 | const normalizedItem = normalizeString(data); 21 | 22 | return Object.entries(filters).every(([operator, values]) => { 23 | return values.some((val) => { 24 | const subValues = val.split(','); 25 | return subValues.every((subVal) => { 26 | const normalizedSubVal = normalizeString(subVal); 27 | 28 | switch (operator.toLowerCase()) { 29 | case 'contains': 30 | return normalizedItem.includes(normalizedSubVal); 31 | case 'notcontains': 32 | return !normalizedItem.includes(normalizedSubVal); 33 | case 'startswith': 34 | return normalizedItem.startsWith(normalizedSubVal); 35 | case 'endswith': 36 | return normalizedItem.endsWith(normalizedSubVal); 37 | case 'exact': 38 | return normalizedItem === normalizedSubVal; 39 | default: 40 | return false; 41 | } 42 | }); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/errorResponse.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@api/routes/index.router'; 2 | 3 | export interface MetaErrorResponse { 4 | status: number; 5 | error: string; 6 | message: string; 7 | details: { 8 | whatsapp_error: string; 9 | whatsapp_code: string | number; 10 | error_user_title: string; 11 | error_user_msg: string; 12 | error_type: string; 13 | error_subcode: number | null; 14 | fbtrace_id: string | null; 15 | context: string; 16 | type: string; 17 | }; 18 | timestamp: string; 19 | } 20 | 21 | /** 22 | * Creates standardized error response for Meta/WhatsApp API errors 23 | */ 24 | export function createMetaErrorResponse(error: any, context: string): MetaErrorResponse { 25 | // Extract Meta/WhatsApp specific error fields 26 | const metaError = error.template || error; 27 | const errorUserTitle = metaError.error_user_title || metaError.message || 'Unknown error'; 28 | const errorUserMsg = metaError.error_user_msg || metaError.message || 'Unknown error'; 29 | 30 | return { 31 | status: HttpStatus.BAD_REQUEST, 32 | error: 'Bad Request', 33 | message: errorUserTitle, 34 | details: { 35 | whatsapp_error: errorUserMsg, 36 | whatsapp_code: metaError.code || 'UNKNOWN_ERROR', 37 | error_user_title: errorUserTitle, 38 | error_user_msg: errorUserMsg, 39 | error_type: metaError.type || 'UNKNOWN', 40 | error_subcode: metaError.error_subcode || null, 41 | fbtrace_id: metaError.fbtrace_id || null, 42 | context, 43 | type: 'whatsapp_api_error', 44 | }, 45 | timestamp: new Date().toISOString(), 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/cache/rediscache.client.ts: -------------------------------------------------------------------------------- 1 | import { CacheConf, CacheConfRedis, configService } from '@config/env.config'; 2 | import { Logger } from '@config/logger.config'; 3 | import { createClient, RedisClientType } from 'redis'; 4 | 5 | class Redis { 6 | private logger = new Logger('Redis'); 7 | private client: RedisClientType = null; 8 | private conf: CacheConfRedis; 9 | private connected = false; 10 | 11 | constructor() { 12 | this.conf = configService.get('CACHE')?.REDIS; 13 | } 14 | 15 | getConnection(): RedisClientType { 16 | if (this.connected) { 17 | return this.client; 18 | } else { 19 | this.client = createClient({ 20 | url: this.conf.URI, 21 | }); 22 | 23 | this.client.on('connect', () => { 24 | this.logger.verbose('redis connecting'); 25 | }); 26 | 27 | this.client.on('ready', () => { 28 | this.logger.verbose('redis ready'); 29 | this.connected = true; 30 | }); 31 | 32 | this.client.on('error', () => { 33 | this.logger.error('redis disconnected'); 34 | this.connected = false; 35 | }); 36 | 37 | this.client.on('end', () => { 38 | this.logger.verbose('redis connection ended'); 39 | this.connected = false; 40 | }); 41 | 42 | try { 43 | this.client.connect(); 44 | this.connected = true; 45 | } catch (e) { 46 | this.connected = false; 47 | this.logger.error('redis connect exception caught: ' + e); 48 | return null; 49 | } 50 | 51 | return this.client; 52 | } 53 | } 54 | } 55 | 56 | export const redisClient = new Redis(); 57 | -------------------------------------------------------------------------------- /src/api/integrations/event/webhook/webhook.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { EventDto } from '@api/integrations/event/event.dto'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { eventManager } from '@api/server.module'; 6 | import { ConfigService } from '@config/env.config'; 7 | import { instanceSchema, webhookSchema } from '@validate/validate.schema'; 8 | import { RequestHandler, Router } from 'express'; 9 | 10 | export class WebhookRouter extends RouterBroker { 11 | constructor( 12 | readonly configService: ConfigService, 13 | ...guards: RequestHandler[] 14 | ) { 15 | super(); 16 | this.router 17 | .post(this.routerPath('set'), ...guards, async (req, res) => { 18 | const response = await this.dataValidate({ 19 | request: req, 20 | schema: webhookSchema, 21 | ClassRef: EventDto, 22 | execute: (instance, data) => eventManager.webhook.set(instance.instanceName, data), 23 | }); 24 | 25 | res.status(HttpStatus.CREATED).json(response); 26 | }) 27 | .get(this.routerPath('find'), ...guards, async (req, res) => { 28 | const response = await this.dataValidate({ 29 | request: req, 30 | schema: instanceSchema, 31 | ClassRef: InstanceDto, 32 | execute: (instance) => eventManager.webhook.get(instance.instanceName), 33 | }); 34 | 35 | res.status(HttpStatus.OK).json(response); 36 | }); 37 | } 38 | 39 | public readonly router: Router = Router(); 40 | } 41 | -------------------------------------------------------------------------------- /src/api/integrations/storage/s3/services/s3.service.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { MediaDto } from '@api/integrations/storage/s3/dto/media.dto'; 3 | import { getObjectUrl } from '@api/integrations/storage/s3/libs/minio.server'; 4 | import { PrismaRepository } from '@api/repository/repository.service'; 5 | import { Logger } from '@config/logger.config'; 6 | import { BadRequestException } from '@exceptions'; 7 | 8 | export class S3Service { 9 | constructor(private readonly prismaRepository: PrismaRepository) {} 10 | 11 | private readonly logger = new Logger('S3Service'); 12 | 13 | public async getMedia(instance: InstanceDto, query?: MediaDto) { 14 | try { 15 | const where: any = { 16 | instanceId: instance.instanceId, 17 | ...query, 18 | }; 19 | 20 | const media = await this.prismaRepository.media.findMany({ 21 | where, 22 | select: { 23 | id: true, 24 | fileName: true, 25 | type: true, 26 | mimetype: true, 27 | createdAt: true, 28 | Message: true, 29 | }, 30 | }); 31 | 32 | if (!media || media.length === 0) { 33 | throw 'Media not found'; 34 | } 35 | 36 | return media; 37 | } catch (error) { 38 | throw new BadRequestException(error); 39 | } 40 | } 41 | 42 | public async getMediaUrl(instance: InstanceDto, data: MediaDto) { 43 | const media = (await this.getMedia(instance, { id: data.id }))[0]; 44 | const mediaUrl = await getObjectUrl(media.fileName, data.expiry); 45 | return { 46 | mediaUrl, 47 | ...media, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Docker/kafka/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | zookeeper: 5 | container_name: zookeeper 6 | image: confluentinc/cp-zookeeper:7.5.0 7 | environment: 8 | - ZOOKEEPER_CLIENT_PORT=2181 9 | - ZOOKEEPER_TICK_TIME=2000 10 | - ZOOKEEPER_SYNC_LIMIT=2 11 | volumes: 12 | - zookeeper_data:/var/lib/zookeeper/ 13 | ports: 14 | - 2181:2181 15 | 16 | kafka: 17 | container_name: kafka 18 | image: confluentinc/cp-kafka:7.5.0 19 | depends_on: 20 | - zookeeper 21 | environment: 22 | - KAFKA_BROKER_ID=1 23 | - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 24 | - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,OUTSIDE:PLAINTEXT 25 | - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092,OUTSIDE://host.docker.internal:9094 26 | - KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT 27 | - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 28 | - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 29 | - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 30 | - KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0 31 | - KAFKA_AUTO_CREATE_TOPICS_ENABLE=true 32 | - KAFKA_LOG_RETENTION_HOURS=168 33 | - KAFKA_LOG_SEGMENT_BYTES=1073741824 34 | - KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS=300000 35 | - KAFKA_COMPRESSION_TYPE=gzip 36 | ports: 37 | - 29092:29092 38 | - 9092:9092 39 | - 9094:9094 40 | volumes: 41 | - kafka_data:/var/lib/kafka/data 42 | 43 | volumes: 44 | zookeeper_data: 45 | kafka_data: 46 | 47 | 48 | networks: 49 | evolution-net: 50 | name: evolution-net 51 | driver: bridge -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📋 Description 2 | 3 | 4 | ## 🔗 Related Issue 5 | 6 | Closes #(issue_number) 7 | 8 | ## 🧪 Type of Change 9 | 10 | - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) 11 | - [ ] ✨ New feature (non-breaking change which adds functionality) 12 | - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | - [ ] 📚 Documentation update 14 | - [ ] 🔧 Refactoring (no functional changes) 15 | - [ ] ⚡ Performance improvement 16 | - [ ] 🧹 Code cleanup 17 | - [ ] 🔒 Security fix 18 | 19 | ## 🧪 Testing 20 | 21 | - [ ] Manual testing completed 22 | - [ ] Functionality verified in development environment 23 | - [ ] No breaking changes introduced 24 | - [ ] Tested with different connection types (if applicable) 25 | 26 | ## 📸 Screenshots (if applicable) 27 | 28 | 29 | ## ✅ Checklist 30 | 31 | - [ ] My code follows the project's style guidelines 32 | - [ ] I have performed a self-review of my code 33 | - [ ] I have commented my code, particularly in hard-to-understand areas 34 | - [ ] I have made corresponding changes to the documentation 35 | - [ ] My changes generate no new warnings 36 | - [ ] I have manually tested my changes thoroughly 37 | - [ ] I have verified the changes work with different scenarios 38 | - [ ] Any dependent changes have been merged and published 39 | 40 | ## 📝 Additional Notes 41 | 42 | -------------------------------------------------------------------------------- /src/utils/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "qrgeneratedsuccesfully": "QRCode successfully generated!", 3 | "scanqr": "Scan this QR code within the next 40 seconds.", 4 | "qrlimitreached": "QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.", 5 | "cw.inbox.connected": "🚀 Connection successfully established!", 6 | "cw.inbox.disconnect": "🚨 Disconnecting WhatsApp from inbox *{{inboxName}}*.", 7 | "cw.inbox.alreadyConnected": "🚨 {{inboxName}} instance is connected.", 8 | "cw.inbox.clearCache": "✅ {{inboxName}} instance cache cleared.", 9 | "cw.inbox.notFound": "⚠️ {{inboxName}} instance not found.", 10 | "cw.inbox.status": "⚠️ {{inboxName}} instance status: *{{state}}*.", 11 | "cw.import.startImport": "💬 Starting to import messages. Please wait...", 12 | "cw.import.importingMessages": "💬 Importing messages. More one moment...", 13 | "cw.import.messagesImported": "💬 {{totalMessagesImported}} messages imported. Refresh page to see the new messages.", 14 | "cw.import.messagesException": "💬 Something went wrong in importing messages.", 15 | "cw.locationMessage.location": "Location", 16 | "cw.locationMessage.latitude": "Latitude", 17 | "cw.locationMessage.longitude": "Longitude", 18 | "cw.locationMessage.locationName": "Name", 19 | "cw.locationMessage.locationAddress": "Address", 20 | "cw.locationMessage.locationUrl": "URL", 21 | "cw.contactMessage.contact": "Contact", 22 | "cw.contactMessage.name": "Name", 23 | "cw.contactMessage.number": "Number", 24 | "cw.message.notsent": "🚨 The message could not be sent. Please check your connection. {{error}}", 25 | "cw.message.numbernotinwhatsapp": "🚨 The message was not sent as the contact is not a valid Whatsapp number.", 26 | "cw.message.edited": "Edited Message" 27 | } -------------------------------------------------------------------------------- /src/api/dto/instance.dto.ts: -------------------------------------------------------------------------------- 1 | import { IntegrationDto } from '@api/integrations/integration.dto'; 2 | import { JsonValue } from '@prisma/client/runtime/library'; 3 | import { WAPresence } from 'baileys'; 4 | 5 | export class InstanceDto extends IntegrationDto { 6 | instanceName: string; 7 | instanceId?: string; 8 | qrcode?: boolean; 9 | businessId?: string; 10 | number?: string; 11 | integration?: string; 12 | token?: string; 13 | status?: string; 14 | ownerJid?: string; 15 | connectionStatus?: string; 16 | profileName?: string; 17 | profilePicUrl?: string; 18 | // settings 19 | rejectCall?: boolean; 20 | msgCall?: string; 21 | groupsIgnore?: boolean; 22 | alwaysOnline?: boolean; 23 | readMessages?: boolean; 24 | readStatus?: boolean; 25 | syncFullHistory?: boolean; 26 | wavoipToken?: string; 27 | // proxy 28 | proxyHost?: string; 29 | proxyPort?: string; 30 | proxyProtocol?: string; 31 | proxyUsername?: string; 32 | proxyPassword?: string; 33 | webhook?: { 34 | enabled?: boolean; 35 | events?: string[]; 36 | headers?: JsonValue; 37 | url?: string; 38 | byEvents?: boolean; 39 | base64?: boolean; 40 | }; 41 | chatwootAccountId?: string; 42 | chatwootConversationPending?: boolean; 43 | chatwootAutoCreate?: boolean; 44 | chatwootDaysLimitImportMessages?: number; 45 | chatwootImportContacts?: boolean; 46 | chatwootImportMessages?: boolean; 47 | chatwootLogo?: string; 48 | chatwootMergeBrazilContacts?: boolean; 49 | chatwootNameInbox?: string; 50 | chatwootOrganization?: string; 51 | chatwootReopenConversation?: boolean; 52 | chatwootSignMsg?: boolean; 53 | chatwootToken?: string; 54 | chatwootUrl?: string; 55 | } 56 | 57 | export class SetPresenceDto { 58 | presence: WAPresence; 59 | } 60 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/chatwoot/validate/chatwoot.schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { v4 } from 'uuid'; 3 | 4 | const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { 5 | const properties = {}; 6 | propertyNames.forEach( 7 | (property) => 8 | (properties[property] = { 9 | minLength: 1, 10 | description: `The "${property}" cannot be empty`, 11 | }), 12 | ); 13 | return { 14 | if: { 15 | propertyNames: { 16 | enum: [...propertyNames], 17 | }, 18 | }, 19 | then: { properties }, 20 | }; 21 | }; 22 | 23 | export const chatwootSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | enabled: { type: 'boolean', enum: [true, false] }, 28 | accountId: { type: 'string' }, 29 | token: { type: 'string' }, 30 | url: { type: 'string' }, 31 | signMsg: { type: 'boolean', enum: [true, false] }, 32 | signDelimiter: { type: ['string', 'null'] }, 33 | nameInbox: { type: ['string', 'null'] }, 34 | reopenConversation: { type: 'boolean', enum: [true, false] }, 35 | conversationPending: { type: 'boolean', enum: [true, false] }, 36 | autoCreate: { type: 'boolean', enum: [true, false] }, 37 | importContacts: { type: 'boolean', enum: [true, false] }, 38 | mergeBrazilContacts: { type: 'boolean', enum: [true, false] }, 39 | importMessages: { type: 'boolean', enum: [true, false] }, 40 | daysLimitImportMessages: { type: 'number' }, 41 | ignoreJids: { type: 'array', items: { type: 'string' } }, 42 | }, 43 | required: ['enabled', 'accountId', 'token', 'url', 'signMsg', 'reopenConversation', 'conversationPending'], 44 | ...isNotEmpty('enabled', 'accountId', 'token', 'url', 'signMsg', 'reopenConversation', 'conversationPending'), 45 | }; 46 | -------------------------------------------------------------------------------- /src/utils/translations/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "qrgeneratedsuccesfully": "QRCode gerado com sucesso!", 3 | "scanqr": "Escaneie o QRCode com o WhatsApp nos próximos 40 segundos.", 4 | "qrlimitreached": "Limite de geração de QRCode atingido! Para gerar um novo QRCode, envie o texto 'init' nesta conversa.", 5 | "cw.inbox.connected": "🚀 Conectado com sucesso!", 6 | "cw.inbox.disconnect": "🚨 Instância *{{inboxName}}* desconectada do WhatsApp.", 7 | "cw.inbox.alreadyConnected": "🚨 Instância *{{inboxName}}* já está conectada.", 8 | "cw.inbox.clearCache": "✅ Instância *{{inboxName}}* cache removido.", 9 | "cw.inbox.notFound": "⚠️ Instância *{{inboxName}}* não encontrada.", 10 | "cw.inbox.status": "⚠️ Status da instância {{inboxName}}: *{{state}}*.", 11 | "cw.import.startImport": "💬 Iniciando importação de mensagens. Por favor, aguarde...", 12 | "cw.import.importingMessages": "💬 Importando mensagens. Mais um momento...", 13 | "cw.import.messagesImported": "💬 {{totalMessagesImported}} mensagens importadas. Atualize a página para ver as novas mensagens.", 14 | "cw.import.messagesException": "💬 Não foi possível importar as mensagens.", 15 | "cw.locationMessage.location": "Localização", 16 | "cw.locationMessage.latitude": "Latitude", 17 | "cw.locationMessage.longitude": "Longitude", 18 | "cw.locationMessage.locationName": "Nome", 19 | "cw.locationMessage.locationAddress": "Endereço", 20 | "cw.locationMessage.locationUrl": "URL", 21 | "cw.contactMessage.contact": "Contato", 22 | "cw.contactMessage.name": "Nome", 23 | "cw.contactMessage.number": "Número", 24 | "cw.message.notsent": "🚨 Não foi possível enviar a mensagem. Verifique sua conexão. {{error}}", 25 | "cw.message.numbernotinwhatsapp": "🚨 A mensagem não foi enviada, pois o contato não é um número válido do WhatsApp.", 26 | "cw.message.edited": "Mensagem editada" 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Evolution API License 2 | 3 | Evolution API is licensed under the Apache License 2.0, with the following additional conditions: 4 | 5 | 1. Evolution API may be utilized commercially, including as a backend service for other applications or as an application development platform for enterprises. Should the conditions below be met, a commercial license must be obtained from the producer: 6 | 7 | a. LOGO and copyright information: In the process of using Evolution API's frontend components, you may not remove or modify the LOGO or copyright information in the Evolution API console or applications. This restriction is inapplicable to uses of Evolution API that do not involve its frontend components. 8 | 9 | b. Usage Notification Requirement: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer. 10 | 11 | Please contact contato@evolution-api.com to inquire about licensing matters. 12 | 13 | 2. As a contributor, you should agree that: 14 | 15 | a. The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary. 16 | b. Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations. 17 | 18 | Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0. 19 | 20 | © 2025 Evolution API 21 | 22 | -------------------------------------------------------------------------------- /src/utils/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "qrgeneratedsuccesfully": "Código QR generado exitosamente!", 3 | "scanqr": "Escanea este código QR en los próximos 40 segundos.", 4 | "qrlimitreached": "🚨 Se alcanzó el límite de generación de QRCode. Para generar un nuevo QRCode, envíe el mensaje 'init' nuevamente.", 5 | "cw.inbox.connected": "🚀 ¡Conexión establecida exitosamente!", 6 | "cw.inbox.disconnect": "🚨 Instancia *{{inboxName}}* desconectado de Whatsapp.", 7 | "cw.inbox.alreadyConnected": "🚨 La instancia {{inboxName}} está conectada.", 8 | "cw.inbox.clearCache": "✅ Caché de la instancia {{inboxName}} borrada.", 9 | "cw.inbox.notFound": "⚠️ Instancia {{inboxName}} no encontrada.", 10 | "cw.inbox.status": "⚠️ Estado de la instancia {{inboxName}}: *{{state}}*.", 11 | "cw.import.startImport": "💬 Empezando a importar mensajes. Espere por favor...", 12 | "cw.import.importingMessages": "💬 Importando mensajes. mas un momento...", 13 | "cw.import.messagesImported": "💬 {{totalMessagesImported}} mensajes importados. Actualiza la página para ver los nuevos mensajes..", 14 | "cw.import.messagesException": "⚠️ Algo salió mal al importar mensajes..", 15 | "cw.locationMessage.location": "Ubicación", 16 | "cw.locationMessage.latitude": "Latitude", 17 | "cw.locationMessage.longitude": "Longitude", 18 | "cw.locationMessage.locationName": "Nombre", 19 | "cw.locationMessage.locationAddress": "Direccion", 20 | "cw.locationMessage.locationUrl": "URL", 21 | "cw.contactMessage.contact": "Contacto", 22 | "cw.contactMessage.name": "Nombre", 23 | "cw.contactMessage.number": "Numero", 24 | "cw.message.notsent": "🚨 El mensaje no se pudo enviar. Comprueba tu conexión. {{error}}", 25 | "cw.message.numbernotinwhatsapp": "🚨 El mensaje no fue enviado porque el contacto no es un número de Whatsapp válido.", 26 | "cw.message.edited": "Mensaje editado" 27 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-alpine AS builder 2 | 3 | RUN apk update && \ 4 | apk add --no-cache git ffmpeg wget curl bash openssl 5 | 6 | LABEL version="2.3.1" description="Api to control whatsapp features through http requests." 7 | LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" 8 | LABEL contact="contato@evolution-api.com" 9 | 10 | WORKDIR /evolution 11 | 12 | COPY ./package*.json ./ 13 | COPY ./tsconfig.json ./ 14 | COPY ./tsup.config.ts ./ 15 | 16 | RUN npm ci --silent 17 | 18 | COPY ./src ./src 19 | COPY ./public ./public 20 | COPY ./prisma ./prisma 21 | COPY ./manager ./manager 22 | COPY ./.env.example ./.env 23 | COPY ./runWithProvider.js ./ 24 | 25 | COPY ./Docker ./Docker 26 | 27 | RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/* 28 | 29 | RUN ./Docker/scripts/generate_database.sh 30 | 31 | RUN npm run build 32 | 33 | FROM node:24-alpine AS final 34 | 35 | RUN apk update && \ 36 | apk add tzdata ffmpeg bash openssl 37 | 38 | ENV TZ=America/Sao_Paulo 39 | ENV DOCKER_ENV=true 40 | 41 | WORKDIR /evolution 42 | 43 | COPY --from=builder /evolution/package.json ./package.json 44 | COPY --from=builder /evolution/package-lock.json ./package-lock.json 45 | 46 | COPY --from=builder /evolution/node_modules ./node_modules 47 | COPY --from=builder /evolution/dist ./dist 48 | COPY --from=builder /evolution/prisma ./prisma 49 | COPY --from=builder /evolution/manager ./manager 50 | COPY --from=builder /evolution/public ./public 51 | COPY --from=builder /evolution/.env ./.env 52 | COPY --from=builder /evolution/Docker ./Docker 53 | COPY --from=builder /evolution/runWithProvider.js ./runWithProvider.js 54 | COPY --from=builder /evolution/tsup.config.ts ./tsup.config.ts 55 | 56 | ENV DOCKER_ENV=true 57 | 58 | EXPOSE 8080 59 | 60 | ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ] 61 | -------------------------------------------------------------------------------- /runWithProvider.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const { execSync } = require('child_process'); 3 | const { existsSync } = require('fs'); 4 | 5 | dotenv.config(); 6 | 7 | const { DATABASE_PROVIDER } = process.env; 8 | const databaseProviderDefault = DATABASE_PROVIDER ?? 'postgresql'; 9 | 10 | if (!DATABASE_PROVIDER) { 11 | console.warn(`DATABASE_PROVIDER is not set in the .env file, using default: ${databaseProviderDefault}`); 12 | } 13 | 14 | // Função para determinar qual pasta de migrations usar 15 | // Função para determinar qual pasta de migrations usar 16 | function getMigrationsFolder(provider) { 17 | switch (provider) { 18 | case 'psql_bouncer': 19 | return 'postgresql-migrations'; // psql_bouncer usa as migrations do postgresql 20 | default: 21 | return `${provider}-migrations`; 22 | } 23 | } 24 | 25 | const migrationsFolder = getMigrationsFolder(databaseProviderDefault); 26 | 27 | let command = process.argv 28 | .slice(2) 29 | .join(' ') 30 | .replace(/DATABASE_PROVIDER/g, databaseProviderDefault); 31 | 32 | // Substituir referências à pasta de migrations pela pasta correta 33 | const migrationsPattern = new RegExp(`${databaseProviderDefault}-migrations`, 'g'); 34 | command = command.replace(migrationsPattern, migrationsFolder); 35 | 36 | if (command.includes('rmdir') && existsSync('prisma\\migrations')) { 37 | try { 38 | execSync('rmdir /S /Q prisma\\migrations', { stdio: 'inherit' }); 39 | } catch (error) { 40 | console.error(`Error removing directory: prisma\\migrations`); 41 | process.exit(1); 42 | } 43 | } else if (command.includes('rmdir')) { 44 | console.warn(`Directory 'prisma\\migrations' does not exist, skipping removal.`); 45 | } 46 | 47 | try { 48 | execSync(command, { stdio: 'inherit' }); 49 | } catch (error) { 50 | console.error(`Error executing command: ${command}`); 51 | process.exit(1); 52 | } -------------------------------------------------------------------------------- /src/api/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { prismaRepository } from '@api/server.module'; 3 | import { Auth, configService, Database } from '@config/env.config'; 4 | import { Logger } from '@config/logger.config'; 5 | import { ForbiddenException, UnauthorizedException } from '@exceptions'; 6 | import { NextFunction, Request, Response } from 'express'; 7 | 8 | const logger = new Logger('GUARD'); 9 | 10 | async function apikey(req: Request, _: Response, next: NextFunction) { 11 | const env = configService.get('AUTHENTICATION').API_KEY; 12 | const key = req.get('apikey'); 13 | const db = configService.get('DATABASE'); 14 | 15 | if (!key) { 16 | throw new UnauthorizedException(); 17 | } 18 | 19 | if (env.KEY === key) { 20 | return next(); 21 | } 22 | 23 | if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) { 24 | throw new ForbiddenException('Missing global api key', 'The global api key must be set'); 25 | } 26 | const param = req.params as unknown as InstanceDto; 27 | 28 | try { 29 | if (param?.instanceName) { 30 | const instance = await prismaRepository.instance.findUnique({ 31 | where: { name: param.instanceName }, 32 | }); 33 | if (instance.token === key) { 34 | return next(); 35 | } 36 | } else { 37 | if (req.originalUrl.includes('/instance/fetchInstances') && db.SAVE_DATA.INSTANCE) { 38 | const instanceByKey = await prismaRepository.instance.findFirst({ 39 | where: { token: key }, 40 | }); 41 | if (instanceByKey) { 42 | return next(); 43 | } 44 | } 45 | } 46 | } catch (error) { 47 | logger.error(error); 48 | } 49 | 50 | throw new UnauthorizedException(); 51 | } 52 | 53 | export const authGuard = { apikey }; 54 | -------------------------------------------------------------------------------- /src/utils/createJid.ts: -------------------------------------------------------------------------------- 1 | // Check if the number is MX or AR 2 | function formatMXOrARNumber(jid: string): string { 3 | const countryCode = jid.substring(0, 2); 4 | 5 | if (Number(countryCode) === 52 || Number(countryCode) === 54) { 6 | if (jid.length === 13) { 7 | const number = countryCode + jid.substring(3); 8 | return number; 9 | } 10 | 11 | return jid; 12 | } 13 | return jid; 14 | } 15 | 16 | // Check if the number is br 17 | function formatBRNumber(jid: string) { 18 | const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); 19 | if (regexp.test(jid)) { 20 | const match = regexp.exec(jid); 21 | if (match && match[1] === '55') { 22 | const joker = Number.parseInt(match[3][0]); 23 | const ddd = Number.parseInt(match[2]); 24 | if (joker < 7 || ddd < 31) { 25 | return match[0]; 26 | } 27 | return match[1] + match[2] + match[3]; 28 | } 29 | return jid; 30 | } else { 31 | return jid; 32 | } 33 | } 34 | 35 | export function createJid(number: string): string { 36 | number = number.replace(/:\d+/, ''); 37 | 38 | if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) { 39 | return number; 40 | } 41 | 42 | if (number.includes('@broadcast')) { 43 | return number; 44 | } 45 | 46 | number = number 47 | ?.replace(/\s/g, '') 48 | .replace(/\+/g, '') 49 | .replace(/\(/g, '') 50 | .replace(/\)/g, '') 51 | .split(':')[0] 52 | .split('@')[0]; 53 | 54 | if (number.includes('-') && number.length >= 24) { 55 | number = number.replace(/[^\d-]/g, ''); 56 | return `${number}@g.us`; 57 | } 58 | 59 | number = number.replace(/\D/g, ''); 60 | 61 | if (number.length >= 18) { 62 | number = number.replace(/[^\d-]/g, ''); 63 | return `${number}@g.us`; 64 | } 65 | 66 | number = formatMXOrARNumber(number); 67 | 68 | number = formatBRNumber(number); 69 | 70 | return `${number}@s.whatsapp.net`; 71 | } 72 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/chatwoot/routes/chatwoot.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto'; 4 | import { HttpStatus } from '@api/routes/index.router'; 5 | import { chatwootController } from '@api/server.module'; 6 | import { chatwootSchema, instanceSchema } from '@validate/validate.schema'; 7 | import { RequestHandler, Router } from 'express'; 8 | 9 | export class ChatwootRouter extends RouterBroker { 10 | constructor(...guards: RequestHandler[]) { 11 | super(); 12 | this.router 13 | .post(this.routerPath('set'), ...guards, async (req, res) => { 14 | const response = await this.dataValidate({ 15 | request: req, 16 | schema: chatwootSchema, 17 | ClassRef: ChatwootDto, 18 | execute: (instance, data) => chatwootController.createChatwoot(instance, data), 19 | }); 20 | 21 | res.status(HttpStatus.CREATED).json(response); 22 | }) 23 | .get(this.routerPath('find'), ...guards, async (req, res) => { 24 | const response = await this.dataValidate({ 25 | request: req, 26 | schema: instanceSchema, 27 | ClassRef: InstanceDto, 28 | execute: (instance) => chatwootController.findChatwoot(instance), 29 | }); 30 | 31 | res.status(HttpStatus.OK).json(response); 32 | }) 33 | .post(this.routerPath('webhook'), async (req, res) => { 34 | const response = await this.dataValidate({ 35 | request: req, 36 | schema: instanceSchema, 37 | ClassRef: InstanceDto, 38 | execute: (instance, data) => chatwootController.receiveWebhook(instance, data), 39 | }); 40 | 41 | res.status(HttpStatus.OK).json(response); 42 | }); 43 | } 44 | 45 | public readonly router: Router = Router(); 46 | } 47 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | api: 5 | container_name: evolution_api 6 | image: evoapicloud/evolution-api:latest 7 | restart: always 8 | depends_on: 9 | - redis 10 | - evolution-postgres 11 | ports: 12 | - "127.0.0.1:8080:8080" 13 | volumes: 14 | - evolution_instances:/evolution/instances 15 | networks: 16 | - evolution-net 17 | - dokploy-network 18 | env_file: 19 | - .env 20 | expose: 21 | - "8080" 22 | 23 | frontend: 24 | container_name: evolution_frontend 25 | image: evoapicloud/evolution-manager:latest 26 | restart: always 27 | ports: 28 | - "3000:80" 29 | networks: 30 | - evolution-net 31 | 32 | redis: 33 | container_name: evolution_redis 34 | image: redis:latest 35 | restart: always 36 | command: > 37 | redis-server --port 6379 --appendonly yes 38 | volumes: 39 | - evolution_redis:/data 40 | networks: 41 | evolution-net: 42 | aliases: 43 | - evolution-redis 44 | dokploy-network: 45 | aliases: 46 | - evolution-redis 47 | expose: 48 | - "6379" 49 | 50 | evolution-postgres: 51 | container_name: evolution_postgres 52 | image: postgres:15 53 | restart: always 54 | env_file: 55 | - .env 56 | command: 57 | - postgres 58 | - -c 59 | - max_connections=1000 60 | - -c 61 | - listen_addresses=* 62 | environment: 63 | - POSTGRES_DB=${POSTGRES_DATABASE} 64 | - POSTGRES_USER=${POSTGRES_USERNAME} 65 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 66 | volumes: 67 | - postgres_data:/var/lib/postgresql/data 68 | networks: 69 | - evolution-net 70 | - dokploy-network 71 | expose: 72 | - "5432" 73 | 74 | volumes: 75 | evolution_instances: 76 | evolution_redis: 77 | postgres_data: 78 | 79 | networks: 80 | evolution-net: 81 | name: evolution-net 82 | driver: bridge 83 | dokploy-network: 84 | external: true -------------------------------------------------------------------------------- /src/api/integrations/event/event.dto.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '@api/integrations/integration.dto'; 2 | import { JsonValue } from '@prisma/client/runtime/library'; 3 | 4 | export class EventDto { 5 | webhook?: { 6 | enabled?: boolean; 7 | events?: string[]; 8 | url?: string; 9 | headers?: JsonValue; 10 | byEvents?: boolean; 11 | base64?: boolean; 12 | }; 13 | 14 | websocket?: { 15 | enabled?: boolean; 16 | events?: string[]; 17 | }; 18 | 19 | sqs?: { 20 | enabled?: boolean; 21 | events?: string[]; 22 | }; 23 | 24 | rabbitmq?: { 25 | enabled?: boolean; 26 | events?: string[]; 27 | }; 28 | 29 | nats?: { 30 | enabled?: boolean; 31 | events?: string[]; 32 | }; 33 | 34 | pusher?: { 35 | enabled?: boolean; 36 | appId?: string; 37 | key?: string; 38 | secret?: string; 39 | cluster?: string; 40 | useTLS?: boolean; 41 | events?: string[]; 42 | }; 43 | 44 | kafka?: { 45 | enabled?: boolean; 46 | events?: string[]; 47 | }; 48 | } 49 | 50 | export function EventInstanceMixin(Base: TBase) { 51 | return class extends Base { 52 | webhook?: { 53 | enabled?: boolean; 54 | events?: string[]; 55 | headers?: JsonValue; 56 | url?: string; 57 | byEvents?: boolean; 58 | base64?: boolean; 59 | }; 60 | 61 | websocket?: { 62 | enabled?: boolean; 63 | events?: string[]; 64 | }; 65 | 66 | sqs?: { 67 | enabled?: boolean; 68 | events?: string[]; 69 | }; 70 | 71 | rabbitmq?: { 72 | enabled?: boolean; 73 | events?: string[]; 74 | }; 75 | 76 | nats?: { 77 | enabled?: boolean; 78 | events?: string[]; 79 | }; 80 | 81 | pusher?: { 82 | enabled?: boolean; 83 | appId?: string; 84 | key?: string; 85 | secret?: string; 86 | cluster?: string; 87 | useTLS?: boolean; 88 | events?: string[]; 89 | }; 90 | 91 | kafka?: { 92 | enabled?: boolean; 93 | events?: string[]; 94 | }; 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/api/guards/instance.guard.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { cache, prismaRepository, waMonitor } from '@api/server.module'; 3 | import { CacheConf, configService } from '@config/env.config'; 4 | import { BadRequestException, ForbiddenException, InternalServerErrorException, NotFoundException } from '@exceptions'; 5 | import { NextFunction, Request, Response } from 'express'; 6 | 7 | async function getInstance(instanceName: string) { 8 | try { 9 | const cacheConf = configService.get('CACHE'); 10 | 11 | const exists = !!waMonitor.waInstances[instanceName]; 12 | 13 | if (cacheConf.REDIS.ENABLED && cacheConf.REDIS.SAVE_INSTANCES) { 14 | const keyExists = await cache.has(instanceName); 15 | 16 | return exists || keyExists; 17 | } 18 | 19 | return exists || (await prismaRepository.instance.findMany({ where: { name: instanceName } })).length > 0; 20 | } catch (error) { 21 | throw new InternalServerErrorException(error?.toString()); 22 | } 23 | } 24 | 25 | export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) { 26 | if (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) { 27 | return next(); 28 | } 29 | 30 | const param = req.params as unknown as InstanceDto; 31 | if (!param?.instanceName) { 32 | throw new BadRequestException('"instanceName" not provided.'); 33 | } 34 | 35 | if (!(await getInstance(param.instanceName))) { 36 | throw new NotFoundException(`The "${param.instanceName}" instance does not exist`); 37 | } 38 | 39 | next(); 40 | } 41 | 42 | export async function instanceLoggedGuard(req: Request, _: Response, next: NextFunction) { 43 | if (req.originalUrl.includes('/instance/create')) { 44 | const instance = req.body as InstanceDto; 45 | if (await getInstance(instance.instanceName)) { 46 | throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`); 47 | } 48 | 49 | if (waMonitor.waInstances[instance.instanceName]) { 50 | delete waMonitor.waInstances[instance.instanceName]; 51 | } 52 | } 53 | 54 | next(); 55 | } 56 | -------------------------------------------------------------------------------- /src/api/abstract/abstract.repository.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService, Database } from '@config/env.config'; 2 | import { ROOT_DIR } from '@config/path.config'; 3 | import { existsSync, mkdirSync, writeFileSync } from 'fs'; 4 | import { join } from 'path'; 5 | 6 | export type IInsert = { insertCount: number }; 7 | 8 | export interface IRepository { 9 | insert(data: any, instanceName: string, saveDb?: boolean): Promise; 10 | update(data: any, instanceName: string, saveDb?: boolean): Promise; 11 | find(query: any): Promise; 12 | delete(query: any, force?: boolean): Promise; 13 | 14 | dbSettings: Database; 15 | readonly storePath: string; 16 | } 17 | 18 | type WriteStore = { 19 | path: string; 20 | fileName: string; 21 | data: U; 22 | }; 23 | 24 | export abstract class Repository implements IRepository { 25 | constructor(configService: ConfigService) { 26 | this.dbSettings = configService.get('DATABASE'); 27 | } 28 | 29 | dbSettings: Database; 30 | readonly storePath = join(ROOT_DIR, 'store'); 31 | 32 | public writeStore = (create: WriteStore) => { 33 | if (!existsSync(create.path)) { 34 | mkdirSync(create.path, { recursive: true }); 35 | } 36 | try { 37 | writeFileSync(join(create.path, create.fileName + '.json'), JSON.stringify({ ...create.data }), { 38 | encoding: 'utf-8', 39 | }); 40 | 41 | return { message: 'create - success' }; 42 | } finally { 43 | create.data = undefined; 44 | } 45 | }; 46 | 47 | // eslint-disable-next-line 48 | public insert(data: any, instanceName: string, saveDb = false): Promise { 49 | throw new Error('Method not implemented.'); 50 | } 51 | 52 | // eslint-disable-next-line 53 | public update(data: any, instanceName: string, saveDb = false): Promise { 54 | throw new Error('Method not implemented.'); 55 | } 56 | 57 | // eslint-disable-next-line 58 | public find(query: any): Promise { 59 | throw new Error('Method not implemented.'); 60 | } 61 | 62 | // eslint-disable-next-line 63 | delete(query: any, force?: boolean): Promise { 64 | throw new Error('Method not implemented.'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240821194524_add_flowise_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Flowise" ( 3 | "id" TEXT NOT NULL, 4 | "enabled" BOOLEAN NOT NULL DEFAULT true, 5 | "description" VARCHAR(255), 6 | "apiUrl" VARCHAR(255), 7 | "apiKey" VARCHAR(255), 8 | "expire" INTEGER DEFAULT 0, 9 | "keywordFinish" VARCHAR(100), 10 | "delayMessage" INTEGER, 11 | "unknownMessage" VARCHAR(100), 12 | "listeningFromMe" BOOLEAN DEFAULT false, 13 | "stopBotFromMe" BOOLEAN DEFAULT false, 14 | "keepOpen" BOOLEAN DEFAULT false, 15 | "debounceTime" INTEGER, 16 | "ignoreJids" JSONB, 17 | "triggerType" "TriggerType", 18 | "triggerOperator" "TriggerOperator", 19 | "triggerValue" TEXT, 20 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 21 | "updatedAt" TIMESTAMP NOT NULL, 22 | "instanceId" TEXT NOT NULL, 23 | 24 | CONSTRAINT "Flowise_pkey" PRIMARY KEY ("id") 25 | ); 26 | 27 | -- CreateTable 28 | CREATE TABLE "FlowiseSetting" ( 29 | "id" TEXT NOT NULL, 30 | "expire" INTEGER DEFAULT 0, 31 | "keywordFinish" VARCHAR(100), 32 | "delayMessage" INTEGER, 33 | "unknownMessage" VARCHAR(100), 34 | "listeningFromMe" BOOLEAN DEFAULT false, 35 | "stopBotFromMe" BOOLEAN DEFAULT false, 36 | "keepOpen" BOOLEAN DEFAULT false, 37 | "debounceTime" INTEGER, 38 | "ignoreJids" JSONB, 39 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 40 | "updatedAt" TIMESTAMP NOT NULL, 41 | "flowiseIdFallback" VARCHAR(100), 42 | "instanceId" TEXT NOT NULL, 43 | 44 | CONSTRAINT "FlowiseSetting_pkey" PRIMARY KEY ("id") 45 | ); 46 | 47 | -- CreateIndex 48 | CREATE UNIQUE INDEX "FlowiseSetting_instanceId_key" ON "FlowiseSetting"("instanceId"); 49 | 50 | -- AddForeignKey 51 | ALTER TABLE "Flowise" ADD CONSTRAINT "Flowise_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 52 | 53 | -- AddForeignKey 54 | ALTER TABLE "FlowiseSetting" ADD CONSTRAINT "FlowiseSetting_flowiseIdFallback_fkey" FOREIGN KEY ("flowiseIdFallback") REFERENCES "Flowise"("id") ON DELETE SET NULL ON UPDATE CASCADE; 55 | 56 | -- AddForeignKey 57 | ALTER TABLE "FlowiseSetting" ADD CONSTRAINT "FlowiseSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 58 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240821171327_add_generic_bot_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "GenericBot" ( 3 | "id" TEXT NOT NULL, 4 | "enabled" BOOLEAN NOT NULL DEFAULT true, 5 | "description" VARCHAR(255), 6 | "apiUrl" VARCHAR(255), 7 | "apiKey" VARCHAR(255), 8 | "expire" INTEGER DEFAULT 0, 9 | "keywordFinish" VARCHAR(100), 10 | "delayMessage" INTEGER, 11 | "unknownMessage" VARCHAR(100), 12 | "listeningFromMe" BOOLEAN DEFAULT false, 13 | "stopBotFromMe" BOOLEAN DEFAULT false, 14 | "keepOpen" BOOLEAN DEFAULT false, 15 | "debounceTime" INTEGER, 16 | "ignoreJids" JSONB, 17 | "triggerType" "TriggerType", 18 | "triggerOperator" "TriggerOperator", 19 | "triggerValue" TEXT, 20 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 21 | "updatedAt" TIMESTAMP NOT NULL, 22 | "instanceId" TEXT NOT NULL, 23 | 24 | CONSTRAINT "GenericBot_pkey" PRIMARY KEY ("id") 25 | ); 26 | 27 | -- CreateTable 28 | CREATE TABLE "GenericSetting" ( 29 | "id" TEXT NOT NULL, 30 | "expire" INTEGER DEFAULT 0, 31 | "keywordFinish" VARCHAR(100), 32 | "delayMessage" INTEGER, 33 | "unknownMessage" VARCHAR(100), 34 | "listeningFromMe" BOOLEAN DEFAULT false, 35 | "stopBotFromMe" BOOLEAN DEFAULT false, 36 | "keepOpen" BOOLEAN DEFAULT false, 37 | "debounceTime" INTEGER, 38 | "ignoreJids" JSONB, 39 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 40 | "updatedAt" TIMESTAMP NOT NULL, 41 | "botIdFallback" VARCHAR(100), 42 | "instanceId" TEXT NOT NULL, 43 | 44 | CONSTRAINT "GenericSetting_pkey" PRIMARY KEY ("id") 45 | ); 46 | 47 | -- CreateIndex 48 | CREATE UNIQUE INDEX "GenericSetting_instanceId_key" ON "GenericSetting"("instanceId"); 49 | 50 | -- AddForeignKey 51 | ALTER TABLE "GenericBot" ADD CONSTRAINT "GenericBot_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 52 | 53 | -- AddForeignKey 54 | ALTER TABLE "GenericSetting" ADD CONSTRAINT "GenericSetting_botIdFallback_fkey" FOREIGN KEY ("botIdFallback") REFERENCES "GenericBot"("id") ON DELETE SET NULL ON UPDATE CASCADE; 55 | 56 | -- AddForeignKey 57 | ALTER TABLE "GenericSetting" ADD CONSTRAINT "GenericSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 58 | -------------------------------------------------------------------------------- /src/api/routes/business.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { NumberDto } from '@api/dto/chat.dto'; 3 | import { businessController } from '@api/server.module'; 4 | import { createMetaErrorResponse } from '@utils/errorResponse'; 5 | import { catalogSchema, collectionsSchema } from '@validate/validate.schema'; 6 | import { RequestHandler, Router } from 'express'; 7 | 8 | import { HttpStatus } from './index.router'; 9 | 10 | export class BusinessRouter extends RouterBroker { 11 | constructor(...guards: RequestHandler[]) { 12 | super(); 13 | this.router 14 | .post(this.routerPath('getCatalog'), ...guards, async (req, res) => { 15 | try { 16 | const response = await this.dataValidate({ 17 | request: req, 18 | schema: catalogSchema, 19 | ClassRef: NumberDto, 20 | execute: (instance, data) => businessController.fetchCatalog(instance, data), 21 | }); 22 | 23 | return res.status(HttpStatus.OK).json(response); 24 | } catch (error) { 25 | // Log error for debugging 26 | console.error('Business catalog error:', error); 27 | 28 | // Use utility function to create standardized error response 29 | const errorResponse = createMetaErrorResponse(error, 'business_catalog'); 30 | return res.status(errorResponse.status).json(errorResponse); 31 | } 32 | }) 33 | 34 | .post(this.routerPath('getCollections'), ...guards, async (req, res) => { 35 | try { 36 | const response = await this.dataValidate({ 37 | request: req, 38 | schema: collectionsSchema, 39 | ClassRef: NumberDto, 40 | execute: (instance, data) => businessController.fetchCollections(instance, data), 41 | }); 42 | 43 | return res.status(HttpStatus.OK).json(response); 44 | } catch (error) { 45 | // Log error for debugging 46 | console.error('Business collections error:', error); 47 | 48 | // Use utility function to create standardized error response 49 | const errorResponse = createMetaErrorResponse(error, 'business_collections'); 50 | return res.status(errorResponse.status).json(errorResponse); 51 | } 52 | }); 53 | } 54 | 55 | public readonly router: Router = Router(); 56 | } 57 | -------------------------------------------------------------------------------- /src/api/integrations/channel/whatsapp/baileys.controller.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { WAMonitoringService } from '@api/services/monitor.service'; 3 | 4 | export class BaileysController { 5 | constructor(private readonly waMonitor: WAMonitoringService) {} 6 | 7 | public async onWhatsapp({ instanceName }: InstanceDto, body: any) { 8 | const instance = this.waMonitor.waInstances[instanceName]; 9 | 10 | return instance.baileysOnWhatsapp(body?.jid); 11 | } 12 | 13 | public async profilePictureUrl({ instanceName }: InstanceDto, body: any) { 14 | const instance = this.waMonitor.waInstances[instanceName]; 15 | 16 | return instance.baileysProfilePictureUrl(body?.jid, body?.type, body?.timeoutMs); 17 | } 18 | 19 | public async assertSessions({ instanceName }: InstanceDto, body: any) { 20 | const instance = this.waMonitor.waInstances[instanceName]; 21 | 22 | return instance.baileysAssertSessions(body?.jids, body?.force); 23 | } 24 | 25 | public async createParticipantNodes({ instanceName }: InstanceDto, body: any) { 26 | const instance = this.waMonitor.waInstances[instanceName]; 27 | 28 | return instance.baileysCreateParticipantNodes(body?.jids, body?.message, body?.extraAttrs); 29 | } 30 | 31 | public async getUSyncDevices({ instanceName }: InstanceDto, body: any) { 32 | const instance = this.waMonitor.waInstances[instanceName]; 33 | 34 | return instance.baileysGetUSyncDevices(body?.jids, body?.useCache, body?.ignoreZeroDevices); 35 | } 36 | 37 | public async generateMessageTag({ instanceName }: InstanceDto) { 38 | const instance = this.waMonitor.waInstances[instanceName]; 39 | 40 | return instance.baileysGenerateMessageTag(); 41 | } 42 | 43 | public async sendNode({ instanceName }: InstanceDto, body: any) { 44 | const instance = this.waMonitor.waInstances[instanceName]; 45 | 46 | return instance.baileysSendNode(body?.stanza); 47 | } 48 | 49 | public async signalRepositoryDecryptMessage({ instanceName }: InstanceDto, body: any) { 50 | const instance = this.waMonitor.waInstances[instanceName]; 51 | 52 | return instance.baileysSignalRepositoryDecryptMessage(body?.jid, body?.type, body?.ciphertext); 53 | } 54 | 55 | public async getAuthState({ instanceName }: InstanceDto) { 56 | const instance = this.waMonitor.waInstances[instanceName]; 57 | 58 | return instance.baileysGetAuthState(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20250515211815_add_evoai_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Evoai" ( 3 | "id" TEXT NOT NULL, 4 | "enabled" BOOLEAN NOT NULL DEFAULT true, 5 | "description" VARCHAR(255), 6 | "agentUrl" VARCHAR(255), 7 | "apiKey" VARCHAR(255), 8 | "expire" INTEGER DEFAULT 0, 9 | "keywordFinish" VARCHAR(100), 10 | "delayMessage" INTEGER, 11 | "unknownMessage" VARCHAR(100), 12 | "listeningFromMe" BOOLEAN DEFAULT false, 13 | "stopBotFromMe" BOOLEAN DEFAULT false, 14 | "keepOpen" BOOLEAN DEFAULT false, 15 | "debounceTime" INTEGER, 16 | "ignoreJids" JSONB, 17 | "splitMessages" BOOLEAN DEFAULT false, 18 | "timePerChar" INTEGER DEFAULT 50, 19 | "triggerType" "TriggerType", 20 | "triggerOperator" "TriggerOperator", 21 | "triggerValue" TEXT, 22 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 23 | "updatedAt" TIMESTAMP NOT NULL, 24 | "instanceId" TEXT NOT NULL, 25 | 26 | CONSTRAINT "Evoai_pkey" PRIMARY KEY ("id") 27 | ); 28 | 29 | -- CreateTable 30 | CREATE TABLE "EvoaiSetting" ( 31 | "id" TEXT NOT NULL, 32 | "expire" INTEGER DEFAULT 0, 33 | "keywordFinish" VARCHAR(100), 34 | "delayMessage" INTEGER, 35 | "unknownMessage" VARCHAR(100), 36 | "listeningFromMe" BOOLEAN DEFAULT false, 37 | "stopBotFromMe" BOOLEAN DEFAULT false, 38 | "keepOpen" BOOLEAN DEFAULT false, 39 | "debounceTime" INTEGER, 40 | "ignoreJids" JSONB, 41 | "splitMessages" BOOLEAN DEFAULT false, 42 | "timePerChar" INTEGER DEFAULT 50, 43 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 44 | "updatedAt" TIMESTAMP NOT NULL, 45 | "evoaiIdFallback" VARCHAR(100), 46 | "instanceId" TEXT NOT NULL, 47 | 48 | CONSTRAINT "EvoaiSetting_pkey" PRIMARY KEY ("id") 49 | ); 50 | 51 | -- CreateIndex 52 | CREATE UNIQUE INDEX "EvoaiSetting_instanceId_key" ON "EvoaiSetting"("instanceId"); 53 | 54 | -- AddForeignKey 55 | ALTER TABLE "Evoai" ADD CONSTRAINT "Evoai_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 56 | 57 | -- AddForeignKey 58 | ALTER TABLE "EvoaiSetting" ADD CONSTRAINT "EvoaiSetting_evoaiIdFallback_fkey" FOREIGN KEY ("evoaiIdFallback") REFERENCES "Evoai"("id") ON DELETE SET NULL ON UPDATE CASCADE; 59 | 60 | -- AddForeignKey 61 | ALTER TABLE "EvoaiSetting" ADD CONSTRAINT "EvoaiSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 62 | -------------------------------------------------------------------------------- /src/api/integrations/channel/meta/meta.controller.ts: -------------------------------------------------------------------------------- 1 | import { PrismaRepository } from '@api/repository/repository.service'; 2 | import { WAMonitoringService } from '@api/services/monitor.service'; 3 | import { Logger } from '@config/logger.config'; 4 | import axios from 'axios'; 5 | 6 | import { ChannelController, ChannelControllerInterface } from '../channel.controller'; 7 | 8 | export class MetaController extends ChannelController implements ChannelControllerInterface { 9 | private readonly logger = new Logger('MetaController'); 10 | 11 | constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { 12 | super(prismaRepository, waMonitor); 13 | } 14 | 15 | integrationEnabled: boolean; 16 | 17 | public async receiveWebhook(data: any) { 18 | if (data.object === 'whatsapp_business_account') { 19 | if (data.entry[0]?.changes[0]?.field === 'message_template_status_update') { 20 | const template = await this.prismaRepository.template.findFirst({ 21 | where: { templateId: `${data.entry[0].changes[0].value.message_template_id}` }, 22 | }); 23 | 24 | if (!template) { 25 | console.log('template not found'); 26 | return; 27 | } 28 | 29 | const { webhookUrl } = template; 30 | 31 | await axios.post(webhookUrl, data.entry[0].changes[0].value, { 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | }); 36 | return; 37 | } 38 | 39 | data.entry?.forEach(async (entry: any) => { 40 | const numberId = entry.changes[0].value.metadata.phone_number_id; 41 | 42 | if (!numberId) { 43 | this.logger.error('WebhookService -> receiveWebhookMeta -> numberId not found'); 44 | return { 45 | status: 'success', 46 | }; 47 | } 48 | 49 | const instance = await this.prismaRepository.instance.findFirst({ 50 | where: { number: numberId }, 51 | }); 52 | 53 | if (!instance) { 54 | this.logger.error('WebhookService -> receiveWebhookMeta -> instance not found'); 55 | return { 56 | status: 'success', 57 | }; 58 | } 59 | 60 | await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data); 61 | 62 | return { 63 | status: 'success', 64 | }; 65 | }); 66 | } 67 | 68 | return { 69 | status: 'success', 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20250514232744_add_n8n_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "N8n" ( 3 | "id" TEXT NOT NULL, 4 | "enabled" BOOLEAN NOT NULL DEFAULT true, 5 | "description" VARCHAR(255), 6 | "webhookUrl" VARCHAR(255), 7 | "basicAuthUser" VARCHAR(255), 8 | "basicAuthPass" VARCHAR(255), 9 | "expire" INTEGER DEFAULT 0, 10 | "keywordFinish" VARCHAR(100), 11 | "delayMessage" INTEGER, 12 | "unknownMessage" VARCHAR(100), 13 | "listeningFromMe" BOOLEAN DEFAULT false, 14 | "stopBotFromMe" BOOLEAN DEFAULT false, 15 | "keepOpen" BOOLEAN DEFAULT false, 16 | "debounceTime" INTEGER, 17 | "ignoreJids" JSONB, 18 | "splitMessages" BOOLEAN DEFAULT false, 19 | "timePerChar" INTEGER DEFAULT 50, 20 | "triggerType" "TriggerType", 21 | "triggerOperator" "TriggerOperator", 22 | "triggerValue" TEXT, 23 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 24 | "updatedAt" TIMESTAMP NOT NULL, 25 | "instanceId" TEXT NOT NULL, 26 | 27 | CONSTRAINT "N8n_pkey" PRIMARY KEY ("id") 28 | ); 29 | 30 | -- CreateTable 31 | CREATE TABLE "N8nSetting" ( 32 | "id" TEXT NOT NULL, 33 | "expire" INTEGER DEFAULT 0, 34 | "keywordFinish" VARCHAR(100), 35 | "delayMessage" INTEGER, 36 | "unknownMessage" VARCHAR(100), 37 | "listeningFromMe" BOOLEAN DEFAULT false, 38 | "stopBotFromMe" BOOLEAN DEFAULT false, 39 | "keepOpen" BOOLEAN DEFAULT false, 40 | "debounceTime" INTEGER, 41 | "ignoreJids" JSONB, 42 | "splitMessages" BOOLEAN DEFAULT false, 43 | "timePerChar" INTEGER DEFAULT 50, 44 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 45 | "updatedAt" TIMESTAMP NOT NULL, 46 | "n8nIdFallback" VARCHAR(100), 47 | "instanceId" TEXT NOT NULL, 48 | 49 | CONSTRAINT "N8nSetting_pkey" PRIMARY KEY ("id") 50 | ); 51 | 52 | -- CreateIndex 53 | CREATE UNIQUE INDEX "N8nSetting_instanceId_key" ON "N8nSetting"("instanceId"); 54 | 55 | -- AddForeignKey 56 | ALTER TABLE "N8n" ADD CONSTRAINT "N8n_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 57 | 58 | -- AddForeignKey 59 | ALTER TABLE "N8nSetting" ADD CONSTRAINT "N8nSetting_n8nIdFallback_fkey" FOREIGN KEY ("n8nIdFallback") REFERENCES "N8n"("id") ON DELETE SET NULL ON UPDATE CASCADE; 60 | 61 | -- AddForeignKey 62 | ALTER TABLE "N8nSetting" ADD CONSTRAINT "N8nSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report a bug or unexpected behavior 3 | title: "[BUG] " 4 | labels: ["bug", "needs-triage"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | Please search existing issues before creating a new one. 13 | 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: 📋 Bug Description 18 | description: A clear and concise description of what the bug is. 19 | placeholder: Describe the bug... 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: reproduction 25 | attributes: 26 | label: 🔄 Steps to Reproduce 27 | description: Steps to reproduce the behavior 28 | placeholder: | 29 | 1. Go to '...' 30 | 2. Click on '....' 31 | 3. Scroll down to '....' 32 | 4. See error 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: expected 38 | attributes: 39 | label: ✅ Expected Behavior 40 | description: A clear and concise description of what you expected to happen. 41 | placeholder: What should happen? 42 | validations: 43 | required: true 44 | 45 | - type: textarea 46 | id: actual 47 | attributes: 48 | label: ❌ Actual Behavior 49 | description: A clear and concise description of what actually happened. 50 | placeholder: What actually happened? 51 | validations: 52 | required: true 53 | 54 | - type: textarea 55 | id: environment 56 | attributes: 57 | label: 🌍 Environment 58 | description: Please provide information about your environment 59 | value: | 60 | - OS: [e.g. Ubuntu 20.04, Windows 10, macOS 12.0] 61 | - Node.js version: [e.g. 18.17.0] 62 | - Evolution API version: [e.g. 2.3.7] 63 | - Database: [e.g. PostgreSQL 14, MySQL 8.0] 64 | - Connection type: [e.g. Baileys, WhatsApp Business API] 65 | validations: 66 | required: true 67 | 68 | - type: textarea 69 | id: logs 70 | attributes: 71 | label: 📋 Logs 72 | description: If applicable, add logs to help explain your problem. 73 | placeholder: Paste relevant logs here... 74 | render: shell 75 | 76 | - type: textarea 77 | id: additional 78 | attributes: 79 | label: 📝 Additional Context 80 | description: Add any other context about the problem here. 81 | placeholder: Any additional information... 82 | -------------------------------------------------------------------------------- /src/api/controllers/proxy.controller.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { ProxyDto } from '@api/dto/proxy.dto'; 3 | import { WAMonitoringService } from '@api/services/monitor.service'; 4 | import { ProxyService } from '@api/services/proxy.service'; 5 | import { Logger } from '@config/logger.config'; 6 | import { BadRequestException, NotFoundException } from '@exceptions'; 7 | import { makeProxyAgent } from '@utils/makeProxyAgent'; 8 | import axios from 'axios'; 9 | 10 | const logger = new Logger('ProxyController'); 11 | 12 | export class ProxyController { 13 | constructor( 14 | private readonly proxyService: ProxyService, 15 | private readonly waMonitor: WAMonitoringService, 16 | ) {} 17 | 18 | public async createProxy(instance: InstanceDto, data: ProxyDto) { 19 | if (!this.waMonitor.waInstances[instance.instanceName]) { 20 | throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`); 21 | } 22 | 23 | if (!data?.enabled) { 24 | data.host = ''; 25 | data.port = ''; 26 | data.protocol = ''; 27 | data.username = ''; 28 | data.password = ''; 29 | } 30 | 31 | if (data.host) { 32 | const testProxy = await this.testProxy(data); 33 | if (!testProxy) { 34 | throw new BadRequestException('Invalid proxy'); 35 | } 36 | } 37 | 38 | return this.proxyService.create(instance, data); 39 | } 40 | 41 | public async findProxy(instance: InstanceDto) { 42 | if (!this.waMonitor.waInstances[instance.instanceName]) { 43 | throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`); 44 | } 45 | 46 | return this.proxyService.find(instance); 47 | } 48 | 49 | public async testProxy(proxy: ProxyDto) { 50 | try { 51 | const serverIp = await axios.get('https://icanhazip.com/'); 52 | const response = await axios.get('https://icanhazip.com/', { 53 | httpsAgent: makeProxyAgent(proxy), 54 | }); 55 | 56 | const result = response?.data !== serverIp?.data; 57 | if (result) { 58 | logger.info('testProxy: proxy connection successful'); 59 | } else { 60 | logger.warn("testProxy: proxy connection doesn't change the origin IP"); 61 | } 62 | 63 | return result; 64 | } catch (error) { 65 | if (axios.isAxiosError(error)) { 66 | logger.error('testProxy error: axios error: ' + error.message); 67 | } else { 68 | logger.error('testProxy error: unexpected error: ' + error); 69 | } 70 | 71 | return false; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /prometheus.yml.example: -------------------------------------------------------------------------------- 1 | # Prometheus configuration example for Evolution API 2 | # Copy this file to prometheus.yml and adjust the settings 3 | 4 | global: 5 | scrape_interval: 15s 6 | evaluation_interval: 15s 7 | 8 | rule_files: 9 | # - "first_rules.yml" 10 | # - "second_rules.yml" 11 | 12 | scrape_configs: 13 | # Evolution API metrics 14 | - job_name: 'evolution-api' 15 | static_configs: 16 | - targets: ['localhost:8080'] # Adjust to your Evolution API URL 17 | 18 | # Metrics endpoint path 19 | metrics_path: '/metrics' 20 | 21 | # Scrape interval for this job 22 | scrape_interval: 30s 23 | 24 | # Basic authentication (if METRICS_AUTH_REQUIRED=true) 25 | basic_auth: 26 | username: 'prometheus' # Should match METRICS_USER 27 | password: 'secure_random_password_here' # Should match METRICS_PASSWORD 28 | 29 | # Optional: Add custom labels 30 | relabel_configs: 31 | - source_labels: [__address__] 32 | target_label: __param_target 33 | - source_labels: [__param_target] 34 | target_label: instance 35 | - target_label: __address__ 36 | replacement: localhost:8080 # Evolution API address 37 | 38 | # Alerting configuration (optional) 39 | alerting: 40 | alertmanagers: 41 | - static_configs: 42 | - targets: 43 | # - alertmanager:9093 44 | 45 | # Example alert rules for Evolution API 46 | # Create a file called evolution_alerts.yml with these rules: 47 | # 48 | # groups: 49 | # - name: evolution-api 50 | # rules: 51 | # - alert: EvolutionAPIDown 52 | # expr: up{job="evolution-api"} == 0 53 | # for: 1m 54 | # labels: 55 | # severity: critical 56 | # annotations: 57 | # summary: "Evolution API is down" 58 | # description: "Evolution API has been down for more than 1 minute." 59 | # 60 | # - alert: EvolutionInstanceDown 61 | # expr: evolution_instance_up == 0 62 | # for: 2m 63 | # labels: 64 | # severity: warning 65 | # annotations: 66 | # summary: "Evolution instance {{ $labels.instance }} is down" 67 | # description: "Instance {{ $labels.instance }} has been down for more than 2 minutes." 68 | # 69 | # - alert: HighInstanceCount 70 | # expr: evolution_instances_total > 100 71 | # for: 5m 72 | # labels: 73 | # severity: warning 74 | # annotations: 75 | # summary: "High number of Evolution instances" 76 | # description: "Evolution API is managing {{ $value }} instances." 77 | -------------------------------------------------------------------------------- /prisma/mysql-migrations/20250515211815_add_evoai_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `Evoai` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `enabled` BOOLEAN NOT NULL DEFAULT true, 5 | `description` VARCHAR(255), 6 | `agentUrl` VARCHAR(255), 7 | `apiKey` VARCHAR(255), 8 | `expire` INTEGER DEFAULT 0, 9 | `keywordFinish` VARCHAR(100), 10 | `delayMessage` INTEGER, 11 | `unknownMessage` VARCHAR(100), 12 | `listeningFromMe` BOOLEAN DEFAULT false, 13 | `stopBotFromMe` BOOLEAN DEFAULT false, 14 | `keepOpen` BOOLEAN DEFAULT false, 15 | `debounceTime` INTEGER, 16 | `ignoreJids` JSON, 17 | `splitMessages` BOOLEAN DEFAULT false, 18 | `timePerChar` INTEGER DEFAULT 50, 19 | `triggerType` ENUM('all', 'keyword', 'none') NULL, 20 | `triggerOperator` ENUM('contains', 'equals', 'startsWith', 'endsWith', 'regex') NULL, 21 | `triggerValue` VARCHAR(191) NULL, 22 | `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 23 | `updatedAt` TIMESTAMP NOT NULL, 24 | `instanceId` VARCHAR(191) NOT NULL, 25 | 26 | PRIMARY KEY (`id`) 27 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 28 | 29 | -- CreateTable 30 | CREATE TABLE `EvoaiSetting` ( 31 | `id` VARCHAR(191) NOT NULL, 32 | `expire` INTEGER DEFAULT 0, 33 | `keywordFinish` VARCHAR(100), 34 | `delayMessage` INTEGER, 35 | `unknownMessage` VARCHAR(100), 36 | `listeningFromMe` BOOLEAN DEFAULT false, 37 | `stopBotFromMe` BOOLEAN DEFAULT false, 38 | `keepOpen` BOOLEAN DEFAULT false, 39 | `debounceTime` INTEGER, 40 | `ignoreJids` JSON, 41 | `splitMessages` BOOLEAN DEFAULT false, 42 | `timePerChar` INTEGER DEFAULT 50, 43 | `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 44 | `updatedAt` TIMESTAMP NOT NULL, 45 | `evoaiIdFallback` VARCHAR(100), 46 | `instanceId` VARCHAR(191) NOT NULL, 47 | 48 | PRIMARY KEY (`id`) 49 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 50 | 51 | -- CreateIndex 52 | CREATE UNIQUE INDEX `EvoaiSetting_instanceId_key` ON `EvoaiSetting`(`instanceId`); 53 | 54 | -- AddForeignKey 55 | ALTER TABLE `Evoai` ADD CONSTRAINT `Evoai_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 56 | 57 | -- AddForeignKey 58 | ALTER TABLE `EvoaiSetting` ADD CONSTRAINT `EvoaiSetting_evoaiIdFallback_fkey` FOREIGN KEY (`evoaiIdFallback`) REFERENCES `Evoai`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; 59 | 60 | -- AddForeignKey 61 | ALTER TABLE `EvoaiSetting` ADD CONSTRAINT `EvoaiSetting_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 62 | -------------------------------------------------------------------------------- /prisma/mysql-migrations/20250514232744_add_n8n_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `N8n` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `enabled` BOOLEAN NOT NULL DEFAULT true, 5 | `description` VARCHAR(255), 6 | `webhookUrl` VARCHAR(255), 7 | `basicAuthUser` VARCHAR(255), 8 | `basicAuthPass` VARCHAR(255), 9 | `expire` INTEGER DEFAULT 0, 10 | `keywordFinish` VARCHAR(100), 11 | `delayMessage` INTEGER, 12 | `unknownMessage` VARCHAR(100), 13 | `listeningFromMe` BOOLEAN DEFAULT false, 14 | `stopBotFromMe` BOOLEAN DEFAULT false, 15 | `keepOpen` BOOLEAN DEFAULT false, 16 | `debounceTime` INTEGER, 17 | `ignoreJids` JSON, 18 | `splitMessages` BOOLEAN DEFAULT false, 19 | `timePerChar` INTEGER DEFAULT 50, 20 | `triggerType` ENUM('all', 'keyword', 'none') NULL, 21 | `triggerOperator` ENUM('contains', 'equals', 'startsWith', 'endsWith', 'regex') NULL, 22 | `triggerValue` VARCHAR(191) NULL, 23 | `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 24 | `updatedAt` TIMESTAMP NOT NULL, 25 | `instanceId` VARCHAR(191) NOT NULL, 26 | 27 | PRIMARY KEY (`id`) 28 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 29 | 30 | -- CreateTable 31 | CREATE TABLE `N8nSetting` ( 32 | `id` VARCHAR(191) NOT NULL, 33 | `expire` INTEGER DEFAULT 0, 34 | `keywordFinish` VARCHAR(100), 35 | `delayMessage` INTEGER, 36 | `unknownMessage` VARCHAR(100), 37 | `listeningFromMe` BOOLEAN DEFAULT false, 38 | `stopBotFromMe` BOOLEAN DEFAULT false, 39 | `keepOpen` BOOLEAN DEFAULT false, 40 | `debounceTime` INTEGER, 41 | `ignoreJids` JSON, 42 | `splitMessages` BOOLEAN DEFAULT false, 43 | `timePerChar` INTEGER DEFAULT 50, 44 | `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 45 | `updatedAt` TIMESTAMP NOT NULL, 46 | `n8nIdFallback` VARCHAR(100), 47 | `instanceId` VARCHAR(191) NOT NULL, 48 | 49 | PRIMARY KEY (`id`) 50 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 51 | 52 | -- CreateIndex 53 | CREATE UNIQUE INDEX `N8nSetting_instanceId_key` ON `N8nSetting`(`instanceId`); 54 | 55 | -- AddForeignKey 56 | ALTER TABLE `N8n` ADD CONSTRAINT `N8n_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 57 | 58 | -- AddForeignKey 59 | ALTER TABLE `N8nSetting` ADD CONSTRAINT `N8nSetting_n8nIdFallback_fkey` FOREIGN KEY (`n8nIdFallback`) REFERENCES `N8n`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; 60 | 61 | -- AddForeignKey 62 | ALTER TABLE `N8nSetting` ADD CONSTRAINT `N8nSetting_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 63 | -------------------------------------------------------------------------------- /src/api/services/cache.service.ts: -------------------------------------------------------------------------------- 1 | import { ICache } from '@api/abstract/abstract.cache'; 2 | import { Logger } from '@config/logger.config'; 3 | import { BufferJSON } from 'baileys'; 4 | 5 | export class CacheService { 6 | private readonly logger = new Logger('CacheService'); 7 | 8 | constructor(private readonly cache: ICache) { 9 | if (cache) { 10 | this.logger.verbose(`cacheservice created using cache engine: ${cache.constructor?.name}`); 11 | } else { 12 | this.logger.verbose(`cacheservice disabled`); 13 | } 14 | } 15 | 16 | async get(key: string): Promise { 17 | if (!this.cache) { 18 | return; 19 | } 20 | return this.cache.get(key); 21 | } 22 | 23 | public async hGet(key: string, field: string) { 24 | if (!this.cache) { 25 | return null; 26 | } 27 | try { 28 | const data = await this.cache.hGet(key, field); 29 | 30 | if (data) { 31 | return JSON.parse(data, BufferJSON.reviver); 32 | } 33 | 34 | return null; 35 | } catch (error) { 36 | this.logger.error(error); 37 | return null; 38 | } 39 | } 40 | 41 | async set(key: string, value: any, ttl?: number) { 42 | if (!this.cache) { 43 | return; 44 | } 45 | this.cache.set(key, value, ttl); 46 | } 47 | 48 | public async hSet(key: string, field: string, value: any) { 49 | if (!this.cache) { 50 | return; 51 | } 52 | try { 53 | const json = JSON.stringify(value, BufferJSON.replacer); 54 | 55 | await this.cache.hSet(key, field, json); 56 | } catch (error) { 57 | this.logger.error(error); 58 | } 59 | } 60 | 61 | async has(key: string) { 62 | if (!this.cache) { 63 | return; 64 | } 65 | return this.cache.has(key); 66 | } 67 | 68 | async delete(key: string) { 69 | if (!this.cache) { 70 | return; 71 | } 72 | return this.cache.delete(key); 73 | } 74 | 75 | async hDelete(key: string, field: string) { 76 | if (!this.cache) { 77 | return false; 78 | } 79 | try { 80 | await this.cache.hDelete(key, field); 81 | return true; 82 | } catch (error) { 83 | this.logger.error(error); 84 | return false; 85 | } 86 | } 87 | 88 | async deleteAll(appendCriteria?: string) { 89 | if (!this.cache) { 90 | return; 91 | } 92 | return this.cache.deleteAll(appendCriteria); 93 | } 94 | 95 | async keys(appendCriteria?: string) { 96 | if (!this.cache) { 97 | return; 98 | } 99 | return this.cache.keys(appendCriteria); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/api/integrations/channel/whatsapp/baileysMessage.processor.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@config/logger.config'; 2 | import { BaileysEventMap, MessageUpsertType, WAMessage } from 'baileys'; 3 | import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs'; 4 | 5 | type MessageUpsertPayload = BaileysEventMap['messages.upsert']; 6 | type MountProps = { 7 | onMessageReceive: (payload: MessageUpsertPayload, settings: any) => Promise; 8 | }; 9 | 10 | export class BaileysMessageProcessor { 11 | private processorLogs = new Logger('BaileysMessageProcessor'); 12 | private subscription?: Subscription; 13 | 14 | protected messageSubject = new Subject<{ 15 | messages: WAMessage[]; 16 | type: MessageUpsertType; 17 | requestId?: string; 18 | settings: any; 19 | }>(); 20 | 21 | mount({ onMessageReceive }: MountProps) { 22 | // Se já existe subscription, fazer cleanup primeiro 23 | if (this.subscription && !this.subscription.closed) { 24 | this.subscription.unsubscribe(); 25 | } 26 | 27 | // Se o Subject foi completado, recriar 28 | if (this.messageSubject.closed) { 29 | this.processorLogs.warn('MessageSubject was closed, recreating...'); 30 | this.messageSubject = new Subject<{ 31 | messages: WAMessage[]; 32 | type: MessageUpsertType; 33 | requestId?: string; 34 | settings: any; 35 | }>(); 36 | } 37 | 38 | this.subscription = this.messageSubject 39 | .pipe( 40 | tap(({ messages }) => { 41 | this.processorLogs.log(`Processing batch of ${messages.length} messages`); 42 | }), 43 | concatMap(({ messages, type, requestId, settings }) => 44 | from(onMessageReceive({ messages, type, requestId }, settings)).pipe( 45 | retryWhen((errors) => 46 | errors.pipe( 47 | tap((error) => this.processorLogs.warn(`Retrying message batch due to error: ${error.message}`)), 48 | delay(1000), // 1 segundo de delay 49 | take(3), // Máximo 3 tentativas 50 | ), 51 | ), 52 | ), 53 | ), 54 | catchError((error) => { 55 | this.processorLogs.error(`Error processing message batch: ${error}`); 56 | return EMPTY; 57 | }), 58 | ) 59 | .subscribe({ 60 | error: (error) => { 61 | this.processorLogs.error(`Message stream error: ${error}`); 62 | }, 63 | }); 64 | } 65 | 66 | processMessage(payload: MessageUpsertPayload, settings: any) { 67 | const { messages, type, requestId } = payload; 68 | this.messageSubject.next({ messages, type, requestId, settings }); 69 | } 70 | 71 | onDestroy() { 72 | this.subscription?.unsubscribe(); 73 | this.messageSubject.complete(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature Request 2 | description: Suggest a new feature or enhancement 3 | title: "[FEATURE] " 4 | labels: ["enhancement", "needs-triage"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for suggesting a new feature! 12 | Please check our [Feature Requests on Canny](https://evolutionapi.canny.io/feature-requests) first. 13 | 14 | - type: textarea 15 | id: problem 16 | attributes: 17 | label: 🎯 Problem Statement 18 | description: Is your feature request related to a problem? Please describe. 19 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: solution 25 | attributes: 26 | label: 💡 Proposed Solution 27 | description: Describe the solution you'd like 28 | placeholder: A clear and concise description of what you want to happen. 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: alternatives 34 | attributes: 35 | label: 🔄 Alternatives Considered 36 | description: Describe alternatives you've considered 37 | placeholder: A clear and concise description of any alternative solutions or features you've considered. 38 | 39 | - type: dropdown 40 | id: priority 41 | attributes: 42 | label: 📊 Priority 43 | description: How important is this feature to you? 44 | options: 45 | - Low - Nice to have 46 | - Medium - Would be helpful 47 | - High - Important for my use case 48 | - Critical - Blocking my work 49 | validations: 50 | required: true 51 | 52 | - type: dropdown 53 | id: component 54 | attributes: 55 | label: 🧩 Component 56 | description: Which component does this feature relate to? 57 | options: 58 | - WhatsApp Integration (Baileys) 59 | - WhatsApp Business API 60 | - Chatwoot Integration 61 | - Typebot Integration 62 | - OpenAI Integration 63 | - Dify Integration 64 | - API Endpoints 65 | - Database 66 | - Authentication 67 | - Webhooks 68 | - File Storage 69 | - Other 70 | 71 | - type: textarea 72 | id: use_case 73 | attributes: 74 | label: 🎯 Use Case 75 | description: Describe your specific use case for this feature 76 | placeholder: How would you use this feature? What problem does it solve for you? 77 | validations: 78 | required: true 79 | 80 | - type: textarea 81 | id: additional 82 | attributes: 83 | label: 📝 Additional Context 84 | description: Add any other context, screenshots, or examples about the feature request here. 85 | placeholder: Any additional information, mockups, or examples... 86 | -------------------------------------------------------------------------------- /src/api/dto/chat.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | proto, 3 | WAPresence, 4 | WAPrivacyGroupAddValue, 5 | WAPrivacyOnlineValue, 6 | WAPrivacyValue, 7 | WAReadReceiptsValue, 8 | } from 'baileys'; 9 | 10 | export class OnWhatsAppDto { 11 | constructor( 12 | public readonly jid: string, 13 | public readonly exists: boolean, 14 | public readonly number: string, 15 | public readonly name?: string, 16 | public readonly lid?: string, 17 | ) {} 18 | } 19 | 20 | export class getBase64FromMediaMessageDto { 21 | message: proto.WebMessageInfo; 22 | convertToMp4?: boolean; 23 | } 24 | 25 | export class WhatsAppNumberDto { 26 | numbers: string[]; 27 | } 28 | 29 | export class NumberDto { 30 | number: string; 31 | } 32 | 33 | export class NumberBusiness { 34 | wid?: string; 35 | jid?: string; 36 | exists?: boolean; 37 | isBusiness: boolean; 38 | name?: string; 39 | message?: string; 40 | description?: string; 41 | email?: string; 42 | websites?: string[]; 43 | website?: string[]; 44 | address?: string; 45 | about?: string; 46 | vertical?: string; 47 | profilehandle?: string; 48 | } 49 | 50 | export class ProfileNameDto { 51 | name: string; 52 | } 53 | 54 | export class ProfileStatusDto { 55 | status: string; 56 | } 57 | 58 | export class ProfilePictureDto { 59 | number?: string; 60 | // url or base64 61 | picture?: string; 62 | } 63 | 64 | class Key { 65 | id: string; 66 | fromMe: boolean; 67 | remoteJid: string; 68 | } 69 | export class ReadMessageDto { 70 | readMessages: Key[]; 71 | } 72 | 73 | export class LastMessage { 74 | key: Key; 75 | messageTimestamp?: number; 76 | } 77 | 78 | export class ArchiveChatDto { 79 | lastMessage?: LastMessage; 80 | chat?: string; 81 | archive: boolean; 82 | } 83 | 84 | export class MarkChatUnreadDto { 85 | lastMessage?: LastMessage; 86 | chat?: string; 87 | } 88 | 89 | export class PrivacySettingDto { 90 | readreceipts: WAReadReceiptsValue; 91 | profile: WAPrivacyValue; 92 | status: WAPrivacyValue; 93 | online: WAPrivacyOnlineValue; 94 | last: WAPrivacyValue; 95 | groupadd: WAPrivacyGroupAddValue; 96 | } 97 | 98 | export class DeleteMessage { 99 | id: string; 100 | fromMe: boolean; 101 | remoteJid: string; 102 | participant?: string; 103 | } 104 | export class Options { 105 | delay?: number; 106 | presence?: WAPresence; 107 | } 108 | class OptionsMessage { 109 | options: Options; 110 | } 111 | export class Metadata extends OptionsMessage { 112 | number: string; 113 | } 114 | 115 | export class SendPresenceDto extends Metadata { 116 | presence: WAPresence; 117 | delay: number; 118 | } 119 | 120 | export class UpdateMessageDto extends Metadata { 121 | number: string; 122 | key: proto.IMessageKey; 123 | text: string; 124 | } 125 | 126 | export class BlockUserDto { 127 | number: string; 128 | status: 'block' | 'unblock'; 129 | } 130 | -------------------------------------------------------------------------------- /src/utils/use-multi-file-auth-state-redis-db.ts: -------------------------------------------------------------------------------- 1 | import { CacheService } from '@api/services/cache.service'; 2 | import { Logger } from '@config/logger.config'; 3 | import { AuthenticationCreds, AuthenticationState, initAuthCreds, proto, SignalDataTypeMap } from 'baileys'; 4 | 5 | export async function useMultiFileAuthStateRedisDb( 6 | instanceName: string, 7 | cache: CacheService, 8 | ): Promise<{ 9 | state: AuthenticationState; 10 | saveCreds: () => Promise; 11 | removeCreds: () => Promise; 12 | }> { 13 | const logger = new Logger('useMultiFileAuthStateRedisDb'); 14 | 15 | const writeData = async (data: any, key: string): Promise => { 16 | try { 17 | return await cache.hSet(instanceName, key, data); 18 | } catch (error) { 19 | return logger.error({ localError: 'writeData', error }); 20 | } 21 | }; 22 | 23 | const readData = async (key: string): Promise => { 24 | try { 25 | return await cache.hGet(instanceName, key); 26 | } catch (error) { 27 | logger.error({ localError: 'readData', error }); 28 | return; 29 | } 30 | }; 31 | 32 | const removeData = async (key: string) => { 33 | try { 34 | return await cache.hDelete(instanceName, key); 35 | } catch (error) { 36 | logger.error({ readData: 'removeData', error }); 37 | } 38 | }; 39 | 40 | async function removeCreds(): Promise { 41 | try { 42 | logger.warn({ action: 'redis.delete', instanceName }); 43 | 44 | return await cache.delete(instanceName); 45 | } catch { 46 | return; 47 | } 48 | } 49 | 50 | const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); 51 | 52 | return { 53 | state: { 54 | creds, 55 | keys: { 56 | get: async (type, ids: string[]) => { 57 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 58 | // @ts-ignore 59 | const data: { [_: string]: SignalDataTypeMap[type] } = {}; 60 | await Promise.all( 61 | ids.map(async (id) => { 62 | let value = await readData(`${type}-${id}`); 63 | if (type === 'app-state-sync-key' && value) { 64 | value = proto.Message.AppStateSyncKeyData.create(value); 65 | } 66 | 67 | data[id] = value; 68 | }), 69 | ); 70 | 71 | return data; 72 | }, 73 | set: async (data: any) => { 74 | const tasks: Promise[] = []; 75 | for (const category in data) { 76 | for (const id in data[category]) { 77 | const value = data[category][id]; 78 | const key = `${category}-${id}`; 79 | tasks.push(value ? await writeData(value, key) : await removeData(key)); 80 | } 81 | } 82 | 83 | await Promise.all(tasks); 84 | }, 85 | }, 86 | }, 87 | saveCreds: async () => { 88 | return await writeData(creds, 'creds'); 89 | }, 90 | 91 | removeCreds, 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/api/integrations/channel/whatsapp/voiceCalls/transport.type.ts: -------------------------------------------------------------------------------- 1 | import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys'; 2 | 3 | export interface ServerToClientEvents { 4 | withAck: (d: string, callback: (e: number) => void) => void; 5 | onWhatsApp: onWhatsAppType; 6 | profilePictureUrl: ProfilePictureUrlType; 7 | assertSessions: AssertSessionsType; 8 | createParticipantNodes: CreateParticipantNodesType; 9 | getUSyncDevices: GetUSyncDevicesType; 10 | generateMessageTag: GenerateMessageTagType; 11 | sendNode: SendNodeType; 12 | 'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType; 13 | } 14 | 15 | export interface ClientToServerEvents { 16 | init: ( 17 | me: Contact | undefined, 18 | account: proto.IADVSignedDeviceIdentity | undefined, 19 | status: WAConnectionState, 20 | ) => void; 21 | 'CB:call': (packet: any) => void; 22 | 'CB:ack,class:call': (packet: any) => void; 23 | 'connection.update:status': ( 24 | me: Contact | undefined, 25 | account: proto.IADVSignedDeviceIdentity | undefined, 26 | status: WAConnectionState, 27 | ) => void; 28 | 'connection.update:qr': (qr: string) => void; 29 | } 30 | 31 | export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void; 32 | export type onWhatsAppCallback = ( 33 | response: { 34 | exists: boolean; 35 | jid: string; 36 | }[], 37 | ) => void; 38 | 39 | export type ProfilePictureUrlType = ( 40 | jid: string, 41 | type: 'image' | 'preview', 42 | timeoutMs: number | undefined, 43 | callback: ProfilePictureUrlCallback, 44 | ) => void; 45 | export type ProfilePictureUrlCallback = (response: string | undefined) => void; 46 | 47 | export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void; 48 | export type AssertSessionsCallback = (response: boolean) => void; 49 | 50 | export type CreateParticipantNodesType = ( 51 | jids: string[], 52 | message: any, 53 | extraAttrs: any, 54 | callback: CreateParticipantNodesCallback, 55 | ) => void; 56 | export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void; 57 | 58 | export type GetUSyncDevicesType = ( 59 | jids: string[], 60 | useCache: boolean, 61 | ignoreZeroDevices: boolean, 62 | callback: GetUSyncDevicesTypeCallback, 63 | ) => void; 64 | export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void; 65 | 66 | export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void; 67 | export type GenerateMessageTagTypeCallback = (response: string) => void; 68 | 69 | export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void; 70 | export type SendNodeTypeCallback = (response: boolean) => void; 71 | 72 | export type SignalRepositoryDecryptMessageType = ( 73 | jid: string, 74 | type: 'pkmsg' | 'msg', 75 | ciphertext: Buffer, 76 | callback: SignalRepositoryDecryptMessageCallback, 77 | ) => void; 78 | export type SignalRepositoryDecryptMessageCallback = (response: any) => void; 79 | --------------------------------------------------------------------------------