├── .DS_Store ├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── -en--bug-report.yaml │ ├── -en--feature-request.yaml │ ├── -pt--reportar-bug.yaml │ └── -pt--solicitar-recurso.yaml └── workflows │ ├── check_code_quality.yml │ ├── publish_docker_image.yml │ ├── publish_docker_image_homolog.yml │ └── publish_docker_image_latest.yml ├── .gitignore ├── .prettierrc.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── Docker ├── minio │ └── docker-compose.yaml ├── mysql │ └── docker-compose.yaml ├── postgres │ └── docker-compose.yaml ├── rabbitmq │ └── docker-compose.yaml ├── redis │ └── docker-compose.yaml ├── scripts │ ├── deploy_database.sh │ ├── env_functions.sh │ └── generate_database.sh └── swarm │ └── evolution_api_v2.yaml ├── Dockerfile ├── Extras └── chatwoot │ ├── configurar_admin.json │ ├── criador_de_empresas.json │ └── criador_de_inbox.json ├── LICENSE ├── README.md ├── docker-compose.dev.yaml ├── docker-compose.yaml ├── local_install.sh ├── manager └── dist │ ├── assets │ ├── images │ │ └── evolution-logo.png │ ├── index-CFAZX6IV.js │ └── index-DNOCacL_.css │ └── index.html ├── package-lock.json ├── package.json ├── prisma ├── mysql-migrations │ ├── 20240809105427_init │ │ └── migration.sql │ ├── 20240813153900_add_unique_index_for_remoted_jid_and_instance_in_contacts │ │ └── migration.sql │ ├── 20240814173138_add_ignore_jids_chatwoot │ │ └── migration.sql │ ├── 20240814214314_integrations_unification │ │ └── migration.sql │ ├── 20240821203259_add_postgres_migrations │ │ └── migration.sql │ ├── 20240824162012_add_type_on_integration_sessions │ │ └── migration.sql │ ├── 20240825131301_change_to_evolution_bot │ │ └── migration.sql │ ├── 20241001172800_add_message_status │ │ └── migration.sql │ ├── 20241108101333_fix_message_status_as_string │ │ └── migration.sql │ └── migration_lock.toml ├── mysql-schema.prisma ├── postgresql-migrations │ ├── 20240609181238_init │ │ └── migration.sql │ ├── 20240610144159_create_column_profile_name_instance │ │ └── migration.sql │ ├── 20240611125754_create_columns_whitelabel_chatwoot │ │ └── migration.sql │ ├── 20240611202817_create_columns_debounce_time_typebot │ │ └── migration.sql │ ├── 20240712144948_add_business_id_column_to_instances │ │ └── migration.sql │ ├── 20240712150256_create_templates_table │ │ └── migration.sql │ ├── 20240712155950_adjusts_in_templates_table │ │ └── migration.sql │ ├── 20240712162206_remove_templates_table │ │ └── migration.sql │ ├── 20240712223655_column_fallback_typebot │ │ └── migration.sql │ ├── 20240712230631_column_ignore_jids_typebot │ │ └── migration.sql │ ├── 20240713184337_add_media_table │ │ └── migration.sql │ ├── 20240718121437_add_openai_tables │ │ └── migration.sql │ ├── 20240718123923_adjusts_openai_tables │ │ └── migration.sql │ ├── 20240722173259_add_name_column_to_openai_creds │ │ └── migration.sql │ ├── 20240722173518_add_name_column_to_openai_creds │ │ └── migration.sql │ ├── 20240723152648_adjusts_in_column_openai_creds │ │ └── migration.sql │ ├── 20240723200254_add_webhookurl_on_message │ │ └── migration.sql │ ├── 20240725184147_create_template_table │ │ └── migration.sql │ ├── 20240725202651_add_webhook_url_template_table │ │ └── migration.sql │ ├── 20240725221646_modify_token_instance_table │ │ └── migration.sql │ ├── 20240729115127_modify_trigger_type_openai_typebot_table │ │ └── migration.sql │ ├── 20240729180347_modify_typebot_session_status_openai_typebot_table │ │ └── migration.sql │ ├── 20240730152156_create_dify_tables │ │ └── migration.sql │ ├── 20240801193907_add_column_speech_to_text_openai_setting_table │ │ └── migration.sql │ ├── 20240803163908_add_column_description_on_integrations_table │ │ └── migration.sql │ ├── 20240808210239_add_column_function_url_openaibot_table │ │ └── migration.sql │ ├── 20240811021156_add_chat_name_column │ │ └── migration.sql │ ├── 20240811183328_add_unique_index_for_remoted_jid_and_instance_in_contacts │ │ └── migration.sql │ ├── 20240813003116_make_label_unique_for_instance │ │ └── migration.sql │ ├── 20240814173033_add_ignore_jids_chatwoot │ │ └── migration.sql │ ├── 20240814202359_integrations_unification │ │ └── migration.sql │ ├── 20240817110155_add_trigger_type_advanced │ │ └── migration.sql │ ├── 20240819154941_add_context_to_integration_session │ │ └── migration.sql │ ├── 20240821120816_bot_id_integration_session │ │ └── migration.sql │ ├── 20240821171327_add_generic_bot_table │ │ └── migration.sql │ ├── 20240821194524_add_flowise_table │ │ └── migration.sql │ ├── 20240824161333_add_type_on_integration_sessions │ │ └── migration.sql │ ├── 20240825130616_change_to_evolution_bot │ │ └── migration.sql │ ├── 20240828140837_add_is_on_whatsapp_table │ │ └── migration.sql │ ├── 20240828141556_remove_name_column_from_on_whatsapp_table │ │ └── migration.sql │ ├── 20240830193533_changed_table_case │ │ └── migration.sql │ ├── 20240906202019_add_headers_on_webhook_config │ │ └── migration.sql │ ├── 20241001180457_add_message_status │ │ └── migration.sql │ ├── 20241006130306_alter_status_on_message_table │ │ └── migration.sql │ ├── 20241007164026_add_unread_messages_on_chat_table │ │ └── migration.sql │ ├── 20241011085129_create_pusher_table │ │ └── migration.sql │ ├── 20241011100803_split_messages_and_time_per_char_integrations │ │ └── migration.sql │ ├── 20241017144950_create_index │ │ └── migration.sql │ ├── 20250116001415_add_wavoip_token_to_settings_table │ │ └── migration.sql │ └── migration_lock.toml └── postgresql-schema.prisma ├── public └── images │ ├── atendai-logo.png │ ├── bmc_qr.png │ ├── code.png │ ├── cover.png │ ├── evolution-logo.png │ ├── evolution-pro.png │ ├── picpay-image.png │ ├── picpay-qr.jpeg │ ├── qrcode-pix.png │ └── video-cover.png ├── runWithProvider.js ├── src ├── @types │ └── express.d.ts ├── api │ ├── abstract │ │ ├── abstract.cache.ts │ │ ├── abstract.repository.ts │ │ └── abstract.router.ts │ ├── controllers │ │ ├── call.controller.ts │ │ ├── chat.controller.ts │ │ ├── group.controller.ts │ │ ├── instance.controller.ts │ │ ├── label.controller.ts │ │ ├── proxy.controller.ts │ │ ├── sendMessage.controller.ts │ │ ├── settings.controller.ts │ │ └── template.controller.ts │ ├── dto │ │ ├── call.dto.ts │ │ ├── chat.dto.ts │ │ ├── chatbot.dto.ts │ │ ├── group.dto.ts │ │ ├── instance.dto.ts │ │ ├── label.dto.ts │ │ ├── proxy.dto.ts │ │ ├── sendMessage.dto.ts │ │ ├── settings.dto.ts │ │ └── template.dto.ts │ ├── guards │ │ ├── auth.guard.ts │ │ ├── instance.guard.ts │ │ └── telemetry.guard.ts │ ├── integrations │ │ ├── channel │ │ │ ├── channel.controller.ts │ │ │ ├── channel.router.ts │ │ │ ├── evolution │ │ │ │ ├── evolution.channel.service.ts │ │ │ │ ├── evolution.controller.ts │ │ │ │ └── evolution.router.ts │ │ │ ├── meta │ │ │ │ ├── meta.controller.ts │ │ │ │ ├── meta.router.ts │ │ │ │ └── whatsapp.business.service.ts │ │ │ └── whatsapp │ │ │ │ ├── baileys.controller.ts │ │ │ │ ├── baileys.router.ts │ │ │ │ ├── voiceCalls │ │ │ │ ├── transport.type.ts │ │ │ │ └── useVoiceCallsBaileys.ts │ │ │ │ └── whatsapp.baileys.service.ts │ │ ├── chatbot │ │ │ ├── chatbot.controller.ts │ │ │ ├── chatbot.router.ts │ │ │ ├── chatbot.schema.ts │ │ │ ├── chatwoot │ │ │ │ ├── controllers │ │ │ │ │ └── chatwoot.controller.ts │ │ │ │ ├── dto │ │ │ │ │ └── chatwoot.dto.ts │ │ │ │ ├── libs │ │ │ │ │ └── postgres.client.ts │ │ │ │ ├── routes │ │ │ │ │ └── chatwoot.router.ts │ │ │ │ ├── services │ │ │ │ │ └── chatwoot.service.ts │ │ │ │ ├── utils │ │ │ │ │ └── chatwoot-import-helper.ts │ │ │ │ └── validate │ │ │ │ │ └── chatwoot.schema.ts │ │ │ ├── dify │ │ │ │ ├── controllers │ │ │ │ │ └── dify.controller.ts │ │ │ │ ├── dto │ │ │ │ │ └── dify.dto.ts │ │ │ │ ├── routes │ │ │ │ │ └── dify.router.ts │ │ │ │ ├── services │ │ │ │ │ └── dify.service.ts │ │ │ │ └── validate │ │ │ │ │ └── dify.schema.ts │ │ │ ├── evolutionBot │ │ │ │ ├── controllers │ │ │ │ │ └── evolutionBot.controller.ts │ │ │ │ ├── dto │ │ │ │ │ └── evolutionBot.dto.ts │ │ │ │ ├── routes │ │ │ │ │ └── evolutionBot.router.ts │ │ │ │ ├── services │ │ │ │ │ └── evolutionBot.service.ts │ │ │ │ └── validate │ │ │ │ │ └── evolutionBot.schema.ts │ │ │ ├── flowise │ │ │ │ ├── controllers │ │ │ │ │ └── flowise.controller.ts │ │ │ │ ├── dto │ │ │ │ │ └── flowise.dto.ts │ │ │ │ ├── routes │ │ │ │ │ └── flowise.router.ts │ │ │ │ ├── services │ │ │ │ │ └── flowise.service.ts │ │ │ │ └── validate │ │ │ │ │ └── flowise.schema.ts │ │ │ ├── openai │ │ │ │ ├── controllers │ │ │ │ │ └── openai.controller.ts │ │ │ │ ├── dto │ │ │ │ │ └── openai.dto.ts │ │ │ │ ├── routes │ │ │ │ │ └── openai.router.ts │ │ │ │ ├── services │ │ │ │ │ └── openai.service.ts │ │ │ │ └── validate │ │ │ │ │ └── openai.schema.ts │ │ │ └── typebot │ │ │ │ ├── controllers │ │ │ │ └── typebot.controller.ts │ │ │ │ ├── dto │ │ │ │ └── typebot.dto.ts │ │ │ │ ├── routes │ │ │ │ └── typebot.router.ts │ │ │ │ ├── services │ │ │ │ └── typebot.service.ts │ │ │ │ └── validate │ │ │ │ └── typebot.schema.ts │ │ ├── event │ │ │ ├── event.controller.ts │ │ │ ├── event.dto.ts │ │ │ ├── event.manager.ts │ │ │ ├── event.router.ts │ │ │ ├── event.schema.ts │ │ │ ├── pusher │ │ │ │ ├── pusher.controller.ts │ │ │ │ ├── pusher.router.ts │ │ │ │ └── pusher.schema.ts │ │ │ ├── rabbitmq │ │ │ │ ├── rabbitmq.controller.ts │ │ │ │ └── rabbitmq.router.ts │ │ │ ├── sqs │ │ │ │ ├── sqs.controller.ts │ │ │ │ └── sqs.router.ts │ │ │ ├── webhook │ │ │ │ ├── webhook.controller.ts │ │ │ │ ├── webhook.router.ts │ │ │ │ └── webhook.schema.ts │ │ │ └── websocket │ │ │ │ ├── websocket.controller.ts │ │ │ │ └── websocket.router.ts │ │ ├── integration.dto.ts │ │ └── storage │ │ │ ├── s3 │ │ │ ├── controllers │ │ │ │ └── s3.controller.ts │ │ │ ├── dto │ │ │ │ └── media.dto.ts │ │ │ ├── libs │ │ │ │ └── minio.server.ts │ │ │ ├── routes │ │ │ │ └── s3.router.ts │ │ │ ├── services │ │ │ │ └── s3.service.ts │ │ │ └── validate │ │ │ │ └── s3.schema.ts │ │ │ └── storage.router.ts │ ├── provider │ │ └── sessions.ts │ ├── repository │ │ └── repository.service.ts │ ├── routes │ │ ├── call.router.ts │ │ ├── chat.router.ts │ │ ├── group.router.ts │ │ ├── index.router.ts │ │ ├── instance.router.ts │ │ ├── label.router.ts │ │ ├── proxy.router.ts │ │ ├── sendMessage.router.ts │ │ ├── settings.router.ts │ │ ├── template.router.ts │ │ └── view.router.ts │ ├── server.module.ts │ ├── services │ │ ├── auth.service.ts │ │ ├── cache.service.ts │ │ ├── channel.service.ts │ │ ├── monitor.service.ts │ │ ├── proxy.service.ts │ │ ├── settings.service.ts │ │ └── template.service.ts │ └── types │ │ └── wa.types.ts ├── cache │ ├── cacheengine.ts │ ├── localcache.ts │ ├── rediscache.client.ts │ └── rediscache.ts ├── config │ ├── env.config.ts │ ├── error.config.ts │ ├── event.config.ts │ ├── logger.config.ts │ └── path.config.ts ├── exceptions │ ├── 400.exception.ts │ ├── 401.exception.ts │ ├── 403.exception.ts │ ├── 404.exception.ts │ ├── 500.exception.ts │ └── index.ts ├── main.ts ├── utils │ ├── advancedOperatorsSearch.ts │ ├── createJid.ts │ ├── findBotByTrigger.ts │ ├── getConversationMessage.ts │ ├── i18n.ts │ ├── instrumentSentry.ts │ ├── makeProxyAgent.ts │ ├── onWhatsappCache.ts │ ├── renderStatus.ts │ ├── sendTelemetry.ts │ ├── server-up.ts │ ├── translations │ │ ├── en.json │ │ ├── es.json │ │ └── pt-BR.json │ ├── use-multi-file-auth-state-prisma.ts │ ├── use-multi-file-auth-state-provider-files.ts │ └── use-multi-file-auth-state-redis-db.ts └── validate │ ├── chat.schema.ts │ ├── group.schema.ts │ ├── instance.schema.ts │ ├── label.schema.ts │ ├── message.schema.ts │ ├── proxy.schema.ts │ ├── settings.schema.ts │ ├── template.schema.ts │ └── validate.schema.ts ├── tsconfig.json └── tsup.config.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *Dockerfile* 3 | *docker-compose* 4 | node_modules 5 | dist -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node-modules 2 | /dist -------------------------------------------------------------------------------- /.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/ban-types': [ 35 | 'error', 36 | { 37 | extendDefaults: true, 38 | types: { 39 | '{}': false, 40 | Object: false, 41 | }, 42 | }, 43 | ], 44 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-en--feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest ideas for the project. 3 | labels: 4 | - enhancement 5 | - en 6 | # Automatically assign the issue to: 7 | # assignees: DavidsonGomes 8 | body: 9 | - type: checkboxes 10 | id: terms 11 | attributes: 12 | label: Welcome! 13 | description: '**DO NOT OPEN FOR GENERAL SUPPORT QUESTIONS.**' 14 | 15 | options: 16 | - label: Yes, I have searched for similar requests on [GitHub](https://github.com/code-chat-br/whatsapp-api/issues) and found none. 17 | required: true 18 | 19 | - type: dropdown 20 | attributes: 21 | label: What type of feature? 22 | description: | 23 | How to write a good feature request? 24 | 25 | - Respect the issue template as much as possible. 26 | - The title should be short and descriptive. 27 | - Explain the conditions that led you to suggest this feature: the context. 28 | - The context should lead to something, an idea or problem you are facing. 29 | - Be clear and concise. 30 | - Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown) 31 | options: 32 | - Integration 33 | - Functionality 34 | - Endpoint 35 | - Database adjustment 36 | - Other 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | attributes: 42 | label: What is the motivation for the request? 43 | description: | 44 | What problem does the feature seek to solve? 45 | Clearly and in detail describe the functionality you want to be implemented. 46 | Explain how it fits into the context of the project. 47 | placeholder: Detailed description 48 | validations: 49 | required: true 50 | 51 | - type: textarea 52 | attributes: 53 | label: Usage Examples 54 | description: | 55 | Provide specific examples of how this functionality could be used. 56 | This can include scenarios or use cases where the feature would be particularly beneficial. 57 | placeholder: text - image - video - flowcharts 58 | validations: 59 | required: false 60 | 61 | - type: textarea 62 | attributes: 63 | label: How should the feature be developed? 64 | description: | 65 | Should it be inserted directly into the code? 66 | Should it be built as a different application that acts as an extension of the API? 67 | For example: a `worker`? 68 | 69 | Discuss how this new functionality could impact other parts of the project, if applicable. 70 | 71 | --- 72 | 73 | If you have ideas on how this functionality could be implemented, please share them here. 74 | This is not mandatory, but it can be helpful for the development team. 75 | 76 | placeholder: Insert feature ideas here 77 | validations: 78 | required: false 79 | 80 | - type: textarea 81 | attributes: 82 | label: Additional Notes 83 | description: Any other information you believe is relevant to your request. 84 | placeholder: Insert your observations here. 85 | validations: 86 | required: false 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-pt--solicitar-recurso.yaml: -------------------------------------------------------------------------------- 1 | name: Solicitação de recursos 2 | description: Sugira ideias para o projeto. 3 | labels: 4 | - enhancement 5 | - pt-br 6 | # Automatically assign the issue to: 7 | # assignees: DavidsonGomes 8 | body: 9 | - type: checkboxes 10 | id: termos 11 | attributes: 12 | label: Bem-vindo! 13 | description: '**NÃO ABRA PARA PERGUNTAS GERAIS DE SUPORTE.**' 14 | 15 | options: 16 | - label: Sim, pesquisei solicitações semelhantes no [GitHub](https://github.com/code-chat-br/whatsapp-api/issues) e não encontrei nenhum. 17 | required: true 18 | 19 | - type: dropdown 20 | attributes: 21 | label: Qual tipo de recurso? 22 | description: | 23 | Como escrever uma boa solicitação de bug? 24 | 25 | - Respeite o modelo de problema tanto quanto possível. 26 | - O título deve ser curto e descritivo. 27 | - Explique as condições que o levaram a reportar este problema: o contexto. 28 | - O contexto deve levar a algo, ideia ou problema que você está enfrentando. 29 | - Seja claro e conciso. 30 | - Formate suas mensagens para ajudar o leitor a se concentrar no que importa e entender a estrutura da sua mensagem, use [sintaxe Markdown](https://help.github.com/articles/github-flavored-markdown) 31 | options: 32 | - Integração 33 | - Funcionalidade 34 | - Endpoint 35 | - Ajuste de banco de dados 36 | - Outro 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | attributes: 42 | label: Qual a motivação para a solicitação? 43 | description: | 44 | Qual problema o recurso busca resolver? 45 | Descreva claramente e em detalhes a funcionalidade que você deseja que seja implementada. 46 | Explique como isso se encaixa no contexto do projeto. 47 | placeholder: Descrição detalhada 48 | validations: 49 | required: true 50 | 51 | - type: textarea 52 | attributes: 53 | label: Exemplos de Uso 54 | description: | 55 | Forneça exemplos específicos de como essa funcionalidade poderia ser utilizada. 56 | Isso pode incluir cenários ou casos de uso onde a funcionalidade seria particularmente benéfica. 57 | placeholder: texto - imagem - video - fluxogramas 58 | validations: 59 | required: false 60 | 61 | - type: textarea 62 | attributes: 63 | label: Como o recurso deve ser desenvolvido? 64 | description: | 65 | Deve ser inserido diretamente no código? 66 | Deve ser construído uma aplicação diferente que atuará como uma extenção da api? 67 | Por exemplo: um `worker`? 68 | 69 | Discuta como essa nova funcionalidade poderia impactar outras partes do projeto, se aplicável. 70 | 71 | --- 72 | 73 | Se você tem ideias sobre como essa funcionalidade pode ser implementada, por favor, compartilhe-as aqui. 74 | Isso não é obrigatório, mas pode ser útil para a equipe de desenvolvimento. 75 | 76 | placeholder: Insira ideias para o recurso 77 | validations: 78 | required: false 79 | 80 | - type: textarea 81 | attributes: 82 | label: Notas Adicionais 83 | description: Qualquer outra informação que você acredita ser relevante para a sua solicitação. 84 | placeholder: Insira aqui as sua observções. 85 | validations: 86 | required: false 87 | -------------------------------------------------------------------------------- /.github/workflows/check_code_quality.yml: -------------------------------------------------------------------------------- 1 | name: Check Code Quality 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | check-lint-and-build: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 10 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Install Node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 20.x 17 | 18 | - name: Install packages 19 | run: npm install 20 | 21 | - name: Check linting 22 | run: npm run lint:check 23 | 24 | - name: Check build 25 | run: npm run db:generate 26 | 27 | - name: Check build 28 | run: npm run build -------------------------------------------------------------------------------- /.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@v4 18 | 19 | - name: Docker meta 20 | id: meta 21 | uses: docker/metadata-action@v5 22 | with: 23 | images: atendai/evolution-api 24 | tags: type=semver,pattern=v{{version}} 25 | 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | 32 | - name: Login to GitHub Container Registry 33 | uses: docker/login-action@v3 34 | with: 35 | username: ${{ secrets.DOCKER_USERNAME }} 36 | password: ${{ secrets.DOCKER_PASSWORD }} 37 | 38 | - name: Build and push 39 | id: docker_build 40 | uses: docker/build-push-action@v5 41 | with: 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | tags: ${{ steps.meta.outputs.tags }} 45 | labels: ${{ steps.meta.outputs.labels }} 46 | 47 | - name: Image digest 48 | run: echo ${{ steps.docker_build.outputs.digest }} -------------------------------------------------------------------------------- /.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@v4 18 | 19 | - name: Docker meta 20 | id: meta 21 | uses: docker/metadata-action@v5 22 | with: 23 | images: atendai/evolution-api 24 | tags: homolog 25 | 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | 32 | - name: Login to Docker Hub 33 | uses: docker/login-action@v3 34 | with: 35 | username: ${{ secrets.DOCKER_USERNAME }} 36 | password: ${{ secrets.DOCKER_PASSWORD }} 37 | 38 | - name: Build and push 39 | id: docker_build 40 | uses: docker/build-push-action@v5 41 | with: 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | tags: ${{ steps.meta.outputs.tags }} 45 | labels: ${{ steps.meta.outputs.labels }} 46 | 47 | - name: Image digest 48 | run: echo ${{ steps.docker_build.outputs.digest }} 49 | -------------------------------------------------------------------------------- /.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@v4 18 | 19 | - name: Docker meta 20 | id: meta 21 | uses: docker/metadata-action@v5 22 | with: 23 | images: atendai/evolution-api 24 | tags: latest 25 | 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | 32 | - name: Login to Docker Hub 33 | uses: docker/login-action@v3 34 | with: 35 | username: ${{ secrets.DOCKER_USERNAME }} 36 | password: ${{ secrets.DOCKER_PASSWORD }} 37 | 38 | - name: Build and push 39 | id: docker_build 40 | uses: docker/build-push-action@v5 41 | with: 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | tags: ${{ steps.meta.outputs.tags }} 45 | labels: ${{ steps.meta.outputs.labels }} 46 | 47 | - name: Image digest 48 | run: echo ${{ steps.docker_build.outputs.digest }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | /Docker/.env 6 | 7 | .vscode 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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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" ]]; 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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" ]]; 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 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS builder 2 | 3 | RUN apk update && \ 4 | apk add git ffmpeg wget curl bash openssl 5 | 6 | LABEL version="2.2.3" description="Api to control whatsapp features through http requests." 7 | LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" 8 | LABEL contact="contato@atendai.com" 9 | 10 | WORKDIR /evolution 11 | 12 | COPY ./package.json ./tsconfig.json ./ 13 | 14 | RUN npm install 15 | 16 | COPY ./src ./src 17 | COPY ./public ./public 18 | COPY ./prisma ./prisma 19 | COPY ./manager ./manager 20 | COPY ./.env.example ./.env 21 | COPY ./runWithProvider.js ./ 22 | COPY ./tsup.config.ts ./ 23 | 24 | COPY ./Docker ./Docker 25 | 26 | RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/* 27 | 28 | RUN ./Docker/scripts/generate_database.sh 29 | 30 | RUN npm run build 31 | 32 | FROM node:20-alpine AS final 33 | 34 | RUN apk update && \ 35 | apk add tzdata ffmpeg bash openssl 36 | 37 | ENV TZ=America/Sao_Paulo 38 | 39 | WORKDIR /evolution 40 | 41 | COPY --from=builder /evolution/package.json ./package.json 42 | COPY --from=builder /evolution/package-lock.json ./package-lock.json 43 | 44 | COPY --from=builder /evolution/node_modules ./node_modules 45 | COPY --from=builder /evolution/dist ./dist 46 | COPY --from=builder /evolution/prisma ./prisma 47 | COPY --from=builder /evolution/manager ./manager 48 | COPY --from=builder /evolution/public ./public 49 | COPY --from=builder /evolution/.env ./.env 50 | COPY --from=builder /evolution/Docker ./Docker 51 | COPY --from=builder /evolution/runWithProvider.js ./runWithProvider.js 52 | COPY --from=builder /evolution/tsup.config.ts ./tsup.config.ts 53 | 54 | ENV DOCKER_ENV=true 55 | 56 | EXPOSE 8080 57 | 58 | ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ] -------------------------------------------------------------------------------- /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@atendai.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 | © 2024 Evolution API 21 | 22 | -------------------------------------------------------------------------------- /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 | volumes: 19 | evolution_instances: 20 | 21 | 22 | networks: 23 | evolution-net: 24 | name: evolution-net 25 | driver: bridge 26 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | container_name: evolution_api 4 | image: atendai/evolution-api:homolog 5 | restart: always 6 | depends_on: 7 | - redis 8 | - postgres 9 | ports: 10 | - 8080:8080 11 | volumes: 12 | - evolution_instances:/evolution/instances 13 | networks: 14 | - evolution-net 15 | env_file: 16 | - .env 17 | expose: 18 | - 8080 19 | 20 | redis: 21 | image: redis:latest 22 | networks: 23 | - evolution-net 24 | container_name: redis 25 | command: > 26 | redis-server --port 6379 --appendonly yes 27 | volumes: 28 | - evolution_redis:/data 29 | ports: 30 | - 6379:6379 31 | 32 | postgres: 33 | container_name: postgres 34 | image: postgres:15 35 | networks: 36 | - evolution-net 37 | command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"] 38 | restart: always 39 | ports: 40 | - 5432:5432 41 | environment: 42 | - POSTGRES_USER=user 43 | - POSTGRES_PASSWORD=pass 44 | - POSTGRES_DB=evolution 45 | - POSTGRES_HOST_AUTH_METHOD=trust 46 | volumes: 47 | - postgres_data:/var/lib/postgresql/data 48 | expose: 49 | - 5432 50 | 51 | volumes: 52 | evolution_instances: 53 | evolution_redis: 54 | postgres_data: 55 | 56 | 57 | networks: 58 | evolution-net: 59 | name: evolution-net 60 | driver: bridge 61 | -------------------------------------------------------------------------------- /manager/dist/assets/images/evolution-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/manager/dist/assets/images/evolution-logo.png -------------------------------------------------------------------------------- /manager/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Evolution Manager 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /prisma/mysql-migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240712155950_adjusts_in_templates_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Template_instanceId_key"; 3 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240722173518_add_name_column_to_openai_creds/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "OpenaiCreds_instanceId_key"; 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/postgresql-migrations/20240725202651_add_webhook_url_template_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Template" ADD COLUMN "webhookUrl" VARCHAR(500); 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240725221646_modify_token_instance_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Instance_token_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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240814173033_add_ignore_jids_chatwoot/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chatwoot" ADD COLUMN "ignoreJids" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240817110155_add_trigger_type_advanced/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "TriggerType" ADD VALUE 'advanced'; 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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/20240824161333_add_type_on_integration_sessions/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "IntegrationSession" ADD COLUMN "type" VARCHAR(100); 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240825130616_change_to_evolution_bot/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `GenericBot` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `GenericSetting` table. If the table is not empty, all the data it contains will be lost. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "GenericBot" DROP CONSTRAINT "GenericBot_instanceId_fkey"; 10 | 11 | -- DropForeignKey 12 | ALTER TABLE "GenericSetting" DROP CONSTRAINT "GenericSetting_botIdFallback_fkey"; 13 | 14 | -- DropForeignKey 15 | ALTER TABLE "GenericSetting" DROP CONSTRAINT "GenericSetting_instanceId_fkey"; 16 | 17 | -- DropTable 18 | DROP TABLE "GenericBot"; 19 | 20 | -- DropTable 21 | DROP TABLE "GenericSetting"; 22 | 23 | -- CreateTable 24 | CREATE TABLE "EvolutionBot" ( 25 | "id" TEXT NOT NULL, 26 | "enabled" BOOLEAN NOT NULL DEFAULT true, 27 | "description" VARCHAR(255), 28 | "apiUrl" VARCHAR(255), 29 | "apiKey" VARCHAR(255), 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 | "triggerType" "TriggerType", 40 | "triggerOperator" "TriggerOperator", 41 | "triggerValue" TEXT, 42 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 43 | "updatedAt" TIMESTAMP NOT NULL, 44 | "instanceId" TEXT NOT NULL, 45 | 46 | CONSTRAINT "EvolutionBot_pkey" PRIMARY KEY ("id") 47 | ); 48 | 49 | -- CreateTable 50 | CREATE TABLE "EvolutionBotSetting" ( 51 | "id" TEXT NOT NULL, 52 | "expire" INTEGER DEFAULT 0, 53 | "keywordFinish" VARCHAR(100), 54 | "delayMessage" INTEGER, 55 | "unknownMessage" VARCHAR(100), 56 | "listeningFromMe" BOOLEAN DEFAULT false, 57 | "stopBotFromMe" BOOLEAN DEFAULT false, 58 | "keepOpen" BOOLEAN DEFAULT false, 59 | "debounceTime" INTEGER, 60 | "ignoreJids" JSONB, 61 | "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 62 | "updatedAt" TIMESTAMP NOT NULL, 63 | "botIdFallback" VARCHAR(100), 64 | "instanceId" TEXT NOT NULL, 65 | 66 | CONSTRAINT "EvolutionBotSetting_pkey" PRIMARY KEY ("id") 67 | ); 68 | 69 | -- CreateIndex 70 | CREATE UNIQUE INDEX "EvolutionBotSetting_instanceId_key" ON "EvolutionBotSetting"("instanceId"); 71 | 72 | -- AddForeignKey 73 | ALTER TABLE "EvolutionBot" ADD CONSTRAINT "EvolutionBot_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 74 | 75 | -- AddForeignKey 76 | ALTER TABLE "EvolutionBotSetting" ADD CONSTRAINT "EvolutionBotSetting_botIdFallback_fkey" FOREIGN KEY ("botIdFallback") REFERENCES "EvolutionBot"("id") ON DELETE SET NULL ON UPDATE CASCADE; 77 | 78 | -- AddForeignKey 79 | ALTER TABLE "EvolutionBotSetting" ADD CONSTRAINT "EvolutionBotSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; 80 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20240906202019_add_headers_on_webhook_config/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Webhook" ADD COLUMN "headers" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/postgresql-migrations/20241001180457_add_message_status/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Message" ADD COLUMN "status" INTEGER; 3 | -------------------------------------------------------------------------------- /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/20241007164026_add_unread_messages_on_chat_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Chat" ADD COLUMN "unreadMessages" INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | DO $$ 10 | BEGIN 11 | IF NOT EXISTS ( 12 | SELECT 1 13 | FROM information_schema.columns 14 | WHERE table_name = 'Setting' 15 | AND column_name = 'wavoipToken' 16 | ) THEN 17 | ALTER TABLE "Setting" ADD COLUMN "wavoipToken" VARCHAR(100); 18 | END IF; 19 | END $$; -------------------------------------------------------------------------------- /prisma/postgresql-migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /public/images/atendai-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/atendai-logo.png -------------------------------------------------------------------------------- /public/images/bmc_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/bmc_qr.png -------------------------------------------------------------------------------- /public/images/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/code.png -------------------------------------------------------------------------------- /public/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/cover.png -------------------------------------------------------------------------------- /public/images/evolution-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/evolution-logo.png -------------------------------------------------------------------------------- /public/images/evolution-pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/evolution-pro.png -------------------------------------------------------------------------------- /public/images/picpay-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/picpay-image.png -------------------------------------------------------------------------------- /public/images/picpay-qr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/picpay-qr.jpeg -------------------------------------------------------------------------------- /public/images/qrcode-pix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/qrcode-pix.png -------------------------------------------------------------------------------- /public/images/video-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/evolution-api/427c99499394d3894e4b065626323c45e91022e4/public/images/video-cover.png -------------------------------------------------------------------------------- /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 | let command = process.argv 15 | .slice(2) 16 | .join(' ') 17 | .replace(/DATABASE_PROVIDER/g, databaseProviderDefault); 18 | 19 | if (command.includes('rmdir') && existsSync('prisma\\migrations')) { 20 | try { 21 | execSync('rmdir /S /Q prisma\\migrations', { stdio: 'inherit' }); 22 | } catch (error) { 23 | console.error(`Error removing directory: prisma\\migrations`); 24 | process.exit(1); 25 | } 26 | } else if (command.includes('rmdir')) { 27 | console.warn(`Directory 'prisma\\migrations' does not exist, skipping removal.`); 28 | } 29 | 30 | try { 31 | execSync(command, { stdio: 'inherit' }); 32 | } catch (error) { 33 | console.error(`Error executing command: ${command}`); 34 | process.exit(1); 35 | } 36 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/api/controllers/group.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AcceptGroupInvite, 3 | CreateGroupDto, 4 | GetParticipant, 5 | GroupDescriptionDto, 6 | GroupInvite, 7 | GroupJid, 8 | GroupPictureDto, 9 | GroupSendInvite, 10 | GroupSubjectDto, 11 | GroupToggleEphemeralDto, 12 | GroupUpdateParticipantDto, 13 | GroupUpdateSettingDto, 14 | } from '@api/dto/group.dto'; 15 | import { InstanceDto } from '@api/dto/instance.dto'; 16 | import { WAMonitoringService } from '@api/services/monitor.service'; 17 | 18 | export class GroupController { 19 | constructor(private readonly waMonitor: WAMonitoringService) {} 20 | 21 | public async createGroup(instance: InstanceDto, create: CreateGroupDto) { 22 | return await this.waMonitor.waInstances[instance.instanceName].createGroup(create); 23 | } 24 | 25 | public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) { 26 | return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture(update); 27 | } 28 | 29 | public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) { 30 | return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject(update); 31 | } 32 | 33 | public async updateGroupDescription(instance: InstanceDto, update: GroupDescriptionDto) { 34 | return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription(update); 35 | } 36 | 37 | public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) { 38 | return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid); 39 | } 40 | 41 | public async fetchAllGroups(instance: InstanceDto, getPaticipants: GetParticipant) { 42 | return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups(getPaticipants); 43 | } 44 | 45 | public async inviteCode(instance: InstanceDto, groupJid: GroupJid) { 46 | return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid); 47 | } 48 | 49 | public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) { 50 | return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode); 51 | } 52 | 53 | public async sendInvite(instance: InstanceDto, data: GroupSendInvite) { 54 | return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data); 55 | } 56 | 57 | public async acceptInviteCode(instance: InstanceDto, inviteCode: AcceptGroupInvite) { 58 | return await this.waMonitor.waInstances[instance.instanceName].acceptInviteCode(inviteCode); 59 | } 60 | 61 | public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { 62 | return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid); 63 | } 64 | 65 | public async findParticipants(instance: InstanceDto, groupJid: GroupJid) { 66 | return await this.waMonitor.waInstances[instance.instanceName].findParticipants(groupJid); 67 | } 68 | 69 | public async updateGParticipate(instance: InstanceDto, update: GroupUpdateParticipantDto) { 70 | return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant(update); 71 | } 72 | 73 | public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) { 74 | return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update); 75 | } 76 | 77 | public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) { 78 | return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(update); 79 | } 80 | 81 | public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) { 82 | return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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/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 | return response?.data !== serverIp?.data; 57 | } catch (error) { 58 | if (axios.isAxiosError(error) && error.response?.data) { 59 | logger.error('testProxy error: ' + error.response.data); 60 | } else if (axios.isAxiosError(error)) { 61 | logger.error('testProxy error: '); 62 | } else { 63 | logger.error('testProxy error: '); 64 | } 65 | return false; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | ) {} 17 | } 18 | 19 | export class getBase64FromMediaMessageDto { 20 | message: proto.WebMessageInfo; 21 | convertToMp4?: boolean; 22 | } 23 | 24 | export class WhatsAppNumberDto { 25 | numbers: string[]; 26 | } 27 | 28 | export class NumberDto { 29 | number: string; 30 | } 31 | 32 | export class NumberBusiness { 33 | wid?: string; 34 | jid?: string; 35 | exists?: boolean; 36 | isBusiness: boolean; 37 | name?: string; 38 | message?: string; 39 | description?: string; 40 | email?: string; 41 | websites?: string[]; 42 | website?: string[]; 43 | address?: string; 44 | about?: string; 45 | vertical?: string; 46 | profilehandle?: string; 47 | } 48 | 49 | export class ProfileNameDto { 50 | name: string; 51 | } 52 | 53 | export class ProfileStatusDto { 54 | status: string; 55 | } 56 | 57 | export class ProfilePictureDto { 58 | number?: string; 59 | // url or base64 60 | picture?: string; 61 | } 62 | 63 | class Key { 64 | id: string; 65 | fromMe: boolean; 66 | remoteJid: string; 67 | } 68 | export class ReadMessageDto { 69 | readMessages: Key[]; 70 | } 71 | 72 | export class LastMessage { 73 | key: Key; 74 | messageTimestamp?: number; 75 | } 76 | 77 | export class ArchiveChatDto { 78 | lastMessage?: LastMessage; 79 | chat?: string; 80 | archive: boolean; 81 | } 82 | 83 | export class MarkChatUnreadDto { 84 | lastMessage?: LastMessage; 85 | chat?: string; 86 | } 87 | 88 | export class PrivacySettingDto { 89 | readreceipts: WAReadReceiptsValue; 90 | profile: WAPrivacyValue; 91 | status: WAPrivacyValue; 92 | online: WAPrivacyOnlineValue; 93 | last: WAPrivacyValue; 94 | groupadd: WAPrivacyGroupAddValue; 95 | } 96 | 97 | export class DeleteMessage { 98 | id: string; 99 | fromMe: boolean; 100 | remoteJid: string; 101 | participant?: string; 102 | } 103 | export class Options { 104 | delay?: number; 105 | presence?: WAPresence; 106 | } 107 | class OptionsMessage { 108 | options: Options; 109 | } 110 | export class Metadata extends OptionsMessage { 111 | number: string; 112 | } 113 | 114 | export class SendPresenceDto extends Metadata { 115 | presence: WAPresence; 116 | delay: number; 117 | } 118 | 119 | export class UpdateMessageDto extends Metadata { 120 | number: string; 121 | key: proto.IMessageKey; 122 | text: string; 123 | } 124 | 125 | export class BlockUserDto { 126 | number: string; 127 | status: 'block' | 'unblock'; 128 | } 129 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | profileName?: string; 16 | profilePicUrl?: string; 17 | // settings 18 | rejectCall?: boolean; 19 | msgCall?: string; 20 | groupsIgnore?: boolean; 21 | alwaysOnline?: boolean; 22 | readMessages?: boolean; 23 | readStatus?: boolean; 24 | syncFullHistory?: boolean; 25 | wavoipToken?: string; 26 | // proxy 27 | proxyHost?: string; 28 | proxyPort?: string; 29 | proxyProtocol?: string; 30 | proxyUsername?: string; 31 | proxyPassword?: string; 32 | webhook?: { 33 | enabled?: boolean; 34 | events?: string[]; 35 | headers?: JsonValue; 36 | url?: string; 37 | byEvents?: boolean; 38 | base64?: boolean; 39 | }; 40 | chatwootAccountId?: string; 41 | chatwootConversationPending?: boolean; 42 | chatwootAutoCreate?: boolean; 43 | chatwootDaysLimitImportMessages?: number; 44 | chatwootImportContacts?: boolean; 45 | chatwootImportMessages?: boolean; 46 | chatwootLogo?: string; 47 | chatwootMergeBrazilContacts?: boolean; 48 | chatwootNameInbox?: string; 49 | chatwootOrganization?: string; 50 | chatwootReopenConversation?: boolean; 51 | chatwootSignMsg?: boolean; 52 | chatwootToken?: string; 53 | chatwootUrl?: string; 54 | } 55 | 56 | export class SetPresenceDto { 57 | presence: WAPresence; 58 | } 59 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/channel/channel.controller.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { ProviderFiles } from '@api/provider/sessions'; 3 | import { PrismaRepository } from '@api/repository/repository.service'; 4 | import { CacheService } from '@api/services/cache.service'; 5 | import { WAMonitoringService } from '@api/services/monitor.service'; 6 | import { Integration } from '@api/types/wa.types'; 7 | import { ConfigService } from '@config/env.config'; 8 | import { BadRequestException } from '@exceptions'; 9 | import EventEmitter2 from 'eventemitter2'; 10 | 11 | import { EvolutionStartupService } from './evolution/evolution.channel.service'; 12 | import { BusinessStartupService } from './meta/whatsapp.business.service'; 13 | import { BaileysStartupService } from './whatsapp/whatsapp.baileys.service'; 14 | 15 | type ChannelDataType = { 16 | configService: ConfigService; 17 | eventEmitter: EventEmitter2; 18 | prismaRepository: PrismaRepository; 19 | cache: CacheService; 20 | chatwootCache: CacheService; 21 | baileysCache: CacheService; 22 | providerFiles: ProviderFiles; 23 | }; 24 | 25 | export interface ChannelControllerInterface { 26 | receiveWebhook(data: any): Promise; 27 | } 28 | 29 | export class ChannelController { 30 | public prismaRepository: PrismaRepository; 31 | public waMonitor: WAMonitoringService; 32 | 33 | constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { 34 | this.prisma = prismaRepository; 35 | this.monitor = waMonitor; 36 | } 37 | 38 | public set prisma(prisma: PrismaRepository) { 39 | this.prismaRepository = prisma; 40 | } 41 | 42 | public get prisma() { 43 | return this.prismaRepository; 44 | } 45 | 46 | public set monitor(waMonitor: WAMonitoringService) { 47 | this.waMonitor = waMonitor; 48 | } 49 | 50 | public get monitor() { 51 | return this.waMonitor; 52 | } 53 | 54 | public init(instanceData: InstanceDto, data: ChannelDataType) { 55 | if (!instanceData.token && instanceData.integration === Integration.WHATSAPP_BUSINESS) { 56 | throw new BadRequestException('token is required'); 57 | } 58 | 59 | if (instanceData.integration === Integration.WHATSAPP_BUSINESS) { 60 | return new BusinessStartupService( 61 | data.configService, 62 | data.eventEmitter, 63 | data.prismaRepository, 64 | data.cache, 65 | data.chatwootCache, 66 | data.baileysCache, 67 | data.providerFiles, 68 | ); 69 | } 70 | 71 | if (instanceData.integration === Integration.EVOLUTION) { 72 | return new EvolutionStartupService( 73 | data.configService, 74 | data.eventEmitter, 75 | data.prismaRepository, 76 | data.cache, 77 | data.chatwootCache, 78 | ); 79 | } 80 | 81 | if (instanceData.integration === Integration.WHATSAPP_BAILEYS) { 82 | return new BaileysStartupService( 83 | data.configService, 84 | data.eventEmitter, 85 | data.prismaRepository, 86 | data.cache, 87 | data.chatwootCache, 88 | data.baileysCache, 89 | data.providerFiles, 90 | ); 91 | } 92 | 93 | return null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router'; 8 | import { FlowiseRouter } from './flowise/routes/flowise.router'; 9 | 10 | export class ChatbotRouter { 11 | public readonly router: Router; 12 | 13 | constructor(...guards: any[]) { 14 | this.router = Router(); 15 | 16 | this.router.use('/evolutionBot', new EvolutionBotRouter(...guards).router); 17 | this.router.use('/chatwoot', new ChatwootRouter(...guards).router); 18 | this.router.use('/typebot', new TypebotRouter(...guards).router); 19 | this.router.use('/openai', new OpenaiRouter(...guards).router); 20 | this.router.use('/dify', new DifyRouter(...guards).router); 21 | this.router.use('/flowise', new FlowiseRouter(...guards).router); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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/evolutionBot/validate/evolutionBot.schema'; 4 | export * from '@api/integrations/chatbot/flowise/validate/flowise.schema'; 5 | export * from '@api/integrations/chatbot/openai/validate/openai.schema'; 6 | export * from '@api/integrations/chatbot/typebot/validate/typebot.schema'; 7 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto'; 3 | import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service'; 4 | import { PrismaRepository } from '@api/repository/repository.service'; 5 | import { waMonitor } from '@api/server.module'; 6 | import { CacheService } from '@api/services/cache.service'; 7 | import { CacheEngine } from '@cache/cacheengine'; 8 | import { Chatwoot, ConfigService, HttpServer } from '@config/env.config'; 9 | import { BadRequestException } from '@exceptions'; 10 | import { isURL } from 'class-validator'; 11 | 12 | export class ChatwootController { 13 | constructor( 14 | private readonly chatwootService: ChatwootService, 15 | private readonly configService: ConfigService, 16 | private readonly prismaRepository: PrismaRepository, 17 | ) {} 18 | 19 | public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { 20 | if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); 21 | 22 | if (data?.enabled) { 23 | if (!isURL(data.url, { require_tld: false })) { 24 | throw new BadRequestException('url is not valid'); 25 | } 26 | 27 | if (!data.accountId) { 28 | throw new BadRequestException('accountId is required'); 29 | } 30 | 31 | if (!data.token) { 32 | throw new BadRequestException('token is required'); 33 | } 34 | 35 | if (data.signMsg !== true && data.signMsg !== false) { 36 | throw new BadRequestException('signMsg is required'); 37 | } 38 | if (data.signMsg === false) data.signDelimiter = null; 39 | } 40 | 41 | if (!data.nameInbox || data.nameInbox === '') { 42 | data.nameInbox = instance.instanceName; 43 | } 44 | 45 | const result = await this.chatwootService.create(instance, data); 46 | 47 | const urlServer = this.configService.get('SERVER').URL; 48 | 49 | const response = { 50 | ...result, 51 | webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, 52 | }; 53 | 54 | return response; 55 | } 56 | 57 | public async findChatwoot(instance: InstanceDto): Promise { 58 | if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); 59 | 60 | const result = await this.chatwootService.find(instance); 61 | 62 | const urlServer = this.configService.get('SERVER').URL; 63 | 64 | if (Object.keys(result || {}).length === 0) { 65 | return { 66 | enabled: false, 67 | url: '', 68 | accountId: '', 69 | token: '', 70 | signMsg: false, 71 | nameInbox: '', 72 | webhook_url: '', 73 | }; 74 | } 75 | 76 | const response = { 77 | ...result, 78 | webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, 79 | }; 80 | 81 | return response; 82 | } 83 | 84 | public async receiveWebhook(instance: InstanceDto, data: any) { 85 | if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); 86 | 87 | const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine()); 88 | const chatwootService = new ChatwootService(waMonitor, this.configService, this.prismaRepository, chatwootCache); 89 | 90 | return chatwootService.receiveWebhook(instance, data); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/api/integrations/chatbot/dify/dto/dify.dto.ts: -------------------------------------------------------------------------------- 1 | import { $Enums, TriggerOperator, TriggerType } from '@prisma/client'; 2 | 3 | export class DifyDto { 4 | enabled?: boolean; 5 | description?: string; 6 | botType?: $Enums.DifyBotType; 7 | apiUrl?: string; 8 | apiKey?: string; 9 | expire?: number; 10 | keywordFinish?: string; 11 | delayMessage?: number; 12 | unknownMessage?: string; 13 | listeningFromMe?: boolean; 14 | stopBotFromMe?: boolean; 15 | keepOpen?: boolean; 16 | debounceTime?: number; 17 | triggerType?: TriggerType; 18 | triggerOperator?: TriggerOperator; 19 | triggerValue?: string; 20 | ignoreJids?: any; 21 | splitMessages?: boolean; 22 | timePerChar?: number; 23 | } 24 | 25 | export class DifySettingDto { 26 | expire?: number; 27 | keywordFinish?: string; 28 | delayMessage?: number; 29 | unknownMessage?: string; 30 | listeningFromMe?: boolean; 31 | stopBotFromMe?: boolean; 32 | keepOpen?: boolean; 33 | debounceTime?: number; 34 | difyIdFallback?: string; 35 | ignoreJids?: any; 36 | splitMessages?: boolean; 37 | timePerChar?: number; 38 | } 39 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/evolutionBot/dto/evolutionBot.dto.ts: -------------------------------------------------------------------------------- 1 | import { TriggerOperator, TriggerType } from '@prisma/client'; 2 | 3 | export class EvolutionBotDto { 4 | enabled?: boolean; 5 | description?: string; 6 | apiUrl?: string; 7 | apiKey?: string; 8 | expire?: number; 9 | keywordFinish?: string; 10 | delayMessage?: number; 11 | unknownMessage?: string; 12 | listeningFromMe?: boolean; 13 | stopBotFromMe?: boolean; 14 | keepOpen?: boolean; 15 | debounceTime?: number; 16 | triggerType?: TriggerType; 17 | triggerOperator?: TriggerOperator; 18 | triggerValue?: string; 19 | ignoreJids?: any; 20 | splitMessages?: boolean; 21 | timePerChar?: number; 22 | } 23 | 24 | export class EvolutionBotSettingDto { 25 | expire?: number; 26 | keywordFinish?: string; 27 | delayMessage?: number; 28 | unknownMessage?: string; 29 | listeningFromMe?: boolean; 30 | stopBotFromMe?: boolean; 31 | keepOpen?: boolean; 32 | debounceTime?: number; 33 | botIdFallback?: string; 34 | ignoreJids?: any; 35 | splitMessages?: boolean; 36 | timePerChar?: number; 37 | } 38 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/evolutionBot/validate/evolutionBot.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 evolutionBotSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | enabled: { type: 'boolean' }, 28 | description: { type: 'string' }, 29 | apiUrl: { type: 'string' }, 30 | apiKey: { type: 'string' }, 31 | triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] }, 32 | triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] }, 33 | triggerValue: { type: 'string' }, 34 | expire: { type: 'integer' }, 35 | keywordFinish: { type: 'string' }, 36 | delayMessage: { type: 'integer' }, 37 | unknownMessage: { type: 'string' }, 38 | listeningFromMe: { type: 'boolean' }, 39 | stopBotFromMe: { type: 'boolean' }, 40 | keepOpen: { type: 'boolean' }, 41 | debounceTime: { type: 'integer' }, 42 | ignoreJids: { type: 'array', items: { type: 'string' } }, 43 | splitMessages: { type: 'boolean' }, 44 | timePerChar: { type: 'integer' }, 45 | }, 46 | required: ['enabled', 'apiUrl', 'triggerType'], 47 | ...isNotEmpty('enabled', 'apiUrl', 'triggerType'), 48 | }; 49 | 50 | export const evolutionBotStatusSchema: JSONSchema7 = { 51 | $id: v4(), 52 | type: 'object', 53 | properties: { 54 | remoteJid: { type: 'string' }, 55 | status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] }, 56 | }, 57 | required: ['remoteJid', 'status'], 58 | ...isNotEmpty('remoteJid', 'status'), 59 | }; 60 | 61 | export const evolutionBotSettingSchema: JSONSchema7 = { 62 | $id: v4(), 63 | type: 'object', 64 | properties: { 65 | expire: { type: 'integer' }, 66 | keywordFinish: { type: 'string' }, 67 | delayMessage: { type: 'integer' }, 68 | unknownMessage: { type: 'string' }, 69 | listeningFromMe: { type: 'boolean' }, 70 | stopBotFromMe: { type: 'boolean' }, 71 | keepOpen: { type: 'boolean' }, 72 | debounceTime: { type: 'integer' }, 73 | ignoreJids: { type: 'array', items: { type: 'string' } }, 74 | botIdFallback: { type: 'string' }, 75 | splitMessages: { type: 'boolean' }, 76 | timePerChar: { type: 'integer' }, 77 | }, 78 | required: [ 79 | 'expire', 80 | 'keywordFinish', 81 | 'delayMessage', 82 | 'unknownMessage', 83 | 'listeningFromMe', 84 | 'stopBotFromMe', 85 | 'keepOpen', 86 | 'debounceTime', 87 | 'ignoreJids', 88 | 'splitMessages', 89 | 'timePerChar', 90 | ], 91 | ...isNotEmpty( 92 | 'expire', 93 | 'keywordFinish', 94 | 'delayMessage', 95 | 'unknownMessage', 96 | 'listeningFromMe', 97 | 'stopBotFromMe', 98 | 'keepOpen', 99 | 'debounceTime', 100 | 'ignoreJids', 101 | 'splitMessages', 102 | 'timePerChar', 103 | ), 104 | }; 105 | 106 | export const evolutionBotIgnoreJidSchema: JSONSchema7 = { 107 | $id: v4(), 108 | type: 'object', 109 | properties: { 110 | remoteJid: { type: 'string' }, 111 | action: { type: 'string', enum: ['add', 'remove'] }, 112 | }, 113 | required: ['remoteJid', 'action'], 114 | ...isNotEmpty('remoteJid', 'action'), 115 | }; 116 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/flowise/dto/flowise.dto.ts: -------------------------------------------------------------------------------- 1 | import { TriggerOperator, TriggerType } from '@prisma/client'; 2 | 3 | export class FlowiseDto { 4 | enabled?: boolean; 5 | description?: string; 6 | apiUrl?: string; 7 | apiKey?: string; 8 | expire?: number; 9 | keywordFinish?: string; 10 | delayMessage?: number; 11 | unknownMessage?: string; 12 | listeningFromMe?: boolean; 13 | stopBotFromMe?: boolean; 14 | keepOpen?: boolean; 15 | debounceTime?: number; 16 | triggerType?: TriggerType; 17 | triggerOperator?: TriggerOperator; 18 | triggerValue?: string; 19 | ignoreJids?: any; 20 | splitMessages?: boolean; 21 | timePerChar?: number; 22 | } 23 | 24 | export class FlowiseSettingDto { 25 | expire?: number; 26 | keywordFinish?: string; 27 | delayMessage?: number; 28 | unknownMessage?: string; 29 | listeningFromMe?: boolean; 30 | stopBotFromMe?: boolean; 31 | keepOpen?: boolean; 32 | debounceTime?: number; 33 | flowiseIdFallback?: string; 34 | ignoreJids?: any; 35 | splitMessages?: boolean; 36 | timePerChar?: number; 37 | } 38 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/flowise/validate/flowise.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 flowiseSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | enabled: { type: 'boolean' }, 28 | description: { type: 'string' }, 29 | apiUrl: { type: 'string' }, 30 | apiKey: { type: 'string' }, 31 | triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] }, 32 | triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] }, 33 | triggerValue: { type: 'string' }, 34 | expire: { type: 'integer' }, 35 | keywordFinish: { type: 'string' }, 36 | delayMessage: { type: 'integer' }, 37 | unknownMessage: { type: 'string' }, 38 | listeningFromMe: { type: 'boolean' }, 39 | stopBotFromMe: { type: 'boolean' }, 40 | keepOpen: { type: 'boolean' }, 41 | debounceTime: { type: 'integer' }, 42 | ignoreJids: { type: 'array', items: { type: 'string' } }, 43 | }, 44 | required: ['enabled', 'apiUrl', 'triggerType'], 45 | ...isNotEmpty('enabled', 'apiUrl', 'triggerType'), 46 | }; 47 | 48 | export const flowiseStatusSchema: JSONSchema7 = { 49 | $id: v4(), 50 | type: 'object', 51 | properties: { 52 | remoteJid: { type: 'string' }, 53 | status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] }, 54 | }, 55 | required: ['remoteJid', 'status'], 56 | ...isNotEmpty('remoteJid', 'status'), 57 | }; 58 | 59 | export const flowiseSettingSchema: JSONSchema7 = { 60 | $id: v4(), 61 | type: 'object', 62 | properties: { 63 | expire: { type: 'integer' }, 64 | keywordFinish: { type: 'string' }, 65 | delayMessage: { type: 'integer' }, 66 | unknownMessage: { type: 'string' }, 67 | listeningFromMe: { type: 'boolean' }, 68 | stopBotFromMe: { type: 'boolean' }, 69 | keepOpen: { type: 'boolean' }, 70 | debounceTime: { type: 'integer' }, 71 | ignoreJids: { type: 'array', items: { type: 'string' } }, 72 | botIdFallback: { type: 'string' }, 73 | }, 74 | required: [ 75 | 'expire', 76 | 'keywordFinish', 77 | 'delayMessage', 78 | 'unknownMessage', 79 | 'listeningFromMe', 80 | 'stopBotFromMe', 81 | 'keepOpen', 82 | 'debounceTime', 83 | 'ignoreJids', 84 | ], 85 | ...isNotEmpty( 86 | 'expire', 87 | 'keywordFinish', 88 | 'delayMessage', 89 | 'unknownMessage', 90 | 'listeningFromMe', 91 | 'stopBotFromMe', 92 | 'keepOpen', 93 | 'debounceTime', 94 | 'ignoreJids', 95 | ), 96 | }; 97 | 98 | export const flowiseIgnoreJidSchema: JSONSchema7 = { 99 | $id: v4(), 100 | type: 'object', 101 | properties: { 102 | remoteJid: { type: 'string' }, 103 | action: { type: 'string', enum: ['add', 'remove'] }, 104 | }, 105 | required: ['remoteJid', 'action'], 106 | ...isNotEmpty('remoteJid', 'action'), 107 | }; 108 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/openai/dto/openai.dto.ts: -------------------------------------------------------------------------------- 1 | import { TriggerOperator, TriggerType } from '@prisma/client'; 2 | 3 | export class OpenaiCredsDto { 4 | name: string; 5 | apiKey: string; 6 | } 7 | 8 | export class OpenaiDto { 9 | enabled?: boolean; 10 | description?: string; 11 | openaiCredsId: string; 12 | botType?: string; 13 | assistantId?: string; 14 | functionUrl?: string; 15 | model?: string; 16 | systemMessages?: string[]; 17 | assistantMessages?: string[]; 18 | userMessages?: string[]; 19 | maxTokens?: number; 20 | expire?: number; 21 | keywordFinish?: string; 22 | delayMessage?: number; 23 | unknownMessage?: string; 24 | listeningFromMe?: boolean; 25 | stopBotFromMe?: boolean; 26 | keepOpen?: boolean; 27 | debounceTime?: number; 28 | triggerType?: TriggerType; 29 | triggerOperator?: TriggerOperator; 30 | triggerValue?: string; 31 | ignoreJids?: any; 32 | splitMessages?: boolean; 33 | timePerChar?: number; 34 | } 35 | 36 | export class OpenaiSettingDto { 37 | openaiCredsId?: string; 38 | expire?: number; 39 | keywordFinish?: string; 40 | delayMessage?: number; 41 | unknownMessage?: string; 42 | listeningFromMe?: boolean; 43 | stopBotFromMe?: boolean; 44 | keepOpen?: boolean; 45 | debounceTime?: number; 46 | openaiIdFallback?: string; 47 | ignoreJids?: any; 48 | speechToText?: boolean; 49 | splitMessages?: boolean; 50 | timePerChar?: number; 51 | } 52 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/typebot/dto/typebot.dto.ts: -------------------------------------------------------------------------------- 1 | import { TriggerOperator, TriggerType } from '@prisma/client'; 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 { 11 | enabled?: boolean; 12 | description?: string; 13 | url: string; 14 | typebot?: string; 15 | expire?: number; 16 | keywordFinish?: string; 17 | delayMessage?: number; 18 | unknownMessage?: string; 19 | listeningFromMe?: boolean; 20 | stopBotFromMe?: boolean; 21 | keepOpen?: boolean; 22 | debounceTime?: number; 23 | triggerType?: TriggerType; 24 | triggerOperator?: TriggerOperator; 25 | triggerValue?: string; 26 | ignoreJids?: any; 27 | } 28 | 29 | export class TypebotSettingDto { 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 | typebotIdFallback?: string; 39 | ignoreJids?: any; 40 | } 41 | -------------------------------------------------------------------------------- /src/api/integrations/chatbot/typebot/validate/typebot.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 typebotSchema: JSONSchema7 = { 24 | $id: v4(), 25 | type: 'object', 26 | properties: { 27 | enabled: { type: 'boolean' }, 28 | description: { type: 'string' }, 29 | url: { type: 'string' }, 30 | typebot: { type: 'string' }, 31 | triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] }, 32 | triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] }, 33 | triggerValue: { type: 'string' }, 34 | expire: { type: 'integer' }, 35 | keywordFinish: { type: 'string' }, 36 | delayMessage: { type: 'integer' }, 37 | unknownMessage: { type: 'string' }, 38 | listeningFromMe: { type: 'boolean' }, 39 | stopBotFromMe: { type: 'boolean' }, 40 | ignoreJids: { type: 'array', items: { type: 'string' } }, 41 | }, 42 | required: ['enabled', 'url', 'typebot', 'triggerType'], 43 | ...isNotEmpty('enabled', 'url', 'typebot', 'triggerType'), 44 | }; 45 | 46 | export const typebotStatusSchema: JSONSchema7 = { 47 | $id: v4(), 48 | type: 'object', 49 | properties: { 50 | remoteJid: { type: 'string' }, 51 | status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] }, 52 | }, 53 | required: ['remoteJid', 'status'], 54 | ...isNotEmpty('remoteJid', 'status'), 55 | }; 56 | 57 | export const typebotStartSchema: JSONSchema7 = { 58 | $id: v4(), 59 | type: 'object', 60 | properties: { 61 | remoteJid: { type: 'string' }, 62 | url: { type: 'string' }, 63 | typebot: { type: 'string' }, 64 | }, 65 | required: ['remoteJid', 'url', 'typebot'], 66 | ...isNotEmpty('remoteJid', 'url', 'typebot'), 67 | }; 68 | 69 | export const typebotSettingSchema: JSONSchema7 = { 70 | $id: v4(), 71 | type: 'object', 72 | properties: { 73 | expire: { type: 'integer' }, 74 | keywordFinish: { type: 'string' }, 75 | delayMessage: { type: 'integer' }, 76 | unknownMessage: { type: 'string' }, 77 | listeningFromMe: { type: 'boolean' }, 78 | stopBotFromMe: { type: 'boolean' }, 79 | keepOpen: { type: 'boolean' }, 80 | debounceTime: { type: 'integer' }, 81 | typebotIdFallback: { type: 'string' }, 82 | ignoreJids: { type: 'array', items: { type: 'string' } }, 83 | }, 84 | required: ['expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'], 85 | ...isNotEmpty('expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'), 86 | }; 87 | 88 | export const typebotIgnoreJidSchema: JSONSchema7 = { 89 | $id: v4(), 90 | type: 'object', 91 | properties: { 92 | remoteJid: { type: 'string' }, 93 | action: { type: 'string', enum: ['add', 'remove'] }, 94 | }, 95 | required: ['remoteJid', 'action'], 96 | ...isNotEmpty('remoteJid', 'action'), 97 | }; 98 | -------------------------------------------------------------------------------- /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 | pusher?: { 30 | enabled?: boolean; 31 | appId?: string; 32 | key?: string; 33 | secret?: string; 34 | cluster?: string; 35 | useTLS?: boolean; 36 | events?: string[]; 37 | }; 38 | } 39 | 40 | export function EventInstanceMixin(Base: TBase) { 41 | return class extends Base { 42 | webhook?: { 43 | enabled?: boolean; 44 | events?: string[]; 45 | headers?: JsonValue; 46 | url?: string; 47 | byEvents?: boolean; 48 | base64?: boolean; 49 | }; 50 | 51 | websocket?: { 52 | enabled?: boolean; 53 | events?: string[]; 54 | }; 55 | 56 | sqs?: { 57 | enabled?: boolean; 58 | events?: string[]; 59 | }; 60 | 61 | rabbitmq?: { 62 | enabled?: boolean; 63 | events?: string[]; 64 | }; 65 | 66 | pusher?: { 67 | enabled?: boolean; 68 | appId?: string; 69 | key?: string; 70 | secret?: string; 71 | cluster?: string; 72 | useTLS?: boolean; 73 | events?: string[]; 74 | }; 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/api/integrations/event/event.router.ts: -------------------------------------------------------------------------------- 1 | import { PusherRouter } from '@api/integrations/event/pusher/pusher.router'; 2 | import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router'; 3 | import { SqsRouter } from '@api/integrations/event/sqs/sqs.router'; 4 | import { WebhookRouter } from '@api/integrations/event/webhook/webhook.router'; 5 | import { WebsocketRouter } from '@api/integrations/event/websocket/websocket.router'; 6 | import { Router } from 'express'; 7 | 8 | export class EventRouter { 9 | public readonly router: Router; 10 | 11 | constructor(configService: any, ...guards: any[]) { 12 | this.router = Router(); 13 | 14 | this.router.use('/webhook', new WebhookRouter(configService, ...guards).router); 15 | this.router.use('/websocket', new WebsocketRouter(...guards).router); 16 | this.router.use('/rabbitmq', new RabbitmqRouter(...guards).router); 17 | this.router.use('/pusher', new PusherRouter(...guards).router); 18 | this.router.use('/sqs', new SqsRouter(...guards).router); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 | sqs: { 20 | $ref: '#/$defs/event', 21 | }, 22 | }, 23 | $defs: { 24 | event: { 25 | type: 'object', 26 | properties: { 27 | enabled: { type: 'boolean', enum: [true, false] }, 28 | events: { 29 | type: 'array', 30 | minItems: 0, 31 | items: { 32 | type: 'string', 33 | enum: EventController.events, 34 | }, 35 | }, 36 | }, 37 | required: ['enabled'], 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/routes/template.router.ts: -------------------------------------------------------------------------------- 1 | import { RouterBroker } from '@api/abstract/abstract.router'; 2 | import { InstanceDto } from '@api/dto/instance.dto'; 3 | import { TemplateDto } from '@api/dto/template.dto'; 4 | import { templateController } from '@api/server.module'; 5 | import { ConfigService } from '@config/env.config'; 6 | import { instanceSchema, templateSchema } from '@validate/validate.schema'; 7 | import { RequestHandler, Router } from 'express'; 8 | 9 | import { HttpStatus } from './index.router'; 10 | 11 | export class TemplateRouter extends RouterBroker { 12 | constructor( 13 | readonly configService: ConfigService, 14 | ...guards: RequestHandler[] 15 | ) { 16 | super(); 17 | this.router 18 | .post(this.routerPath('create'), ...guards, async (req, res) => { 19 | const response = await this.dataValidate({ 20 | request: req, 21 | schema: templateSchema, 22 | ClassRef: TemplateDto, 23 | execute: (instance, data) => templateController.createTemplate(instance, data), 24 | }); 25 | 26 | res.status(HttpStatus.CREATED).json(response); 27 | }) 28 | .get(this.routerPath('find'), ...guards, async (req, res) => { 29 | const response = await this.dataValidate({ 30 | request: req, 31 | schema: instanceSchema, 32 | ClassRef: InstanceDto, 33 | execute: (instance) => templateController.findTemplate(instance), 34 | }); 35 | 36 | res.status(HttpStatus.OK).json(response); 37 | }); 38 | } 39 | 40 | public readonly router: Router = Router(); 41 | } 42 | -------------------------------------------------------------------------------- /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/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/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/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 (error) { 29 | return null; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 (error) { 28 | return null; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/api/services/template.service.ts: -------------------------------------------------------------------------------- 1 | import { InstanceDto } from '@api/dto/instance.dto'; 2 | import { TemplateDto } from '@api/dto/template.dto'; 3 | import { PrismaRepository } from '@api/repository/repository.service'; 4 | import { ConfigService, WaBusiness } from '@config/env.config'; 5 | import { Logger } from '@config/logger.config'; 6 | import axios from 'axios'; 7 | 8 | import { WAMonitoringService } from './monitor.service'; 9 | 10 | export class TemplateService { 11 | constructor( 12 | private readonly waMonitor: WAMonitoringService, 13 | public readonly prismaRepository: PrismaRepository, 14 | private readonly configService: ConfigService, 15 | ) {} 16 | 17 | private readonly logger = new Logger('TemplateService'); 18 | 19 | private businessId: string; 20 | private token: string; 21 | 22 | public async find(instance: InstanceDto) { 23 | const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance; 24 | 25 | if (!getInstance) { 26 | throw new Error('Instance not found'); 27 | } 28 | 29 | this.businessId = getInstance.businessId; 30 | this.token = getInstance.token; 31 | 32 | const response = await this.requestTemplate({}, 'GET'); 33 | 34 | if (!response) { 35 | throw new Error('Error to create template'); 36 | } 37 | 38 | return response.data; 39 | } 40 | 41 | public async create(instance: InstanceDto, data: TemplateDto) { 42 | try { 43 | const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance; 44 | 45 | if (!getInstance) { 46 | throw new Error('Instance not found'); 47 | } 48 | 49 | this.businessId = getInstance.businessId; 50 | this.token = getInstance.token; 51 | 52 | const postData = { 53 | name: data.name, 54 | category: data.category, 55 | allow_category_change: data.allowCategoryChange, 56 | language: data.language, 57 | components: data.components, 58 | }; 59 | 60 | const response = await this.requestTemplate(postData, 'POST'); 61 | 62 | if (!response || response.error) { 63 | throw new Error('Error to create template'); 64 | } 65 | 66 | const template = await this.prismaRepository.template.create({ 67 | data: { 68 | templateId: response.id, 69 | name: data.name, 70 | template: response, 71 | webhookUrl: data.webhookUrl, 72 | instanceId: getInstance.id, 73 | }, 74 | }); 75 | 76 | return template; 77 | } catch (error) { 78 | this.logger.error(error); 79 | throw new Error('Error to create template'); 80 | } 81 | } 82 | 83 | private async requestTemplate(data: any, method: string) { 84 | try { 85 | let urlServer = this.configService.get('WA_BUSINESS').URL; 86 | const version = this.configService.get('WA_BUSINESS').VERSION; 87 | urlServer = `${urlServer}/${version}/${this.businessId}/message_templates`; 88 | const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }; 89 | if (method === 'GET') { 90 | const result = await axios.get(urlServer, { headers }); 91 | return result.data; 92 | } else if (method === 'POST') { 93 | const result = await axios.post(urlServer, data, { headers }); 94 | return result.data; 95 | } 96 | } catch (e) { 97 | this.logger.error(e.response.data); 98 | return e.response.data.error; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /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/cache/localcache.ts: -------------------------------------------------------------------------------- 1 | import { ICache } from '@api/abstract/abstract.cache'; 2 | import { CacheConf, CacheConfLocal, ConfigService } from '@config/env.config'; 3 | import { Logger } from '@config/logger.config'; 4 | import { BufferJSON } from 'baileys'; 5 | import NodeCache from 'node-cache'; 6 | 7 | export class LocalCache implements ICache { 8 | private readonly logger = new Logger('LocalCache'); 9 | private conf: CacheConfLocal; 10 | static localCache = new NodeCache(); 11 | 12 | constructor( 13 | private readonly configService: ConfigService, 14 | private readonly module: string, 15 | ) { 16 | this.conf = this.configService.get('CACHE')?.LOCAL; 17 | } 18 | 19 | async get(key: string): Promise { 20 | return LocalCache.localCache.get(this.buildKey(key)); 21 | } 22 | 23 | async set(key: string, value: any, ttl?: number) { 24 | return LocalCache.localCache.set(this.buildKey(key), value, ttl || this.conf.TTL); 25 | } 26 | 27 | async has(key: string) { 28 | return LocalCache.localCache.has(this.buildKey(key)); 29 | } 30 | 31 | async delete(key: string) { 32 | return LocalCache.localCache.del(this.buildKey(key)); 33 | } 34 | 35 | async deleteAll(appendCriteria?: string) { 36 | const keys = await this.keys(appendCriteria); 37 | if (!keys?.length) { 38 | return 0; 39 | } 40 | 41 | return LocalCache.localCache.del(keys); 42 | } 43 | 44 | async keys(appendCriteria?: string) { 45 | const filter = `${this.buildKey('')}${appendCriteria ? `${appendCriteria}:` : ''}`; 46 | 47 | return LocalCache.localCache.keys().filter((key) => key.substring(0, filter.length) === filter); 48 | } 49 | 50 | buildKey(key: string) { 51 | return `${this.module}:${key}`; 52 | } 53 | 54 | async hGet(key: string, field: string) { 55 | try { 56 | const data = LocalCache.localCache.get(this.buildKey(key)) as Object; 57 | 58 | if (data && field in data) { 59 | return JSON.parse(data[field], BufferJSON.reviver); 60 | } 61 | 62 | return null; 63 | } catch (error) { 64 | this.logger.error(error); 65 | } 66 | } 67 | 68 | async hSet(key: string, field: string, value: any) { 69 | try { 70 | const json = JSON.stringify(value, BufferJSON.replacer); 71 | 72 | let hash = LocalCache.localCache.get(this.buildKey(key)); 73 | 74 | if (!hash) { 75 | hash = {}; 76 | } 77 | 78 | hash[field] = json; 79 | LocalCache.localCache.set(this.buildKey(key), hash); 80 | } catch (error) { 81 | this.logger.error(error); 82 | } 83 | } 84 | 85 | async hDelete(key: string, field: string) { 86 | try { 87 | const data = LocalCache.localCache.get(this.buildKey(key)) as Object; 88 | 89 | if (data && field in data) { 90 | delete data[field]; 91 | LocalCache.localCache.set(this.buildKey(key), data); 92 | return 1; 93 | } 94 | 95 | return 0; 96 | } catch (error) { 97 | this.logger.error(error); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /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/cache/rediscache.ts: -------------------------------------------------------------------------------- 1 | import { ICache } from '@api/abstract/abstract.cache'; 2 | import { CacheConf, CacheConfRedis, ConfigService } from '@config/env.config'; 3 | import { Logger } from '@config/logger.config'; 4 | import { BufferJSON } from 'baileys'; 5 | import { RedisClientType } from 'redis'; 6 | 7 | import { redisClient } from './rediscache.client'; 8 | 9 | export class RedisCache implements ICache { 10 | private readonly logger = new Logger('RedisCache'); 11 | private client: RedisClientType; 12 | private conf: CacheConfRedis; 13 | 14 | constructor( 15 | private readonly configService: ConfigService, 16 | private readonly module: string, 17 | ) { 18 | this.conf = this.configService.get('CACHE')?.REDIS; 19 | this.client = redisClient.getConnection(); 20 | } 21 | async get(key: string): Promise { 22 | try { 23 | return JSON.parse(await this.client.get(this.buildKey(key))); 24 | } catch (error) { 25 | this.logger.error(error); 26 | } 27 | } 28 | 29 | async hGet(key: string, field: string) { 30 | try { 31 | const data = await this.client.hGet(this.buildKey(key), field); 32 | 33 | if (data) { 34 | return JSON.parse(data, BufferJSON.reviver); 35 | } 36 | 37 | return null; 38 | } catch (error) { 39 | this.logger.error(error); 40 | } 41 | } 42 | 43 | async set(key: string, value: any, ttl?: number) { 44 | try { 45 | await this.client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value)); 46 | } catch (error) { 47 | this.logger.error(error); 48 | } 49 | } 50 | 51 | async hSet(key: string, field: string, value: any) { 52 | try { 53 | const json = JSON.stringify(value, BufferJSON.replacer); 54 | 55 | await this.client.hSet(this.buildKey(key), field, json); 56 | } catch (error) { 57 | this.logger.error(error); 58 | } 59 | } 60 | 61 | async has(key: string) { 62 | try { 63 | return (await this.client.exists(this.buildKey(key))) > 0; 64 | } catch (error) { 65 | this.logger.error(error); 66 | } 67 | } 68 | 69 | async delete(key: string) { 70 | try { 71 | return await this.client.del(this.buildKey(key)); 72 | } catch (error) { 73 | this.logger.error(error); 74 | } 75 | } 76 | 77 | async hDelete(key: string, field: string) { 78 | try { 79 | return await this.client.hDel(this.buildKey(key), field); 80 | } catch (error) { 81 | this.logger.error(error); 82 | } 83 | } 84 | 85 | async deleteAll(appendCriteria?: string) { 86 | try { 87 | const keys = await this.keys(appendCriteria); 88 | if (!keys?.length) { 89 | return 0; 90 | } 91 | 92 | return await this.client.del(keys); 93 | } catch (error) { 94 | this.logger.error(error); 95 | } 96 | } 97 | 98 | async keys(appendCriteria?: string) { 99 | try { 100 | const match = `${this.buildKey('')}${appendCriteria ? `${appendCriteria}:` : ''}*`; 101 | const keys = []; 102 | for await (const key of this.client.scanIterator({ 103 | MATCH: match, 104 | COUNT: 100, 105 | })) { 106 | keys.push(key); 107 | } 108 | 109 | return [...new Set(keys)]; 110 | } catch (error) { 111 | this.logger.error(error); 112 | } 113 | } 114 | 115 | buildKey(key: string) { 116 | return `${this.conf?.PREFIX_KEY}:${this.module}:${key}`; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/config/event.config.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter2 from 'eventemitter2'; 2 | 3 | const maxListeners = parseInt(process.env.EVENT_EMITTER_MAX_LISTENERS, 10) || 50; 4 | 5 | export const eventEmitter = new EventEmitter2({ 6 | delimiter: '.', 7 | newListener: false, 8 | ignoreErrors: false, 9 | maxListeners: maxListeners, 10 | }); 11 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/utils/findBotByTrigger.ts: -------------------------------------------------------------------------------- 1 | import { advancedOperatorsSearch } from './advancedOperatorsSearch'; 2 | 3 | export const findBotByTrigger = async (botRepository: any, content: string, instanceId: string) => { 4 | // Check for triggerType 'all' 5 | const findTriggerAll = await botRepository.findFirst({ 6 | where: { 7 | enabled: true, 8 | triggerType: 'all', 9 | instanceId: instanceId, 10 | }, 11 | }); 12 | 13 | if (findTriggerAll) return findTriggerAll; 14 | 15 | const findTriggerAdvanced = await botRepository.findMany({ 16 | where: { 17 | enabled: true, 18 | triggerType: 'advanced', 19 | instanceId: instanceId, 20 | }, 21 | }); 22 | for (const advanced of findTriggerAdvanced) { 23 | if (advancedOperatorsSearch(content, advanced.triggerValue)) { 24 | return advanced; 25 | } 26 | } 27 | 28 | // Check for exact match 29 | const findTriggerEquals = await botRepository.findFirst({ 30 | where: { 31 | enabled: true, 32 | triggerType: 'keyword', 33 | triggerOperator: 'equals', 34 | triggerValue: content, 35 | instanceId: instanceId, 36 | }, 37 | }); 38 | 39 | if (findTriggerEquals) return findTriggerEquals; 40 | 41 | // Check for regex match 42 | const findRegex = await botRepository.findMany({ 43 | where: { 44 | enabled: true, 45 | triggerType: 'keyword', 46 | triggerOperator: 'regex', 47 | instanceId: instanceId, 48 | }, 49 | }); 50 | 51 | let findTriggerRegex = null; 52 | 53 | for (const regex of findRegex) { 54 | const regexValue = new RegExp(regex.triggerValue); 55 | 56 | if (regexValue.test(content)) { 57 | findTriggerRegex = regex; 58 | break; 59 | } 60 | } 61 | 62 | if (findTriggerRegex) return findTriggerRegex; 63 | 64 | // Check for startsWith match 65 | const findStartsWith = await botRepository.findMany({ 66 | where: { 67 | enabled: true, 68 | triggerType: 'keyword', 69 | triggerOperator: 'startsWith', 70 | instanceId: instanceId, 71 | }, 72 | }); 73 | 74 | let findTriggerStartsWith = null; 75 | 76 | for (const startsWith of findStartsWith) { 77 | if (content.startsWith(startsWith.triggerValue)) { 78 | findTriggerStartsWith = startsWith; 79 | break; 80 | } 81 | } 82 | 83 | if (findTriggerStartsWith) return findTriggerStartsWith; 84 | 85 | // Check for endsWith match 86 | const findEndsWith = await botRepository.findMany({ 87 | where: { 88 | enabled: true, 89 | triggerType: 'keyword', 90 | triggerOperator: 'endsWith', 91 | instanceId: instanceId, 92 | }, 93 | }); 94 | 95 | let findTriggerEndsWith = null; 96 | 97 | for (const endsWith of findEndsWith) { 98 | if (content.endsWith(endsWith.triggerValue)) { 99 | findTriggerEndsWith = endsWith; 100 | break; 101 | } 102 | } 103 | 104 | if (findTriggerEndsWith) return findTriggerEndsWith; 105 | 106 | // Check for contains match 107 | const findContains = await botRepository.findMany({ 108 | where: { 109 | enabled: true, 110 | triggerType: 'keyword', 111 | triggerOperator: 'contains', 112 | instanceId: instanceId, 113 | }, 114 | }); 115 | 116 | let findTriggerContains = null; 117 | 118 | for (const contains of findContains) { 119 | if (content.includes(contains.triggerValue)) { 120 | findTriggerContains = contains; 121 | break; 122 | } 123 | } 124 | 125 | if (findTriggerContains) return findTriggerContains; 126 | 127 | return null; 128 | }; 129 | -------------------------------------------------------------------------------- /src/utils/getConversationMessage.ts: -------------------------------------------------------------------------------- 1 | import { configService, S3 } from '@config/env.config'; 2 | 3 | const getTypeMessage = (msg: any) => { 4 | let mediaId: string; 5 | 6 | if (configService.get('S3').ENABLE) mediaId = msg.message.mediaUrl; 7 | else mediaId = msg.key.id; 8 | 9 | const types = { 10 | conversation: msg?.message?.conversation, 11 | extendedTextMessage: msg?.message?.extendedTextMessage?.text, 12 | contactMessage: msg?.message?.contactMessage?.displayName, 13 | locationMessage: msg?.message?.locationMessage?.degreesLatitude, 14 | viewOnceMessageV2: 15 | msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || 16 | msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || 17 | msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, 18 | listResponseMessage: msg?.message?.listResponseMessage?.title, 19 | responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId, 20 | templateButtonReplyMessage: 21 | msg?.message?.templateButtonReplyMessage?.selectedId || msg?.message?.buttonsResponseMessage?.selectedButtonId, 22 | // Medias 23 | audioMessage: msg?.message?.speechToText 24 | ? msg?.message?.speechToText 25 | : msg?.message?.audioMessage 26 | ? `audioMessage|${mediaId}` 27 | : undefined, 28 | imageMessage: msg?.message?.imageMessage 29 | ? `imageMessage|${mediaId}${msg?.message?.imageMessage?.caption ? `|${msg?.message?.imageMessage?.caption}` : ''}` 30 | : undefined, 31 | videoMessage: msg?.message?.videoMessage 32 | ? `videoMessage|${mediaId}${msg?.message?.videoMessage?.caption ? `|${msg?.message?.videoMessage?.caption}` : ''}` 33 | : undefined, 34 | documentMessage: msg?.message?.documentMessage 35 | ? `documentMessage|${mediaId}${ 36 | msg?.message?.documentMessage?.caption ? `|${msg?.message?.documentMessage?.caption}` : '' 37 | }` 38 | : undefined, 39 | documentWithCaptionMessage: msg?.message?.documentWithCaptionMessage?.message?.documentMessage 40 | ? `documentWithCaptionMessage|${mediaId}${ 41 | msg?.message?.documentWithCaptionMessage?.message?.documentMessage?.caption 42 | ? `|${msg?.message?.documentWithCaptionMessage?.message?.documentMessage?.caption}` 43 | : '' 44 | }` 45 | : undefined, 46 | externalAdReplyBody: msg?.contextInfo?.externalAdReply?.body 47 | ? `externalAdReplyBody|${msg.contextInfo.externalAdReply.body}` 48 | : undefined, 49 | }; 50 | 51 | const messageType = Object.keys(types).find((key) => types[key] !== undefined) || 'unknown'; 52 | 53 | return { ...types, messageType }; 54 | }; 55 | 56 | const getMessageContent = (types: any) => { 57 | const typeKey = Object.keys(types).find((key) => key !== 'externalAdReplyBody' && types[key] !== undefined); 58 | 59 | let result = typeKey ? types[typeKey] : undefined; 60 | 61 | if (types.externalAdReplyBody) { 62 | result = result ? `${result}\n${types.externalAdReplyBody}` : types.externalAdReplyBody; 63 | } 64 | 65 | return result; 66 | }; 67 | 68 | export const getConversationMessage = (msg: any) => { 69 | const types = getTypeMessage(msg); 70 | 71 | const messageContent = getMessageContent(types); 72 | 73 | return messageContent; 74 | }; 75 | -------------------------------------------------------------------------------- /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 | resources[language] = { 16 | translation: require(languagePath), 17 | }; 18 | } 19 | }); 20 | 21 | i18next.init({ 22 | resources, 23 | fallbackLng: 'en', 24 | lng: configService.get('LANGUAGE'), 25 | debug: false, 26 | 27 | interpolation: { 28 | escapeValue: false, 29 | }, 30 | }); 31 | export default i18next; 32 | -------------------------------------------------------------------------------- /src/utils/instrumentSentry.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node'; 2 | 3 | const dsn = process.env.SENTRY_DSN; 4 | 5 | if (dsn) { 6 | Sentry.init({ 7 | dsn: dsn, 8 | environment: process.env.NODE_ENV || 'development', 9 | tracesSampleRate: 1.0, 10 | profilesSampleRate: 1.0, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/makeProxyAgent.ts: -------------------------------------------------------------------------------- 1 | import { HttpsProxyAgent } from 'https-proxy-agent'; 2 | 3 | type Proxy = { 4 | host: string; 5 | password?: string; 6 | port: string; 7 | protocol: string; 8 | username?: string; 9 | }; 10 | 11 | export function makeProxyAgent(proxy: Proxy | string) { 12 | if (typeof proxy === 'string') { 13 | return new HttpsProxyAgent(proxy); 14 | } 15 | 16 | const { host, password, port, protocol, username } = proxy; 17 | let proxyUrl = `${protocol}://${host}:${port}`; 18 | 19 | if (username && password) { 20 | proxyUrl = `${protocol}://${username}:${password}@${host}:${port}`; 21 | } 22 | return new HttpsProxyAgent(proxyUrl); 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/onWhatsappCache.ts: -------------------------------------------------------------------------------- 1 | import { prismaRepository } from '@api/server.module'; 2 | import { configService, Database } from '@config/env.config'; 3 | import dayjs from 'dayjs'; 4 | 5 | function getAvailableNumbers(remoteJid: string) { 6 | const numbersAvailable: string[] = []; 7 | 8 | if (remoteJid.startsWith('+')) { 9 | remoteJid = remoteJid.slice(1); 10 | } 11 | 12 | const [number, domain] = remoteJid.split('@'); 13 | 14 | // Brazilian numbers 15 | if (remoteJid.startsWith('55')) { 16 | const numberWithDigit = 17 | number.slice(4, 5) === '9' && number.length === 13 ? number : `${number.slice(0, 4)}9${number.slice(4)}`; 18 | const numberWithoutDigit = number.length === 12 ? number : number.slice(0, 4) + number.slice(5); 19 | 20 | numbersAvailable.push(numberWithDigit); 21 | numbersAvailable.push(numberWithoutDigit); 22 | } 23 | 24 | // Mexican/Argentina numbers 25 | // Ref: https://faq.whatsapp.com/1294841057948784 26 | else if (number.startsWith('52') || number.startsWith('54')) { 27 | let prefix = ''; 28 | if (number.startsWith('52')) { 29 | prefix = '1'; 30 | } 31 | if (number.startsWith('54')) { 32 | prefix = '9'; 33 | } 34 | 35 | const numberWithDigit = 36 | number.slice(2, 3) === prefix && number.length === 13 37 | ? number 38 | : `${number.slice(0, 2)}${prefix}${number.slice(2)}`; 39 | const numberWithoutDigit = number.length === 12 ? number : number.slice(0, 2) + number.slice(3); 40 | 41 | numbersAvailable.push(numberWithDigit); 42 | numbersAvailable.push(numberWithoutDigit); 43 | } 44 | 45 | // Other countries 46 | else { 47 | numbersAvailable.push(remoteJid); 48 | } 49 | 50 | return numbersAvailable.map((number) => `${number}@${domain}`); 51 | } 52 | 53 | interface ISaveOnWhatsappCacheParams { 54 | remoteJid: string; 55 | } 56 | export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { 57 | if (configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) { 58 | const upsertsQuery = data.map((item) => { 59 | const remoteJid = item.remoteJid.startsWith('+') ? item.remoteJid.slice(1) : item.remoteJid; 60 | const numbersAvailable = getAvailableNumbers(remoteJid); 61 | 62 | return prismaRepository.isOnWhatsapp.upsert({ 63 | create: { remoteJid: remoteJid, jidOptions: numbersAvailable.join(',') }, 64 | update: { jidOptions: numbersAvailable.join(',') }, 65 | where: { remoteJid: remoteJid }, 66 | }); 67 | }); 68 | 69 | await prismaRepository.$transaction(upsertsQuery); 70 | } 71 | } 72 | 73 | export async function getOnWhatsappCache(remoteJids: string[]) { 74 | let results: { 75 | remoteJid: string; 76 | number: string; 77 | jidOptions: string[]; 78 | }[] = []; 79 | 80 | if (configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) { 81 | const remoteJidsWithoutPlus = remoteJids.map((remoteJid) => getAvailableNumbers(remoteJid)).flat(); 82 | 83 | const onWhatsappCache = await prismaRepository.isOnWhatsapp.findMany({ 84 | where: { 85 | OR: remoteJidsWithoutPlus.map((remoteJid) => ({ jidOptions: { contains: remoteJid } })), 86 | updatedAt: { 87 | gte: dayjs().subtract(configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP_DAYS, 'days').toDate(), 88 | }, 89 | }, 90 | }); 91 | 92 | results = onWhatsappCache.map((item) => ({ 93 | remoteJid: item.remoteJid, 94 | number: item.remoteJid.split('@')[0], 95 | jidOptions: item.jidOptions.split(','), 96 | })); 97 | } 98 | 99 | return results; 100 | } 101 | -------------------------------------------------------------------------------- /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/utils/sendTelemetry.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import fs from 'fs'; 3 | 4 | const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 5 | 6 | export interface TelemetryData { 7 | route: string; 8 | apiVersion: string; 9 | timestamp: Date; 10 | } 11 | 12 | export const sendTelemetry = async (route: string): Promise => { 13 | const enabled = process.env.TELEMETRY_ENABLED === undefined || process.env.TELEMETRY_ENABLED === 'true'; 14 | 15 | if (!enabled) { 16 | return; 17 | } 18 | 19 | if (route === '/') { 20 | return; 21 | } 22 | 23 | const telemetry: TelemetryData = { 24 | route, 25 | apiVersion: `${packageJson.version}`, 26 | timestamp: new Date(), 27 | }; 28 | 29 | const url = 30 | process.env.TELEMETRY_URL && process.env.TELEMETRY_URL !== '' 31 | ? process.env.TELEMETRY_URL 32 | : 'https://log.evolution-api.com/telemetry'; 33 | 34 | axios 35 | .post(url, telemetry) 36 | .then(() => {}) 37 | .catch(() => {}); 38 | }; 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | }> { 12 | const logger = new Logger('useMultiFileAuthStateRedisDb'); 13 | 14 | const writeData = async (data: any, key: string): Promise => { 15 | try { 16 | return await cache.hSet(instanceName, key, data); 17 | } catch (error) { 18 | return logger.error({ localError: 'writeData', error }); 19 | } 20 | }; 21 | 22 | const readData = async (key: string): Promise => { 23 | try { 24 | return await cache.hGet(instanceName, key); 25 | } catch (error) { 26 | logger.error({ localError: 'readData', error }); 27 | return; 28 | } 29 | }; 30 | 31 | const removeData = async (key: string) => { 32 | try { 33 | return await cache.hDelete(instanceName, key); 34 | } catch (error) { 35 | logger.error({ readData: 'removeData', error }); 36 | } 37 | }; 38 | 39 | const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); 40 | 41 | return { 42 | state: { 43 | creds, 44 | keys: { 45 | get: async (type, ids: string[]) => { 46 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 47 | // @ts-ignore 48 | const data: { [_: string]: SignalDataTypeMap[type] } = {}; 49 | await Promise.all( 50 | ids.map(async (id) => { 51 | let value = await readData(`${type}-${id}`); 52 | if (type === 'app-state-sync-key' && value) { 53 | value = proto.Message.AppStateSyncKeyData.fromObject(value); 54 | } 55 | 56 | data[id] = value; 57 | }), 58 | ); 59 | 60 | return data; 61 | }, 62 | set: async (data: any) => { 63 | const tasks: Promise[] = []; 64 | for (const category in data) { 65 | for (const id in data[category]) { 66 | const value = data[category][id]; 67 | const key = `${category}-${id}`; 68 | tasks.push(value ? await writeData(value, key) : await removeData(key)); 69 | } 70 | } 71 | 72 | await Promise.all(tasks); 73 | }, 74 | }, 75 | }, 76 | saveCreds: async () => { 77 | return await writeData(creds, 'creds'); 78 | }, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /src/validate/validate.schema.ts: -------------------------------------------------------------------------------- 1 | // Integrations Schema 2 | export * from './chat.schema'; 3 | export * from './group.schema'; 4 | export * from './instance.schema'; 5 | export * from './label.schema'; 6 | export * from './message.schema'; 7 | export * from './proxy.schema'; 8 | export * from './settings.schema'; 9 | export * from './template.schema'; 10 | export * from '@api/integrations/chatbot/chatbot.schema'; 11 | export * from '@api/integrations/event/event.schema'; 12 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | }, 19 | }); 20 | --------------------------------------------------------------------------------