diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml index d48b82a5821e3..75b32ea920c59 100644 --- a/.github/workflows/publish_foss_docker.yml +++ b/.github/workflows/publish_foss_docker.yml @@ -9,7 +9,7 @@ on: push: branches: - develop - - master + - uno tags: - v* workflow_dispatch: @@ -27,25 +27,27 @@ jobs: uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - - name: Strip enterprise code - run: | - rm -rf enterprise - rm -rf spec/enterprise + # - name: Strip enterprise code + # run: | + # rm -rf enterprise + # rm -rf spec/enterprise - - name: Set Chatwoot edition - run: | - echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile + # - name: Set Chatwoot edition + # run: | + # echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile - name: set docker tag run: | - echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV + echo "DOCKER_TAG=clairton/chatwoot:$GIT_REF" >> $GITHUB_ENV + echo "DOCKER_ARM_TAG=clairton/chatwoot-arm:$GIT_REF" >> $GITHUB_ENV - name: replace docker tag if master if: github.ref_name == 'master' run: | - echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV + echo "DOCKER_TAG=clairton/chatwoot:latest" >> $GITHUB_ENV + echo "DOCKER_ARM_TAG=clairton/chatwoot-arm:latest" >> $GITHUB_ENV - name: Login to DockerHub uses: docker/login-action@v1 @@ -53,11 +55,25 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + # https://github.com/docker/buildx/discussions/1382 cache build - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile platforms: linux/amd64 push: true tags: ${{ env.DOCKER_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push arm64 + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + platforms: linux/arm64 + push: true + tags: ${{ env.DOCKER_ARM_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index b0b2372aed6bc..13ee4f61667ac 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -10,6 +10,7 @@ on: branches: - develop - master + - uno pull_request: workflow_dispatch: diff --git a/Gemfile b/Gemfile index ec57da19302f5..a22791edcd748 100644 --- a/Gemfile +++ b/Gemfile @@ -155,6 +155,8 @@ gem 'stripe' ## to populate db with sample data gem 'faker' +gem 'phonelib' + # Include logrange conditionally in intializer using env variable gem 'lograge', '~> 0.14.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 70bee91706823..ba727d7fa231b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -543,7 +543,10 @@ GEM pg_search (2.3.6) activerecord (>= 5.2) activesupport (>= 5.2) - pgvector (0.1.1) + pgvector (0.2.2) + phonelib (0.7.1) + coderay (~> 1.1) + method_source (~> 1.0) procore-sift (1.0.0) activerecord (>= 6.1) pry (0.14.2) @@ -916,6 +919,7 @@ DEPENDENCIES pg pg_search pgvector + phonelib procore-sift pry-rails puma diff --git a/Procfile b/Procfile index f281f018a1091..c6688d341df12 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare && echo $SOURCE_VERSION > .git_sha -web: bundle exec rails ip_lookup:setup && bin/rails server -p $PORT -e $RAILS_ENV -worker: bundle exec rails ip_lookup:setup && bundle exec sidekiq -C config/sidekiq.yml +web: bundle exec rails server -p $PORT -e $RAILS_ENV +worker: bundle exec sidekiq -C config/sidekiq.yml diff --git a/README.md b/README.md index 152660fb792a3..b5685a95f7666 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,24 @@ ___ -# Chatwoot +# Chatwoot Uno + - essa versão do chatwoot que esta sendo usada aqui tem algumas customizações que ainda não foram aceitas pelo time do chatwoot e para um melhor uso com a unoapi(http://github.com/clairton/unoapi-cloud): + - funciona as conversas em grupo + - trata a mensagem enviadas por outras conexões, inclusive o aplicativo + - desabilita a janela de 24 horas do whatsapp cloud oficial + - sincroniza as imagens de perfil dos grupos e usuarios + - possibilidade de editar o endereço da caixa de entrada do whatsapp, assim pode usar a oficial e a unoapi na mesma instalação(não usar a env WHATSAPP_CLOUD_BASE_URL) + - opção no superadmin de habilitar para colocar o nome do agente na mensagem + - opção no superadmin de habilitar para marcar as mensagem no whatsapp como lido quando o agente visualiza a conversa + - opção no superadmin de esconder para a aba de todas as conversas + - opção no superadmin de esconder para o filtro de conversas + - opção no superadmin de esconder a parte de contatos + - da opção de alterar logo e nome da empresa + Exemplo de stack com os dois projetos integrados: https://github.com/clairton/unoapi-cloud/tree/main/examples/unochat + + + +# Chatwoot Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.

@@ -98,7 +115,7 @@ Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app. ### Other deployment options -For other supported options, checkout our [deployment page](https://chatwoot.com/deploy). +For other supported options, checkout our [deployment page](https://chatwoot.com/deploy). ## Security diff --git a/app/builders/contact_inbox_builder.rb b/app/builders/contact_inbox_builder.rb index 8fcd2b15818e0..94c0e936aa459 100644 --- a/app/builders/contact_inbox_builder.rb +++ b/app/builders/contact_inbox_builder.rb @@ -23,11 +23,21 @@ def generate_source_id phone_source_id when 'Channel::Api', 'Channel::WebWidget' SecureRandom.uuid + when 'Channel::NotificaMe' + notifica_me_source_id else raise "Unsupported operation for this channel: #{@inbox.channel_type}" end end + def notifica_me_source_id + if ['telegram', 'whatsapp', 'sms'].include?(@inbox.channel.notifica_me_type) + return @contact.phone_number + end + raise ActionController::ParameterMissing, 'contact email' unless @contact.source_id + return @contact.source_id + end + def email_source_id raise ActionController::ParameterMissing, 'contact email' unless @contact.email diff --git a/app/builders/contact_inbox_with_contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb index 2f30093dbf8a7..c4899498bb973 100644 --- a/app/builders/contact_inbox_with_contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -15,12 +15,16 @@ def perform def find_or_create_contact_and_contact_inbox @contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present? - return @contact_inbox if @contact_inbox + if @contact_inbox + update_contact_avatar(@contact_inbox.contact) unless @contact_inbox.contact.avatar.attached? + return @contact_inbox + end ActiveRecord::Base.transaction(requires_new: true) do build_contact_with_contact_inbox + update_contact_avatar(@contact) unless @contact.avatar.attached? end - update_contact_avatar(@contact) unless @contact.avatar.attached? + @contact_inbox end diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index e1087b19f8936..bcf5104a8c911 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -105,7 +105,7 @@ def validate_email_addresses(all_emails) end def message_type - if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming' + if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming' && !@private raise StandardError, 'Incoming messages are only allowed in Api inboxes' end @@ -138,6 +138,15 @@ def message_sender AgentBot.where(account_id: [nil, @conversation.account.id]).find_by(id: @params[:sender_id]) end + def status_param + @params[:status] = :progress if params_status_progress? + @params[:status].present? ? { status: @params[:status] } : {} + end + + def source_id_param + @params[:source_id].present? ? { source_id: @params[:source_id] } : {} + end + def message_params { account_id: @conversation.account_id, @@ -150,7 +159,15 @@ def message_params items: @items, in_reply_to: @in_reply_to, echo_id: @params[:echo_id], - source_id: @params[:source_id] - }.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params) + }.merge(external_created_at) + .merge(automation_rule_id) + .merge(campaign_id) + .merge(template_params) + .merge(status_param) + .merge(source_id_param) + end + + def params_status_progress? + @params[:status].blank? && @message_type == 'outgoing' && !@private && @params[:action] == 'create' && (@conversation.inbox&.whatsapp? || @conversation.inbox&.notifica_me?) end end diff --git a/app/controllers/api/v1/accounts/campaigns_controller.rb b/app/controllers/api/v1/accounts/campaigns_controller.rb index b1a13224615ed..6509ed606cf1e 100644 --- a/app/controllers/api/v1/accounts/campaigns_controller.rb +++ b/app/controllers/api/v1/accounts/campaigns_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController before_action :check_authorization def index - @campaigns = Current.account.campaigns + @campaigns = Current.account.campaigns.limit(200).order(created_at: :desc) end def show; end @@ -28,7 +28,13 @@ def campaign end def campaign_params - params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id, - :scheduled_at, audience: [:type, :id], trigger_rules: {}) + fields = [:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id, :scheduled_at] + fields << { trigger_rules: {} } + fields << if Inbox.find(params[:inbox_id])&.channel.try(:provider) == 'unoapi' + { audience: [:name, :phone_number, :identifier, :due_at, :value, :scheduled_at, :email, :wait_for_seconds] } + else + { audience: [:type, :id] } + end + params.require(:campaign).permit(fields) end end diff --git a/app/controllers/api/v1/accounts/channels/notifica_me_channels_controller.rb b/app/controllers/api/v1/accounts/channels/notifica_me_channels_controller.rb new file mode 100644 index 0000000000000..31927263a61f0 --- /dev/null +++ b/app/controllers/api/v1/accounts/channels/notifica_me_channels_controller.rb @@ -0,0 +1,63 @@ + +class Api::V1::Accounts::Channels::NotificaMeChannelsController < Api::V1::Accounts::BaseController + before_action :authorize_request + + + def index + unless request.query_parameters["token"] + return render :json => { error: "Put the NotificaMe Token" }, status: 422 + end + url = URI("https://hub.notificame.com.br/v1/channels") + response = HTTParty.get( + url, + headers: { + 'X-API-Token' => request.query_parameters["token"], + 'Content-Type' => 'application/json' + }, + format: :json + ) + if response.success? + render json: { data: { channels: response.parsed_response }}, status: 200 + else + render json: { error: response.parsed_response }, status: 422 + end + end + + def create + ActiveRecord::Base.transaction do + build_inbox + setup_webhooks + render json: @inbox, status: 200 + rescue StandardError => e + Rails.logger.error("NotificaMe channel create error #{e}, #{e.backtrace}") + render_could_not_create_error(e.message) + raise ActiveRecord::Rollback + end + end + + private + + def authorize_request + authorize ::Inbox + end + + def setup_webhooks + ::NotificaMe::WebhookSetupService.new(inbox: @inbox).perform + end + + def build_inbox + @notifica_me_channel = Current.account.notifica_me_channels.create!( + notifica_me_id: permitted_params[:notifica_me_id], + notifica_me_token: permitted_params[:notifica_me_token], + notifica_me_type: permitted_params[:notifica_me_type], + ) + @inbox = Current.account.inboxes.create!( + name: permitted_params[:name], + channel: @notifica_me_channel + ) + end + + def permitted_params + params.require(:notifica_me_channel).permit(:notifica_me_id, :notifica_me_token, :notifica_me_type, :name) + end +end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 250c64a86105d..38134e8e96afe 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -94,7 +94,13 @@ def create def update @contact.assign_attributes(contact_update_params) - @contact.save! + # 'Channel::TwilioSms', 'Channel::Whatsapp', 'Channel::Sms' + Contact.transaction do + @contact.contact_inboxes + .select{ |ci| ['Channel::Whatsapp'].include?(ci.inbox.channel_type) } + .each{ |ci| ci.update_attribute(:source_id, @contact.phone_number.delete('+').to_s) } + @contact.save! + end process_avatar_from_url end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 2aedf19284b3e..0288dabcc6d24 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -125,7 +125,7 @@ def permitted_update_params def update_last_seen_on_conversation(last_seen_at, update_assignee) # rubocop:disable Rails/SkipsModelValidations - @conversation.update_column(:agent_last_seen_at, last_seen_at) + @conversation.update_column(:agent_last_seen_at, last_seen_at) && UpdateLastSeenJob.perform_later(@conversation.id, current_user, last_seen_at) @conversation.update_column(:assignee_last_seen_at, last_seen_at) if update_assignee.present? # rubocop:enable Rails/SkipsModelValidations end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 332f1528fc1e5..2d4d4b3f915b0 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -32,7 +32,7 @@ def set_global_config 'LOGOUT_REDIRECT_LINK', 'DISABLE_USER_PROFILE_UPDATE', 'DEPLOYMENT_ENV', - 'CSML_EDITOR_HOST' + 'CSML_EDITOR_HOST', 'CONVERSATION_STYLE_CSS' ).merge(app_config) end @@ -64,6 +64,7 @@ def app_config FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), IS_ENTERPRISE: ChatwootApp.enterprise?, AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''), + UNOAPI_AUTH_TOKEN: GlobalConfigService.load('UNOAPI_AUTH_TOKEN', ''), GIT_SHA: GIT_HASH } end diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index b8f3bd9a9a48f..8d79e2515f25c 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -37,6 +37,8 @@ def allowed_configs %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] when 'microsoft' %w[AZURE_APP_ID AZURE_APP_SECRET] + when 'unoapi' + %w[UNOAPI_AUTH_TOKEN] when 'email' ['MAILER_INBOUND_EMAIL_DOMAIN'] else diff --git a/app/controllers/webhooks/notifica_me_controller.rb b/app/controllers/webhooks/notifica_me_controller.rb new file mode 100644 index 0000000000000..90377cb716041 --- /dev/null +++ b/app/controllers/webhooks/notifica_me_controller.rb @@ -0,0 +1,6 @@ +class Webhooks::NotificaMeController < ActionController::API + def process_payload + Webhooks::NotificaMeEventsJob.perform_later(params.to_unsafe_hash) + head :ok + end +end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index da169d61f2ae3..5e652ba9d2a97 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -17,6 +17,7 @@ import { verifyServiceWorkerExistence, } from './helper/pushHelper'; import ReconnectService from 'dashboard/helper/ReconnectService'; +import Webphone from './components/layout/webphoneComponents/Webphone.vue'; export default { name: 'App', @@ -30,6 +31,7 @@ export default { WootSnackbarBox, UpgradeBanner, PendingEmailVerificationBanner, + Webphone, }, data() { return { @@ -123,6 +125,7 @@ export default { :class="{ 'app-rtl--wrapper': isRTL }" :dir="isRTL ? 'rtl' : 'ltr'" > + + + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/Draggable.vue b/app/javascript/dashboard/components/layout/webphoneComponents/Draggable.vue new file mode 100644 index 0000000000000..60f140df2c47e --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/Draggable.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/Webphone.vue b/app/javascript/dashboard/components/layout/webphoneComponents/Webphone.vue new file mode 100644 index 0000000000000..6f69aa8f4d157 --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/Webphone.vue @@ -0,0 +1,525 @@ + + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/icons/Microphone.vue b/app/javascript/dashboard/components/layout/webphoneComponents/icons/Microphone.vue new file mode 100644 index 0000000000000..8f8599eb5c759 --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/icons/Microphone.vue @@ -0,0 +1,39 @@ + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/icons/MicrophoneSlash.vue b/app/javascript/dashboard/components/layout/webphoneComponents/icons/MicrophoneSlash.vue new file mode 100644 index 0000000000000..c1c5b28c5c085 --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/icons/MicrophoneSlash.vue @@ -0,0 +1,39 @@ + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/icons/Numpad.vue b/app/javascript/dashboard/components/layout/webphoneComponents/icons/Numpad.vue new file mode 100644 index 0000000000000..8d36a41ace40d --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/icons/Numpad.vue @@ -0,0 +1,39 @@ + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/icons/Phone.vue b/app/javascript/dashboard/components/layout/webphoneComponents/icons/Phone.vue new file mode 100644 index 0000000000000..31586473690dd --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/icons/Phone.vue @@ -0,0 +1,39 @@ + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/icons/PhoneSlash.vue b/app/javascript/dashboard/components/layout/webphoneComponents/icons/PhoneSlash.vue new file mode 100644 index 0000000000000..4d44820079ce6 --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/icons/PhoneSlash.vue @@ -0,0 +1,39 @@ + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/icons/PhoneTransfer.vue b/app/javascript/dashboard/components/layout/webphoneComponents/icons/PhoneTransfer.vue new file mode 100644 index 0000000000000..24995d6cfba3c --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/icons/PhoneTransfer.vue @@ -0,0 +1,39 @@ + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/icons/Video.vue b/app/javascript/dashboard/components/layout/webphoneComponents/icons/Video.vue new file mode 100644 index 0000000000000..1311e2b35d1a3 --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/icons/Video.vue @@ -0,0 +1,39 @@ + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/icons/VideoSlash.vue b/app/javascript/dashboard/components/layout/webphoneComponents/icons/VideoSlash.vue new file mode 100644 index 0000000000000..d7d020b397d90 --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/icons/VideoSlash.vue @@ -0,0 +1,39 @@ + + diff --git a/app/javascript/dashboard/components/layout/webphoneComponents/store/index.js b/app/javascript/dashboard/components/layout/webphoneComponents/store/index.js new file mode 100644 index 0000000000000..04a8b21dbb79c --- /dev/null +++ b/app/javascript/dashboard/components/layout/webphoneComponents/store/index.js @@ -0,0 +1,27 @@ +// src/store.js + +import Vue from 'vue'; +import Vuex from 'vuex'; + +Vue.use(Vuex); + +const store = new Vuex.Store({ + state: { + message: 'Hello, World!', + }, + mutations: { + setMessage(state, payload) { + state.message = payload; + }, + }, + actions: { + updateMessage({ commit }, newMessage) { + commit('setMessage', newMessage); + }, + }, + getters: { + getMessage: state => state.message, + }, +}); + +export default store; diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index 95bd7d0e1d10d..95f5c11330453 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -30,16 +30,20 @@ export default { if (key === 'email') { return this.enabledFeatures.channel_email; } + if (key === 'notifica_me') { + return this.enabledFeatures.channel_notifica_me; + } + if (key === 'whatsapp') { + return this.enabledFeatures.channel_whatsapp; + } + if (key === 'website') { + return this.enabledFeatures.channel_website; + } + if (key === 'api') { + return this.enabledFeatures.channel_api; + } - return [ - 'website', - 'twilio', - 'api', - 'whatsapp', - 'sms', - 'telegram', - 'line', - ].includes(key); + return ['twilio', 'sms', 'telegram', 'line'].includes(key); }, }, methods: { diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 7b222aa7e5603..5b92d9a4ab256 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -78,6 +78,7 @@ export default { sms: 'sms', 'Channel::Line': 'line', 'Channel::Telegram': 'telegram', + 'Channel::NotificaMe': 'notifica_me', 'Channel::WebWidget': '', }[this.badge]; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 0685ebc53b879..40a489a60bc53 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -83,6 +83,7 @@ export default { inboxesList: 'inboxes/getInboxes', activeInbox: 'getSelectedInbox', accountId: 'getCurrentAccountId', + callInfo: 'webphone/getCallInfo', }), bulkActionCheck() { return !this.hideThumbnail && !this.hovered && !this.selected; @@ -285,6 +286,48 @@ export default { > {{ currentContact.name }} +

+ +

{{ $t('WEBPHONE.VOICE_CALL') }}

+

-

+

+ {{ $t('WEBPHONE.ACTIVE') }} +

+

+ {{ $t('WEBPHONE.TERMINATE') }} +

+

+ {{ $t('WEBPHONE.TERMINATE') }} +

+

+ {{ $t('WEBPHONE.CONNECT_CALLING') }} +

+

+ {{ $t('WEBPHONE.CALLING') }} +

+
({}), }, inReplyTo: { - type: Object, - default: () => ({}), + type: Promise, + default: Promise.resolve({}), }, }, setup() { @@ -85,9 +88,15 @@ export default { hasMediaLoadError: false, contextMenuPosition: {}, showBackgroundHighlight: false, + inReplyToMessage: {}, }; }, computed: { + ...mapGetters({ + isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', + currentRole: 'getCurrentRole', + accountId: 'getCurrentAccountId', + }), attachments() { // Here it is used to get sender and created_at for each attachment return this.data?.attachments.map(attachment => ({ @@ -180,11 +189,22 @@ export default { contextMenuEnabledOptions() { return { copy: this.hasText, - delete: this.hasText || this.hasAttachments, + delete: + !this.hideDeleteMessageForAgents && + (this.hasText || this.hasAttachments), cannedResponse: this.isOutgoing && this.hasText, replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing, }; }, + hideDeleteMessageForAgents() { + return ( + this.currentRole !== 'administrator' && + this.isFeatureEnabledonAccount( + this.accountId, + 'hide_delete_message_for_agent' + ) + ); + }, contentAttributes() { return this.data.content_attributes || {}; }, @@ -347,10 +367,11 @@ export default { this.hasMediaLoadError = false; }, }, - mounted() { + async mounted() { this.hasMediaLoadError = false; this.$emitter.on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu); this.setupHighlightTimer(); + this.inReplyToMessage = await this.inReplyTo; }, beforeDestroy() { this.$emitter.off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu); @@ -449,7 +470,9 @@ export default { >
diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagePreview.vue b/app/javascript/dashboard/components/widgets/conversation/MessagePreview.vue index 4df6e16e17bc2..8ca512c5beef4 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagePreview.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagePreview.vue @@ -2,9 +2,21 @@ import { MESSAGE_TYPE } from 'widget/helpers/constants'; import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; import { ATTACHMENT_ICONS } from 'shared/constants/messages'; +import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue'; +import InstagramStory from './bubble/InstagramStory.vue'; +import BubbleLocation from './bubble/Location.vue'; +import BubbleContact from './bubble/Contact.vue'; +import BubbleFile from './bubble/File.vue'; export default { name: 'MessagePreview', + components: { + BubbleImageAudioVideo, + InstagramStory, + BubbleLocation, + BubbleContact, + BubbleFile, + }, props: { message: { type: Object, @@ -18,6 +30,10 @@ export default { type: String, default: '', }, + short: { + type: Boolean, + default: true, + }, }, setup() { const { getPlainText } = useMessageFormatter(); @@ -25,7 +41,21 @@ export default { getPlainText, }; }, + data: () => { + return { + previewMessage: null, + }; + }, computed: { + contentAttributes() { + return this.message.content_attributes || {}; + }, + isAnInstagramStory() { + return this.contentAttributes.image_type === 'story_mention'; + }, + attachments() { + return this.message?.attachments || []; + }, messageByAgent() { const { message_type: messageType } = this.message; return messageType === MESSAGE_TYPE.OUTGOING; @@ -57,6 +87,24 @@ export default { return this.message && this.message.content_type === 'sticker'; }, }, + watch: { + data() { + this.hasMediaLoadError = false; + }, + }, + mounted() { + this.hasMediaLoadError = false; + }, + methods: { + isAttachmentImageVideoAudio(fileType) { + return ['image', 'audio', 'video', 'story_mention', 'ig_reel'].includes( + fileType + ); + }, + onMediaLoadError() { + this.hasMediaLoadError = true; + }, + }, }; @@ -94,13 +142,42 @@ export default { {{ parsedLastMessage }} - - {{ $t(`${attachmentMessageContent}`) }} +
+ + {{ $t(`${attachmentMessageContent}`) }} +
+
+
+ + + + + +
+
{{ defaultEmptyMessage || $t('CHAT_LIST.NO_CONTENT') }} diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index c5d1facee7569..4a9f446a5aeb4 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -1,5 +1,8 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js index 4209fa9e0d876..afc6b9637a18a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js @@ -7,6 +7,7 @@ import Sms from './channels/Sms.vue'; import Whatsapp from './channels/Whatsapp.vue'; import Line from './channels/Line.vue'; import Telegram from './channels/Telegram.vue'; +import NotificaMe from './channels/NotificaMe.vue'; const channelViewList = { facebook: Facebook, @@ -18,6 +19,7 @@ const channelViewList = { whatsapp: Whatsapp, line: Line, telegram: Telegram, + notifica_me: NotificaMe, }; export default { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue index 88eba6b4e7cc7..85138cfc5ad96 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue @@ -17,6 +17,7 @@ export default { apiKey: '', phoneNumberId: '', businessAccountId: '', + advanced: false, }; }, computed: { @@ -63,7 +64,9 @@ export default { }); } catch (error) { useAlert( - error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE') + error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE') + + '\n detail:' + + error ); } }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/NotificaMe.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/NotificaMe.vue new file mode 100644 index 0000000000000..e99d71924d0b7 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/NotificaMe.vue @@ -0,0 +1,179 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Unoapi.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Unoapi.vue new file mode 100644 index 0000000000000..887dc622ecf0b --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Unoapi.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue index d361e0f78946e..b812e844f9c5d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue @@ -1,6 +1,7 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/macros/constants.js b/app/javascript/dashboard/routes/dashboard/settings/macros/constants.js index 111a3632d9ccb..e0bad4cb6a5ec 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/macros/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/macros/constants.js @@ -44,6 +44,11 @@ export const MACRO_ACTION_TYPES = [ label: 'Resolve conversation', inputType: null, }, + { + key: 'send_webhook_event', + label: 'Send Webhook Event', + inputType: 'url', + }, { key: 'send_attachment', label: 'Send Attachment', diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js old mode 100755 new mode 100644 index f2e03f733bffa..2453bc5c47dbb --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -46,6 +46,7 @@ import userNotificationSettings from './modules/userNotificationSettings'; import webhooks from './modules/webhooks'; import draftMessages from './modules/draftMessages'; import SLAReports from './modules/SLAReports'; +import webphone from './modules/webphone'; const plugins = []; @@ -97,6 +98,7 @@ export default new Vuex.Store({ draftMessages, sla, slaReports: SLAReports, + webphone, }, plugins, }); diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index e3f2922b6389f..cc1d093bfb192 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -5,6 +5,7 @@ import InboxesAPI from '../../api/inboxes'; import WebChannel from '../../api/channel/webChannel'; import FBChannel from '../../api/channel/fbChannel'; import TwilioChannel from '../../api/channel/twilioChannel'; +import NotificaMeChannel from '../../api/channel/notificaMeChannel'; import { throwErrorMessage } from '../utils/api'; import AnalyticsHelper from '../../helper/AnalyticsHelper'; import { ACCOUNT_EVENTS } from '../../helper/AnalyticsHelper/events'; @@ -138,7 +139,7 @@ export const actions = { get: async ({ commit }) => { commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: true }); try { - const response = await InboxesAPI.get(true); + const response = await InboxesAPI.get(false); commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: false }); commit(types.default.SET_INBOXES, response.data.payload); } catch (error) { @@ -173,6 +174,19 @@ export const actions = { return throwErrorMessage(error); } }, + createNotificaMeChannel: async ({ commit }, params) => { + try { + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true }); + const response = await NotificaMeChannel.create(params); + commit(types.default.ADD_INBOXES, response.data); + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false }); + sendAnalyticsEvent('notifica_me'); + return response.data; + } catch (error) { + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false }); + throw new Error(error); + } + }, createTwilioChannel: async ({ commit }, params) => { try { commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true }); diff --git a/app/javascript/dashboard/store/modules/webphone.js b/app/javascript/dashboard/store/modules/webphone.js new file mode 100644 index 0000000000000..48cabe81deb71 --- /dev/null +++ b/app/javascript/dashboard/store/modules/webphone.js @@ -0,0 +1,278 @@ +import * as types from '../mutation-types'; +import Wavoip from 'wavoip-api'; +import { useAlert } from 'dashboard/composables'; + +const findRecordById = ($state, id) => + $state.records.find(record => record.id === Number(id)) || {}; + +const defaultState = { + records: [], + uiFlags: { + isOpen: true, + isFetching: false, + isFetchingItem: false, + isUpdating: false, + isCheckoutInProcess: false, + }, + call: { + id: null, + duration: 0, + tag: null, + phone: null, + picture_profile: null, + status: null, + direction: null, + whatsapp_instance: null, + active_start_date: null, + chat_id: null, + inbox_name: null, + }, + wavoip: {}, +}; + +export const getters = { + getAccount: $state => id => { + return findRecordById($state, id); + }, + getUIFlags($state) { + return $state.uiFlags; + }, + getCallInfo($state) { + return $state.call; + }, + getWavoip($state) { + return $state.wavoip; + }, +}; + +export const actions = { + startWavoip: async ({ commit, state }, { inboxName, token }) => { + if (state.wavoip[token] && token) { + return; + } + + const WAV = new Wavoip(); + const whatsapp_instance = WAV.connect(token); + + commit(types.default.ADD_WAVOIP, { + token: token, + whatsapp_instance: whatsapp_instance, + inboxName: inboxName, + }); + + whatsapp_instance.socket.on('connect', () => {}); + + whatsapp_instance.socket.on('disconnect', () => {}); + }, + outcomingCall: async ({ commit, state, dispatch }, callInfo) => { + let { phone, contact_name, chat_id } = callInfo; + + let instances = callInfo.instances ?? Object.keys(state.wavoip); + let token = callInfo.token ?? instances[0]; + let wavoip = state.wavoip[token].whatsapp_instance; + let inbox_name = state.wavoip[token].inbox_name; + + let offerResponse; + if (wavoip) { + offerResponse = await wavoip + .callStart({ + whatsappid: phone, + }) + .then(response => { + let output; + if (response.type === 'success') { + let profile_picture = response?.result?.profile_picture; + + output = { + profile_picture: profile_picture, + }; + } else { + output = { + error: true, + message: response?.result, + }; + } + + return output; + }) + .catch(response => { + return { + error: true, + message: response?.result, + }; + }); + } else { + offerResponse = { + error: true, + }; + } + + if (offerResponse.error) { + let remainingInstances = instances.filter(instance => instance !== token); + + if (offerResponse.message === 'Numero não existe') { + throw new Error(offerResponse.message); + } else if (offerResponse.message === 'Limite de ligações atingido') { + useAlert('Limite de ligações diários atingido'); + } + + if (remainingInstances.length > 0) { + dispatch('outcomingCall', { + ...callInfo, + instances: remainingInstances, + token: null, + }); + } else { + throw new Error('Linha ocupada, tente mais tarde ou faça um upgrade'); + } + + return; + } + + commit(types.default.SET_WEBPHONE_CALL, { + id: token, + duration: 0, + tag: contact_name, + phone: phone, + picture_profile: offerResponse?.profile_picture, + status: 'outcoming_calling', + direction: 'outcoming', + whatsapp_instance: token, + inbox_name: inbox_name, + chat_id: chat_id, + }); + + commit(types.default.SET_WEBPHONE_UI_FLAG, { + isOpen: true, + }); + }, + incomingCall: async ({ commit, state, rootGetters, dispatch }, callInfo) => { + try { + const userStatus = rootGetters.getCurrentUserAvailability; + + if (state.call.id || userStatus !== 'online') { + dispatch('rejectCall', callInfo.token); + return; + } + + let { phone, contact_name, profile_picture, token } = callInfo; + + let inbox_name = state.wavoip[token].inbox_name; + + commit(types.default.SET_WEBPHONE_CALL, { + id: token, + duration: 0, + tag: contact_name, + phone: phone, + picture_profile: profile_picture, + status: 'offer', + direction: 'incoming', + whatsapp_instance: token, + inbox_name: inbox_name, + chat_id: null, + }); + + commit(types.default.SET_WEBPHONE_UI_FLAG, { + isOpen: true, + }); + } catch (error) { + throw new Error(error); + } + }, + updateCallStatus: ({ commit }, status) => { + commit(types.default.SET_WEBPHONE_CALL, { + status: status, + }); + + if (status === 'accept') { + commit(types.default.SET_WEBPHONE_CALL, { + active_start_date: new Date(), + }); + } + }, + acceptCall: async ({ state, dispatch }) => { + try { + const wavoip_token = state.call.whatsapp_instance; + const wavoip = state.wavoip[wavoip_token].whatsapp_instance; + + await wavoip.acceptCall(); + + dispatch('updateCallStatus', 'accept'); + } catch (error) { + // Ignore error + } + }, + rejectCall: async ({ state, dispatch }, token) => { + try { + const wavoip_token = token ?? state.call.whatsapp_instance; + const wavoip = state.wavoip[wavoip_token].whatsapp_instance; + + wavoip.rejectCall(); + dispatch('resetCall'); + } catch (error) { + // Ignore error + } + }, + endCall: async ({ state }) => { + try { + const wavoip_token = state.call.whatsapp_instance; + const wavoip = state.wavoip[wavoip_token].whatsapp_instance; + + wavoip.endCall(); + } catch (error) { + // Ignore error + } + }, + resetCall: async ({ commit }) => { + commit(types.default.SET_WEBPHONE_CALL, { + id: null, + duration: 0, + tag: null, + phone: null, + picture_profile: null, + status: null, + direction: null, + whatsapp_instance: null, + active_start_date: null, + inbox_name: null, + chat_id: null, + }); + }, + updateWebphoneVisible: ({ commit }, { isOpen }) => { + commit(types.default.SET_WEBPHONE_UI_FLAG, { + isOpen: isOpen, + }); + }, +}; + +export const mutations = { + [types.default.SET_WEBPHONE_UI_FLAG]($state, data) { + $state.uiFlags = { + ...$state.uiFlags, + ...data, + }; + }, + [types.default.ADD_WAVOIP]($state, data) { + $state.wavoip = { + ...$state.wavoip, + [data.token]: { + whatsapp_instance: data.whatsapp_instance, + inbox_name: data.inboxName, + }, + }; + }, + [types.default.SET_WEBPHONE_CALL]($state, data) { + $state.call = { + ...$state.call, + ...data, + }; + }, +}; + +export default { + namespaced: true, + state: defaultState, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 2a6bcacb8ab5b..d32f71ec0c97e 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -333,4 +333,10 @@ export default { SET_SLA_REPORTS: 'SET_SLA_REPORTS', SET_SLA_REPORTS_METRICS: 'SET_SLA_REPORTS_METRICS', SET_SLA_REPORTS_META: 'SET_SLA_REPORTS_META', + + // Webphone + + SET_WEBPHONE_UI_FLAG: 'SET_ACCOUNT_UI_FLAG', + SET_WEBPHONE_CALL: 'SET_WEBPHONE_CALL', + ADD_WAVOIP: 'ADD_WAVOIP', }; diff --git a/app/javascript/shared/components/FluentIcon/Icon.vue b/app/javascript/shared/components/FluentIcon/Icon.vue index f09e4d3f4d55e..7ab48948dc2e1 100644 --- a/app/javascript/shared/components/FluentIcon/Icon.vue +++ b/app/javascript/shared/components/FluentIcon/Icon.vue @@ -31,6 +31,9 @@ export default { pathSource() { // To support icons with multiple paths const path = this.icons[`${this.icon}-${this.type}`]; + if (!path) { + throw new Error(`Icon not found ${this.icon}-${this.type}`); + } if (path.constructor === Array) { return path; } diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 918d012be783a..703c54d4816d8 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -285,5 +285,6 @@ "M9.60364 9.20645C9.60364 8.67008 10.0385 8.23523 10.5749 8.23523C11.1113 8.23523 11.5461 8.67008 11.5461 9.20645V11.4511C11.5461 11.9875 11.1113 12.4223 10.5749 12.4223C10.0385 12.4223 9.60364 11.9875 9.60364 11.4511V9.20645Z", "M17.1442 5.57049C13.5275 5.06019 10.5793 5.04007 6.88135 5.56825C5.9466 5.70176 5.32812 5.79197 4.85654 5.92976C4.41928 6.05757 4.17061 6.20994 3.96492 6.43984C3.539 6.91583 3.48286 7.45419 3.4248 9.33184C3.36775 11.1772 3.48076 12.831 3.69481 14.6918C3.80887 15.6834 3.88736 16.3526 4.01268 16.8613C4.13155 17.3439 4.27532 17.6034 4.47513 17.802C4.67654 18.0023 4.93467 18.1435 5.40841 18.2581C5.90952 18.3793 6.56702 18.4526 7.5442 18.5592C10.7045 18.904 13.0702 18.9022 16.2423 18.561C17.2313 18.4546 17.8995 18.3813 18.4081 18.2609C18.8913 18.1465 19.1511 18.0063 19.3497 17.8118C19.5442 17.6213 19.6928 17.3587 19.8217 16.852C19.9561 16.3234 20.0476 15.624 20.18 14.5966C20.4162 12.7633 20.5863 11.1533 20.5929 9.3896C20.5999 7.50391 20.5613 6.96737 20.1306 6.46971C19.9226 6.22932 19.6696 6.0713 19.2224 5.93968C18.7395 5.79754 18.1042 5.70594 17.1442 5.57049ZM6.65555 3.98715C10.5078 3.43695 13.6072 3.45849 17.3674 3.98902L17.4224 3.99678C18.3127 4.12235 19.0648 4.22844 19.6733 4.40753C20.33 4.60078 20.8792 4.89417 21.3382 5.4245C22.2041 6.42482 22.1984 7.6117 22.1909 9.18858C22.1905 9.25686 22.1902 9.32584 22.19 9.3956C22.183 11.2604 22.0026 12.949 21.764 14.8006L21.7577 14.8496C21.6332 15.8159 21.5307 16.6121 21.3695 17.2458C21.2 17.9121 20.9467 18.4833 20.4672 18.9529C19.9919 19.4183 19.4302 19.6602 18.776 19.8151C18.1582 19.9613 17.3895 20.044 16.4629 20.1436L16.4131 20.149C13.1283 20.5023 10.6472 20.5043 7.37097 20.1469L7.32043 20.1414C6.40679 20.0417 5.64604 19.9587 5.03292 19.8104C4.38112 19.6527 3.82317 19.406 3.34911 18.9347C2.87346 18.4618 2.62363 17.8999 2.46191 17.2433C2.30938 16.6241 2.22071 15.8531 2.11393 14.9246L2.10815 14.8743C1.88863 12.9659 1.76823 11.23 1.82845 9.28246C1.83063 9.2118 1.83272 9.14191 1.83479 9.07281C1.8816 7.50776 1.91671 6.33374 2.7747 5.37486C3.22992 4.86612 3.76798 4.58399 4.40853 4.39678C5.00257 4.22316 5.73505 4.11858 6.60207 3.99479C6.61981 3.99225 6.63764 3.9897 6.65555 3.98715Z" ], - "scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0" + "scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0", + "clock-outline": "M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333ZM11.25 6a.75.75 0 0 1 .743.648L12 6.75V12h3.25a.75.75 0 0 1 .102 1.493l-.102.007h-4a.75.75 0 0 1-.743-.648l-.007-.102v-6a.75.75 0 0 1 .75-.75Z" } diff --git a/app/javascript/shared/components/FluentIcon/icons.json b/app/javascript/shared/components/FluentIcon/icons.json index 441989c15d96c..6a361f2887a43 100644 --- a/app/javascript/shared/components/FluentIcon/icons.json +++ b/app/javascript/shared/components/FluentIcon/icons.json @@ -22,5 +22,6 @@ "M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z", "M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z", "M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z" - ] + ], + "clock-outline": "M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333ZM11.25 6a.75.75 0 0 1 .743.648L12 6.75V12h3.25a.75.75 0 0 1 .102 1.493l-.102.007h-4a.75.75 0 0 1-.743-.648l-.007-.102v-6a.75.75 0 0 1 .75-.75Z" } diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js index 3953cfb402dd5..0e248a7a6cbb9 100644 --- a/app/javascript/shared/mixins/inboxMixin.js +++ b/app/javascript/shared/mixins/inboxMixin.js @@ -9,6 +9,7 @@ export const INBOX_TYPES = { TELEGRAM: 'Channel::Telegram', LINE: 'Channel::Line', SMS: 'Channel::Sms', + NOTIFICA_ME: 'Channel::NotificaMe', }; export const INBOX_FEATURES = { @@ -26,6 +27,7 @@ export const INBOX_FEATURE_MAP = { INBOX_TYPES.WHATSAPP, INBOX_TYPES.TELEGRAM, INBOX_TYPES.API, + INBOX_TYPES.NOTIFICA_ME, ], [INBOX_FEATURES.REPLY_TO_OUTGOING]: [ INBOX_TYPES.WEB, @@ -33,6 +35,7 @@ export const INBOX_FEATURE_MAP = { INBOX_TYPES.WHATSAPP, INBOX_TYPES.TELEGRAM, INBOX_TYPES.API, + INBOX_TYPES.NOTIFICA_ME, ], }; @@ -74,6 +77,9 @@ export default { isATelegramChannel() { return this.channelType === INBOX_TYPES.TELEGRAM; }, + isANotificaMeChannel() { + return this.channelType === INBOX_TYPES.NOTIFICA_ME; + }, isATwilioSMSChannel() { const { medium: medium = '' } = this.inbox; return this.isATwilioChannel && medium === 'sms'; @@ -91,6 +97,12 @@ export default { this.whatsAppAPIProvider === 'whatsapp_cloud' ); }, + isAUnoapiChannel() { + return ( + this.channelType === INBOX_TYPES.WHATSAPP && + this.whatsAppAPIProvider === 'unoapi' + ); + }, is360DialogWhatsAppChannel() { return ( this.channelType === INBOX_TYPES.WHATSAPP && diff --git a/app/javascript/shared/store/globalConfig.js b/app/javascript/shared/store/globalConfig.js index 5a266bd216201..6a32af6aae30f 100644 --- a/app/javascript/shared/store/globalConfig.js +++ b/app/javascript/shared/store/globalConfig.js @@ -20,6 +20,7 @@ const { WIDGET_BRAND_URL: widgetBrandURL, DISABLE_USER_PROFILE_UPDATE: disableUserProfileUpdate, DEPLOYMENT_ENV: deploymentEnv, + CONVERSATION_STYLE_CSS: conversationStyleCss, } = window.globalConfig || {}; const state = { @@ -44,6 +45,7 @@ const state = { privacyURL, termsURL, widgetBrandURL, + conversationStyleCss: conversationStyleCss || '', }; export const getters = { diff --git a/app/jobs/campaign_message_job.rb b/app/jobs/campaign_message_job.rb new file mode 100644 index 0000000000000..39fb98d8fce3b --- /dev/null +++ b/app/jobs/campaign_message_job.rb @@ -0,0 +1,67 @@ +class CampaignMessageJob < ApplicationJob + include Whatsapp::IncomingMessageServiceHelpers + queue_as :low + retry_on ActiveRecord::RecordNotFound, wait: 30.seconds, attempts: 5 + + def perform(account_id, inbox_id, campaign_id, content, audience) + contact_inbox = create_contact_inbox(inbox_id, audience) + conversation = create_conversation(contact_inbox) + conversation.messages.create!( + content: bind(content, audience), + account_id: account_id, + content_type: :text, + inbox_id: inbox_id, + message_type: :outgoing, + status: :progress, + additional_attributes: { + campaign_id: campaign_id, + audience_id: audience[:audience_id] + } + ) + end + + private + + def bind(template, params) + params[:first_name] = params[:name].split[0]&.capitalize if params[:name] + [:identifier, :first_name, :name, :due_at, :value, :scheduled_at].reduce(template) do |content, field| + params[field] ? content.gsub("##{field}", params[field]) : content + end + end + + def create_contact_inbox(inbox_id, params) + phone_number = params[:phone_number].delete('+').to_s + phone_number = brazil_phone_number?(phone_number) ? normalised_brazil_mobile_number(phone_number) : phone_number + + attributes = { name: params[:name], phone_number: "+#{phone_number}" } + + contact_inbox = ContactInboxWithContactBuilder.new( + source_id: phone_number, + inbox: Inbox.find(inbox_id), + contact_attributes: attributes + ).perform + + raise ActiveRecord::RecordNotFound if contact_inbox.nil? + + [:email, :identifier].each do |field| + next if contact_inbox.contact[field] || !params[field] + + # rubocop:disable Rails/SkipsModelValidations + begin + contact_inbox.contact.update_column(field, params[field]) + rescue ActiveRecord::RecordNotUnique => e + Rails.logger.warn("Ignore exception on campaign message job: #{e.message}") + end + # rubocop:enable Rails/SkipsModelValidations + end + + contact_inbox + end + + def create_conversation(contact_inbox) + conversation = ConversationBuilder.new(params: { status: :resolved }, contact_inbox: contact_inbox).perform + raise ActiveRecord::RecordNotFound if conversation.nil? + + conversation + end +end diff --git a/app/jobs/campaign_message_update_job.rb b/app/jobs/campaign_message_update_job.rb new file mode 100644 index 0000000000000..7a028e8e446f4 --- /dev/null +++ b/app/jobs/campaign_message_update_job.rb @@ -0,0 +1,22 @@ +class CampaignMessageUpdateJob < ApplicationJob + queue_as :low + + def perform(campaign_id, audience_id, status) + campaign = Campaign.find(campaign_id) + new_audience = campaign.audience.map do |a| + if a['audience_id'] == audience_id && (!a['status'] || a['status'] == :failed || (statuses(status) > statuses(a['status']))) + a['status'] = status + end + a + end + # rubocop:disable Rails/SkipsModelValidations + campaign.update_column(:audience, new_audience) + # rubocop:enable Rails/SkipsModelValidations + end + + private + + def statuses(status) + [:scheduled, :progress, :sent, :delivered, :read, :failed].find_index(status.to_sym) + end +end diff --git a/app/jobs/channels/notifica_me/templates_sync_job.rb b/app/jobs/channels/notifica_me/templates_sync_job.rb new file mode 100644 index 0000000000000..dc9cae43c3c49 --- /dev/null +++ b/app/jobs/channels/notifica_me/templates_sync_job.rb @@ -0,0 +1,7 @@ +class Channels::NotificaMe::TemplatesSyncJob < ApplicationJob + queue_as :low + + def perform(channel) + channel.sync_templates + end +end diff --git a/app/jobs/channels/notifica_me/templates_sync_scheduler_job.rb b/app/jobs/channels/notifica_me/templates_sync_scheduler_job.rb new file mode 100644 index 0000000000000..1cfda13a9799e --- /dev/null +++ b/app/jobs/channels/notifica_me/templates_sync_scheduler_job.rb @@ -0,0 +1,17 @@ +class Channels::NotificaMe::TemplatesSyncSchedulerJob < ApplicationJob + queue_as :low + + def perform + Channel::NotificaMe + .where( + '(message_templates_last_updated <= ? OR message_templates_last_updated IS NULL) AND notifica_me_type = ?', + 3.hours.ago, + 'whatsapp_business_account' + ) + .order(Arel.sql('message_templates_last_updated IS NULL DESC, message_templates_last_updated ASC')) + .limit(Limits::BULK_EXTERNAL_HTTP_CALLS_LIMIT) + .each do |channel| + Channels::NotificaMe::TemplatesSyncJob.perform_later(channel) + end + end +end diff --git a/app/jobs/internal/check_new_versions_job.rb b/app/jobs/internal/check_new_versions_job.rb index a141385d08522..2363fa2399587 100644 --- a/app/jobs/internal/check_new_versions_job.rb +++ b/app/jobs/internal/check_new_versions_job.rb @@ -11,7 +11,7 @@ def perform private def update_version_info - return if @instance_info['version'].blank? + return if @instance_info.blank? || @instance_info['version'].blank? ::Redis::Alfred.set(::Redis::Alfred::LATEST_CHATWOOT_VERSION, @instance_info['version']) end diff --git a/app/jobs/send_reply_job.rb b/app/jobs/send_reply_job.rb index cef80c9df0bcd..194ebb2d5a5d6 100644 --- a/app/jobs/send_reply_job.rb +++ b/app/jobs/send_reply_job.rb @@ -1,5 +1,6 @@ class SendReplyJob < ApplicationJob queue_as :high + retry_on ActiveRecord::RecordNotFound, wait: 30.seconds, attempts: 5 def perform(message_id) message = Message.find(message_id) @@ -12,7 +13,8 @@ def perform(message_id) 'Channel::Line' => ::Line::SendOnLineService, 'Channel::Telegram' => ::Telegram::SendOnTelegramService, 'Channel::Whatsapp' => ::Whatsapp::SendOnWhatsappService, - 'Channel::Sms' => ::Sms::SendOnSmsService + 'Channel::Sms' => ::Sms::SendOnSmsService, + 'Channel::NotificaMe' => ::NotificaMe::SendOnNotificaMeService } case channel_name diff --git a/app/jobs/trigger_scheduled_items_job.rb b/app/jobs/trigger_scheduled_items_job.rb index 15b0011c8b8d9..642f450238b40 100644 --- a/app/jobs/trigger_scheduled_items_job.rb +++ b/app/jobs/trigger_scheduled_items_job.rb @@ -22,6 +22,9 @@ def perform # Job to clear notifications which are older than 1 month Notification::RemoveOldNotificationJob.perform_later + + # Job to sync templates notifica_me + Channels::NotificaMe::TemplatesSyncSchedulerJob.perform_later end end diff --git a/app/jobs/update_last_seen_job.rb b/app/jobs/update_last_seen_job.rb new file mode 100644 index 0000000000000..5924c1036a7b5 --- /dev/null +++ b/app/jobs/update_last_seen_job.rb @@ -0,0 +1,37 @@ +class UpdateLastSeenJob < ApplicationJob + queue_as :default + + def perform(conversation_id, user, agent_last_seen_at) + conversation = Conversation.find(conversation_id) + + agent_viewed(conversation, user) if conversation.account.feature_enabled?('agent_conversation_viewed') + + read_message(conversation, agent_last_seen_at) if conversation.account.feature_enabled?('read_message') + end + + private + + def read_message(conversation, agent_last_seen_at) + messages = conversation.messages.to_read(agent_last_seen_at) + Rails.logger.debug { "Conversation #{conversation.id} with #{messages.size} message(s) to update status to read" } + messages.each do |message| + Rails.logger.debug { "Update message id #{message.id} source #{message.source_id} status to read" } + message.update(status: :read) + end + end + + def agent_viewed(conversation, user) + return unless conversation.open? + + key = "conversation:last_seen:#{conversation.id}:#{user.id}" + return if ::Redis::Alfred.get(key) + + ::Redis::Alfred.setex(key, true, 1.hour) + params = { + message_type: :activity, + content_type: :text, + content: "Conversa visualizada por #{user.id} - #{user.name}" + } + Messages::MessageBuilder.new(user, conversation, params).perform + end +end diff --git a/app/jobs/webhook_job.rb b/app/jobs/webhook_job.rb index 57d3739b72ce0..599e2abc82a5e 100644 --- a/app/jobs/webhook_job.rb +++ b/app/jobs/webhook_job.rb @@ -1,7 +1,8 @@ class WebhookJob < ApplicationJob queue_as :medium - # There are 3 types of webhooks, account, inbox and agent_bot - def perform(url, payload, webhook_type = :account_webhook) - Webhooks::Trigger.execute(url, payload, webhook_type) + + def perform(url, payload, webhook_type = :account_webhook, method = :post, headers = { content_type: :json, accept: :json }) + # There are 3 types of webhooks, account, inbox and agent_bot + Webhooks::Trigger.execute(url, payload, webhook_type, method, headers) end end diff --git a/app/jobs/webhooks/notifica_me_events_job.rb b/app/jobs/webhooks/notifica_me_events_job.rb new file mode 100644 index 0000000000000..007b83afd8dc5 --- /dev/null +++ b/app/jobs/webhooks/notifica_me_events_job.rb @@ -0,0 +1,458 @@ +# { +# "type":"MESSAGE_STATUS", +# "timestamp":"2024-03-29 07:55:58 pm", +# "subscriptionId":"f44d13f2-fb66-4dc8-85fe-7973b296dd3a", +# "channel":"telegram", +# "messageId":"c09b633a-ceab-4bb5-854a-5ce5d27717cb", +# "contentIndex":0, +# "messageStatus":{ +# "timestamp":"2024-03-29 07:55:58 pm", +# "code":"REJECTED", +# "description":"The message was rejected by the provider" +# }, +# "hub_access_token":"f20018fa-eb17-11ee-880c-0efa6ad28f4f.f44d13f2-fb66-4dc8-85fe-7973b296dd3a", +# "controller":"webhooks/notifica_me", +# "action":"process_payload", +# "channel_id":"f44d13f2-fb66-4dc8-85fe-7973b296dd3a", +# "notifica_me":{ +# "type":"MESSAGE_STATUS", +# "timestamp":"2024-03-29 07:55:58 pm", +# "subscriptionId":"f44d13f2-fb66-4dc8-85fe-7973b296dd3a", +# "channel":"telegram", +# "messageId":"c09b633a-ceab-4bb5-854a-5ce5d27717cb", +# "contentIndex":0, +# "messageStatus":{ +# "timestamp":"2024-03-29 07:55:58 pm", +# "code":"REJECTED", +# "description":"The message was rejected by the provider" +# } +# } +# } + +# { +# "type":"MESSAGE", +# "id":"9a643fde-7081-48cb-b2f2-14ddfb64cee9", +# "timestamp":"2024-03-29 10:38:07 pm", +# "subscriptionId":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "channel":"facebook", +# "direction":"IN", +# "message":{ +# "id":"9a643fde-7081-48cb-b2f2-14ddfb64cee9", +# "from":"7136628119690362", +# "to":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "direction":"IN", +# "channel":"facebook", +# "visitor":{ +# "name":"Clairton Rodrigo Heinzen", +# "firstName":"Clairton Rodrigo", +# "lastName":"Heinzen", +# "picture":"https://platform-lookaside.fbsbx.com/platform/profilepic/?eai=AXEMHxV_i8n2M787_v-PphBIRvN70e50_ZaJaygq0OQNIPH7-sfrUhrglqdj5eLWYJ23wF8DG84M&psid=7136628119690362&width=1024&ext=1714343887&hash=AfplUu_FmSY14ettnD4O4XLRtMcAv7jy3Qp7ZQWQJLvZCw" +# }, +# "contents":[ +# { +# "type":"text", +# "text":"teste" +# } +# ], +# "timestamp":"2024-03-29 10:38:07 pm" +# }, +# "hub_access_token":"f20018fa-eb17-11ee-880c-0efa6ad28f4f.a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "controller":"webhooks/notifica_me", +# "action":"process_payload", +# "channel_id":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "notifica_me":{ +# "type":"MESSAGE", +# "id":"9a643fde-7081-48cb-b2f2-14ddfb64cee9", +# "timestamp":"2024-03-29 10:38:07 pm", +# "subscriptionId":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "channel":"facebook", +# "direction":"IN", +# "message":{ +# "id":"9a643fde-7081-48cb-b2f2-14ddfb64cee9", +# "from":"7136628119690362", +# "to":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "direction":"IN", +# "channel":"facebook", +# "visitor":{ +# "name":"Clairton Rodrigo Heinzen", +# "firstName":"Clairton Rodrigo", +# "lastName":"Heinzen", +# "picture":"https://platform-lookaside.fbsbx.com/platform/profilepic/?eai=AXEMHxV_i8n2M787_v-PphBIRvN70e50_ZaJaygq0OQNIPH7-sfrUhrglqdj5eLWYJ23wF8DG84M&psid=7136628119690362&width=1024&ext=1714343887&hash=AfplUu_FmSY14ettnD4O4XLRtMcAv7jy3Qp7ZQWQJLvZCw" +# }, +# "contents":[ +# { +# "type":"text", +# "text":"teste" +# } +# ], +# "timestamp":"2024-03-29 10:38:07 pm" +# } +# } +# } + +# { +# "type":"MESSAGE_STATUS", +# "timestamp":"2024-03-29 11:40:01 pm", +# "subscriptionId":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "channel":"facebook", +# "messageId":"0550ff2d-62f1-45e7-bbf6-fb7b33bb6f80", +# "contentIndex":0, +# "messageStatus":{ +# "timestamp":"2024-03-29 11:40:01 pm", +# "code":"SENT", +# "description":"The message has been forwarded to the provider" +# }, +# "hub_access_token":"f20018fa-eb17-11ee-880c-0efa6ad28f4f.a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "controller":"webhooks/notifica_me", +# "action":"process_payload", +# "channel_id":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "notifica_me":{ +# "type":"MESSAGE_STATUS", +# "timestamp":"2024-03-29 11:40:01 pm", +# "subscriptionId":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "channel":"facebook", +# "messageId":"0550ff2d-62f1-45e7-bbf6-fb7b33bb6f80", +# "contentIndex":0, +# "messageStatus":{ +# "timestamp":"2024-03-29 11:40:01 pm", +# "code":"SENT", +# "description":"The message has been forwarded to the provider" +# } +# }, +# "retried":true +# } + +# { +# "type":"MESSAGE", +# "id":"b35acb2b-f514-4265-bbeb-2c7b6a067317", +# "timestamp":"2024-03-30 11:45:31 am", +# "subscriptionId":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "channel":"facebook", +# "direction":"IN", +# "message":{ +# "id":"b35acb2b-f514-4265-bbeb-2c7b6a067317", +# "from":"7136628119690362", +# "to":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "direction":"IN", +# "channel":"facebook", +# "visitor":{ +# "name":"Clairton Rodrigo Heinzen", +# "firstName":"Clairton Rodrigo", +# "lastName":"Heinzen", +# "picture":"https://platform-lookaside.fbsbx.com/platform/profilepic/?eai=AXEMHxV_i8n2M787_v-PphBIRvN70e50_ZaJaygq0OQNIPH7-sfrUhrglqdj5eLWYJ23wF8DG84M&psid=7136628119690362&width=1024&ext=1714343887&hash=AfplUu_FmSY14ettnD4O4XLRtMcAv7jy3Qp7ZQWQJLvZCw" +# }, +# "contents":[ +# { +# "type":"file", +# "fileUrl":"https://cdn.fbsbx.com/v/t59.2708-21/434485005_930612625522428_5224348991750059641_n.pdf/ACFrOgBpkv2Rqm98YhldLaIfNbg45BYfTLeGSZtJLjOXbs0VB0pZImxnSOykt_yGe_GV8fDeOa1rDvDy9bxB32beMOBiQjcFFWHEGj0rqfCVK6RkKUdKFZ2EehpJGKf18fWyornPsoJo1-lnpFPH.pdf?_nc_cat=101&ccb=1-7&_nc_sid=2b0e22&_nc_ohc=rsmp8Hn2sogAX-QUEHb&_nc_ht=cdn.fbsbx.com&oh=03_AdR6tStc0_BdouhOMftMaz7QOOWYkdOKcvQw8OuAmRAg-A&oe=6609BE0E", +# "fileMimeType":"application/pdf", +# "fileName":"ACFrOgBpkv2Rqm98YhldLaIfNbg45BYfTLeGSZtJLjOXbs0VB0pZImxnSOykt_yGe_GV8fDeOa1rDvDy9bxB32beMOBiQjcFFWHEGj0rqfCVK6RkKUdKFZ2EehpJGKf18fWyornPsoJo1-lnpFPH.pdf" +# } +# ], +# "timestamp":"2024-03-30 11:45:31 am" +# }, +# "hub_access_token":"f20018fa-eb17-11ee-880c-0efa6ad28f4f.a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "controller":"webhooks/notifica_me", +# "action":"process_payload", +# "channel_id":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "notifica_me":{ +# "type":"MESSAGE", +# "id":"b35acb2b-f514-4265-bbeb-2c7b6a067317", +# "timestamp":"2024-03-30 11:45:31 am", +# "subscriptionId":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "channel":"facebook", +# "direction":"IN", +# "message":{ +# "id":"b35acb2b-f514-4265-bbeb-2c7b6a067317", +# "from":"7136628119690362", +# "to":"a852adee-f1f4-440e-9443-9ec21b0b3ed5", +# "direction":"IN", +# "channel":"facebook", +# "visitor":{ +# "name":"Clairton Rodrigo Heinzen", +# "firstName":"Clairton Rodrigo", +# "lastName":"Heinzen", +# "picture":"https://platform-lookaside.fbsbx.com/platform/profilepic/?eai=AXEMHxV_i8n2M787_v-PphBIRvN70e50_ZaJaygq0OQNIPH7-sfrUhrglqdj5eLWYJ23wF8DG84M&psid=7136628119690362&width=1024&ext=1714343887&hash=AfplUu_FmSY14ettnD4O4XLRtMcAv7jy3Qp7ZQWQJLvZCw" +# }, +# "contents":[ +# { +# "type":"file", +# "fileUrl":"https://cdn.fbsbx.com/v/t59.2708-21/434485005_930612625522428_5224348991750059641_n.pdf/ACFrOgBpkv2Rqm98YhldLaIfNbg45BYfTLeGSZtJLjOXbs0VB0pZImxnSOykt_yGe_GV8fDeOa1rDvDy9bxB32beMOBiQjcFFWHEGj0rqfCVK6RkKUdKFZ2EehpJGKf18fWyornPsoJo1-lnpFPH.pdf?_nc_cat=101&ccb=1-7&_nc_sid=2b0e22&_nc_ohc=rsmp8Hn2sogAX-QUEHb&_nc_ht=cdn.fbsbx.com&oh=03_AdR6tStc0_BdouhOMftMaz7QOOWYkdOKcvQw8OuAmRAg-A&oe=6609BE0E", +# "fileMimeType":"application/pdf", +# "fileName":"ACFrOgBpkv2Rqm98YhldLaIfNbg45BYfTLeGSZtJLjOXbs0VB0pZImxnSOykt_yGe_GV8fDeOa1rDvDy9bxB32beMOBiQjcFFWHEGj0rqfCVK6RkKUdKFZ2EehpJGKf18fWyornPsoJo1-lnpFPH.pdf" +# } +# ], +# "timestamp":"2024-03-30 11:45:31 am" +# } +# } +# } + +# { +# "type":"MESSAGE", +# "id":"66ba7478-9a4b-445e-8586-8e5e8ca848b4", +# "timestamp":"2024-04-10 06:44:39 pm", +# "subscriptionId":"b9fc5428-a397-44f1-9826-18097000e6e8", +# "channel":"telegram", +# "direction":"IN", +# "message":{ +# "id":"66ba7478-9a4b-445e-8586-8e5e8ca848b4", +# "from":"6917951225", +# "to":"b9fc5428-a397-44f1-9826-18097000e6e8", +# "direction":"IN", +# "channel":"telegram", +# "visitor":{ +# "name":"Silvia Heinzen", +# "firstName":"Silvia", +# "lastName":"Heinzen", +# "picture":"" +# }, +# "group":{ +# "id":"", +# "name":"" +# }, +# "isGroup":"false", +# "contents":[ +# { +# "type":"text", +# "text":"Oi" +# } +# ], +# "timestamp":"2024-04-10 06:44:39 pm" +# }, +# "hub_access_token":"f20018fa-eb17-11ee-880c-0efa6ad28f4f.b9fc5428-a397-44f1-9826-18097000e6e8", +# "controller":"webhooks/notifica_me", +# "action":"process_payload", +# "channel_id":"b9fc5428-a397-44f1-9826-18097000e6e8", +# "notifica_me":{ +# "type":"MESSAGE", +# "id":"66ba7478-9a4b-445e-8586-8e5e8ca848b4", +# "timestamp":"2024-04-10 06:44:39 pm", +# "subscriptionId":"b9fc5428-a397-44f1-9826-18097000e6e8", +# "channel":"telegram", +# "direction":"IN", +# "message":{ +# "id":"66ba7478-9a4b-445e-8586-8e5e8ca848b4", +# "from":"6917951225", +# "to":"b9fc5428-a397-44f1-9826-18097000e6e8", +# "direction":"IN", +# "channel":"telegram", +# "visitor":{ +# "name":"Silvia Heinzen", +# "firstName":"Silvia", +# "lastName":"Heinzen", +# "picture":"" +# }, +# "group":{ +# "id":"", +# "name":"" +# }, +# "isGroup":"false", +# "contents":[ +# { +# "type":"text", +# "text":"Oi" +# } +# ], +# "timestamp":"2024-04-10 06:44:39 pm" +# } +# } +# } + +# { +# "type":"MESSAGE_STATUS", +# "timestamp":"2024-09-14 11:37:34 pm", +# "subscriptionId":"db3abe8f-1b4e-4523-a8af-48a69e18d283", +# "channel":"mercadolivre", +# "messageId":"f1948709-a8de-4915-a9a2-ac76a05e03ea", +# "contentIndex":0, +# "messageStatus":{ +# "timestamp":"2024-09-14 11:37:34 pm", +# "code":"REJECTED", +# "description":"The message was rejected by the provider", +# "error":{ +# "code":400, +# "message":"Item must be active" +# } +# }, +# "hub_access_token":"84ce7e18-6ea4-11ee-a8c4-02e634721ec1.db3abe8f-1b4e-4523-a8af-48a69e18d283", +# "controller":"webhooks/notifica_me", +# "action":"process_payload", +# "channel_id":"db3abe8f-1b4e-4523-a8af-48a69e18d283", +# "notifica_me":{ +# "type":"MESSAGE_STATUS", +# "timestamp":"2024-09-14 11:37:34 pm", +# "subscriptionId":"db3abe8f-1b4e-4523-a8af-48a69e18d283", +# "channel":"mercadolivre", +# "messageId":"f1948709-a8de-4915-a9a2-ac76a05e03ea", +# "contentIndex":0, +# "messageStatus":{ +# "timestamp":"2024-09-14 11:37:34 pm", +# "code":"REJECTED", +# "description":"The message was rejected by the provider", +# "error":{ +# "code":400, +# "message":"Item must be active" +# } +# } +# }, +# "retried":true +# } + +class Webhooks::NotificaMeEventsJob < ApplicationJob + include ::FileTypeHelper + queue_as :default + + def perform(params = {}) + Rails.logger.error("NotificaMe params webhook #{params}") + channel = Channel::NotificaMe.find_by(notifica_me_id: params['channel_id']) + unless channel + Rails.logger.warn("NotificaMe Channel #{params['channel_id']} not found") + return + end + + process_event_params(channel, params) + end + + private + + def process_event_params(channel, params) + if params['type'] == 'MESSAGE_STATUS' + messageStatus = params['messageStatus']['code'] + messageProviderId = params['messageStatus']['providerMessageId'] + messageId = params['messageId'] + source_id = %w[SENT REJECTED ERROR].include?(messageStatus) ? messageId : messageProviderId + Rails.logger.warn("NotificaMe Message source id #{source_id} for status #{messageStatus}") + message = Message.find_by(source_id: source_id) + unless message + if params['retried'] + Rails.logger.warn("NotificaMe Message source id #{source_id} not found") + else + Rails.logger.warn("NotificaMe Message source id #{source_id} not found try again") + params['retried'] = true + time = messageStatus == 'SENT' ? 3 : 5 + Webhooks::NotificaMeEventsJob.set(wait: time.seconds).perform_later(params) + end + return + end + index = Message.statuses[message.status] + if %w[ERROR REJECTED].include?(messageStatus) + Rails.logger.warn("NotificaMe Message source id #{source_id} update to failed") + error = (params['messageStatus']['error'] && params['messageStatus']['error']['message']) || params['messageStatus']['description'] + message.update!(status: :failed, external_error: error) + elsif messageStatus == 'SENT' + Rails.logger.warn("NotificaMe Message source id #{source_id} update to sent current index #{index} compare #{Message.statuses[:sent]}") + attrs = { status: :sent } + attrs[:source_id] = messageProviderId if messageProviderId + message.update!(attrs) if index < Message.statuses[:sent] || message.status == :failed + elsif messageStatus == 'DELIVERED' + Rails.logger.warn("NotificaMe Message source id #{source_id} update to delivered current index #{index} compare #{Message.statuses[:delivered]}") + message.update!(status: :delivered) if index < Message.statuses[:delivered] || message.status == :failed + elsif messageStatus == 'READ' + Rails.logger.warn("NotificaMe Message source id #{source_id} update to read current index #{index} compare #{Message.statuses[:read]}") + message.update!(status: :read) if index < Message.statuses[:read] || message.status == :failed + end + elsif params['type'] == 'MESSAGE' + return if params['direction'] == 'OUT' + + message = params['message'] + visitor = message['visitor'] + contact_inbox = ::ContactInboxWithContactBuilder.new( + source_id: message['from'], + inbox: channel.inbox, + contact_attributes: { + phone_number: visitor[:phone_number], + email: visitor[:email], + name: visitor[:name], + avatar_url: visitor[:picture] + } + ).perform + + timestamp = message[:timestamp] ? message[:timestamp].to_i : Time.now.to_i + conversation = find_or_create_conversation(contact_inbox) + ActiveRecord::Base.transaction do # rubocop:disable Metrics/BlockLength + ms = message['contents'].map do |c| # rubocop:disable Metrics/BlockLength + content = c[c['type']] || '' + m = conversation.messages.build( + content: content, + account_id: contact_inbox.inbox.account_id, + inbox_id: contact_inbox.inbox.id, + message_type: :incoming, + content_type: :text, + sender: contact_inbox.contact, + source_id: message['id'], + status: :progress, + created_at: timestamp + ) + + if c['type'] != 'text' + attachment_file = nil + if ['whatsapp_business_account'].include?(channel.notifica_me_type) # rubocop:disable Performance/CollectionLiteralInLoop + url = 'https://hub.notificame.com.br/v1/channels/whatsapp/media' + headers = { + 'X-API-Token' => channel.notifica_me_token, + 'Content-Type' => 'application/json' + } + body = { + from: channel.notifica_me_id, + contents: [{ + type: c['type'], + fileMimeType: c['fileMimeType'], + fileUrl: c['fileUrl'] + }] + } + response = HTTParty.post( + url, + body: JSON.dump(body), + headers: headers + ) + tempfile = Tempfile.new('notificamehub', binmode: true) + File.binwrite(tempfile.path, response.body) + attachment_file = tempfile + else + attachment_file = Down.download(c['fileUrl']) + end + + content_type = c['fileMimeType'] + content_type = 'audio/mp4' if c['type'] == 'audio' && (content_type == 'video/mp4') + + if attachment_file.present? + a = m.attachments.new( + account_id: contact_inbox.inbox.account_id, + file_type: c['type'], + file: { + io: attachment_file, + filename: c['fileName'] || c['type'], + content_type: content_type + } + ) + a.save! + end + end + m.save! + m + end + ms + rescue StandardError => e + Rails.logger.error("NotificaMe channel create message error#{e}, #{e.backtrace}") + raise ActiveRecord::Rollback + end + end + end + + def find_or_create_conversation(contact_inbox) + conversation = if contact_inbox.inbox.lock_to_single_conversation + contact_inbox.conversations.last + else + contact_inbox.conversations.where.not(status: :resolved).last + end + + conversation || ::Conversation.create!( + { + account_id: contact_inbox.inbox.account_id, + inbox_id: contact_inbox.inbox.id, + contact_id: contact_inbox.contact.id, + contact_inbox_id: contact_inbox.id + } + ) + end +end diff --git a/app/jobs/webhooks/whatsapp_events_job.rb b/app/jobs/webhooks/whatsapp_events_job.rb index b84e9cde50b4b..aea105f8e6b60 100644 --- a/app/jobs/webhooks/whatsapp_events_job.rb +++ b/app/jobs/webhooks/whatsapp_events_job.rb @@ -1,5 +1,6 @@ class Webhooks::WhatsappEventsJob < ApplicationJob queue_as :low + retry_on ActiveRecord::RecordNotFound, wait: 30.seconds, attempts: 5 def perform(params = {}) channel = find_channel_from_whatsapp_business_payload(params) @@ -8,6 +9,8 @@ def perform(params = {}) case channel.provider when 'whatsapp_cloud' Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params).perform + when 'unoapi' + Whatsapp::IncomingMessageUnoapiService.new(inbox: channel.inbox, params: params).perform else Whatsapp::IncomingMessageService.new(inbox: channel.inbox, params: params).perform end diff --git a/app/listeners/automation_rule_listener.rb b/app/listeners/automation_rule_listener.rb index 5dabda47e34dc..f510d8c95a450 100644 --- a/app/listeners/automation_rule_listener.rb +++ b/app/listeners/automation_rule_listener.rb @@ -1,53 +1,18 @@ class AutomationRuleListener < BaseListener def conversation_updated(event) - return if performed_by_automation?(event) - - conversation = event.data[:conversation] - account = conversation.account - changed_attributes = event.data[:changed_attributes] - - return unless rule_present?('conversation_updated', account) - - rules = current_account_rules('conversation_updated', account) - - rules.each do |rule| - conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation, { changed_attributes: changed_attributes }).perform - AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present? - end + conversation_event(event, 'conversation_updated') end def conversation_created(event) - return if performed_by_automation?(event) - - conversation = event.data[:conversation] - account = conversation.account - changed_attributes = event.data[:changed_attributes] - - return unless rule_present?('conversation_created', account) - - rules = current_account_rules('conversation_created', account) - - rules.each do |rule| - conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation, { changed_attributes: changed_attributes }).perform - ::AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present? - end + conversation_event(event, 'conversation_created') end def conversation_opened(event) - return if performed_by_automation?(event) - - conversation = event.data[:conversation] - account = conversation.account - changed_attributes = event.data[:changed_attributes] - - return unless rule_present?('conversation_opened', account) - - rules = current_account_rules('conversation_opened', account) + conversation_event(event, 'conversation_opened') + end - rules.each do |rule| - conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation, { changed_attributes: changed_attributes }).perform - AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present? - end + def conversation_resolved(event) + conversation_event(event, 'conversation_resolved') end def message_created(event) @@ -91,4 +56,23 @@ def ignore_message_created_event?(event) message = event.data[:message] performed_by_automation?(event) || message.activity? end + + private + + def conversation_event(event, conversation_status) + return if performed_by_automation?(event) + + conversation = event.data[:conversation] + account = conversation.account + changed_attributes = event.data[:changed_attributes] + + return unless rule_present?(conversation_status, account) + + rules = current_account_rules(conversation_status, account) + + rules.each do |rule| + conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation, { changed_attributes: changed_attributes }).perform + AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present? + end + end end diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb index 3fdd823b86e4c..bb980516a6d23 100644 --- a/app/listeners/webhook_listener.rb +++ b/app/listeners/webhook_listener.rb @@ -100,8 +100,23 @@ def deliver_api_inbox_webhooks(payload, inbox) WebhookJob.perform_later(inbox.channel.webhook_url, payload, :api_inbox_webhook) end + def deliver_whatsapp_inbox_webhooks(payload, inbox) + payload = payload.deep_symbolize_keys + + return unless inbox.channel_type == 'Channel::Whatsapp' && payload[:event] == 'message_updated' && payload[:message_type] == 'incoming' + + WebhookJob.perform_later( + inbox.channel.message_path(payload), + inbox.channel.message_update_payload(payload), + :account_webhook, + inbox.channel.message_update_http_method, + inbox.channel.api_headers + ) + end + def deliver_webhook_payloads(payload, inbox) deliver_account_webhooks(payload, inbox.account) deliver_api_inbox_webhooks(payload, inbox) + deliver_whatsapp_inbox_webhooks(payload, inbox) end end diff --git a/app/models/account.rb b/app/models/account.rb index 419ccc471d0bc..1f47afa0a98cb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -74,6 +74,7 @@ class Account < ApplicationRecord has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget' has_many :webhooks, dependent: :destroy_async has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp' + has_many :notifica_me_channels, dependent: :destroy_async, class_name: '::Channel::NotificaMe' has_many :working_hours, dependent: :destroy_async has_one_attached :contacts_export diff --git a/app/models/campaign.rb b/app/models/campaign.rb index 9898699ab46a9..f900d5587902d 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -48,7 +48,7 @@ class Campaign < ApplicationRecord has_many :conversations, dependent: :nullify, autosave: true before_validation :ensure_correct_campaign_attributes - after_commit :set_display_id, unless: :display_id? + after_create_commit :load_attributes_created_by_db_triggers def trigger! return unless one_off? @@ -56,25 +56,41 @@ def trigger! Twilio::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Twilio SMS' Sms::OneoffSmsCampaignService.new(campaign: self).perform if inbox.inbox_type == 'Sms' + one_off_unoapi end private - def set_display_id - reload + def one_off_unoapi + return unless inbox.channel_type == 'Channel::Whatsapp' && inbox&.channel&.provider == 'unoapi' + + Whatsapp::OneoffUnoapiCampaignService.new(campaign: self).perform + end + + def load_attributes_created_by_db_triggers + # Display id is set via a trigger in the database + # So we need to specifically fetch it after the record is created + # We can't use reload because it will clear the previous changes, which we need for the dispatcher + obj_from_db = self.class.find(id) + self[:display_id] = obj_from_db[:display_id] + rescue ActiveRecord::RecordNotFound + Rails.logger.error("Ignore raise ActiveRecord::RecordNotFound on load campaign id #{id}") end def validate_campaign_inbox return unless inbox - errors.add :inbox, 'Unsupported Inbox type' unless ['Website', 'Twilio SMS', 'Sms'].include? inbox.inbox_type + errors.add :inbox, 'Unsupported Inbox type' unless ['Website', 'Twilio SMS', 'Sms', 'Whatsapp'].include? inbox.inbox_type + return unless inbox.inbox_type == 'Whatsapp' && inbox.channel.provider != 'unoapi' + + errors.add :inbox, 'Unsupported Whatsapp provider' end # TO-DO we clean up with better validations when campaigns evolve into more inboxes def ensure_correct_campaign_attributes return if inbox.blank? - if ['Twilio SMS', 'Sms'].include?(inbox.inbox_type) + if ['Twilio SMS', 'Sms', 'Whatsapp'].include?(inbox.inbox_type) self.campaign_type = 'one_off' self.scheduled_at ||= Time.now.utc else diff --git a/app/models/channel/notifica_me.rb b/app/models/channel/notifica_me.rb new file mode 100644 index 0000000000000..eb5e6a756b749 --- /dev/null +++ b/app/models/channel/notifica_me.rb @@ -0,0 +1,63 @@ +# == Schema Information +# +# Table name: channel_notifica_me +# +# id :bigint not null, primary key +# message_templates :jsonb not null +# message_templates_last_updated :datetime +# notifica_me_token :string not null +# notifica_me_type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# notifica_me_id :string not null +# +# Indexes +# +# index_channel_notifica_me (notifica_me_id,account_id) UNIQUE +# + +class Channel::NotificaMe < ApplicationRecord + include Channelable + + self.table_name = 'channel_notifica_me' + EDITABLE_ATTRS = [:notifica_me_id, :notifica_me_type, :notifica_me_token].freeze + + validates :notifica_me_id, presence: true + validates :notifica_me_type, presence: true + validates :notifica_me_token, presence: true + validates :notifica_me_id, uniqueness: { scope: :account_id } + + def name + 'NotificaMe' + end + + def whatsapp? + notifica_me_type == 'whatsapp_business_account' + end + + def mercado_livre? + notifica_me_type == 'mercado_livre' + end + + def notifica_me_path + return 'whatsapp' if whatsapp? + return 'mercadolivre' if mercado_livre? + + notifica_me_type + end + + def sync_templates + url = "http://hub.notificame.com.br/v1/templates/#{notifica_me_id}" + response = HTTParty.get( + url, + headers: { + 'X-API-Token' => notifica_me_token, + 'Content-Type' => 'application/json' + }, + format: :json + ) + templates = response.parsed_response['data'] || {} + update(message_templates: templates, message_templates_last_updated: Time.now.utc) if templates.present? + end +end diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index f95c2c30aa7cf..ad28302f589fa 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -25,7 +25,7 @@ class Channel::Whatsapp < ApplicationRecord EDITABLE_ATTRS = [:phone_number, :provider, { provider_config: {} }].freeze # default at the moment is 360dialog lets change later. - PROVIDERS = %w[default whatsapp_cloud].freeze + PROVIDERS = %w[default whatsapp_cloud unoapi].freeze before_validation :ensure_webhook_verify_token validates :provider, inclusion: { in: PROVIDERS } @@ -41,13 +41,15 @@ def name def provider_service if provider == 'whatsapp_cloud' Whatsapp::Providers::WhatsappCloudService.new(whatsapp_channel: self) + elsif provider == 'unoapi' + Whatsapp::Providers::UnoapiService.new(whatsapp_channel: self) else Whatsapp::Providers::Whatsapp360DialogService.new(whatsapp_channel: self) end end def messaging_window_enabled? - true + provider_config['url'] == 'https://graph.facebook.com' end def mark_message_templates_updated @@ -61,14 +63,21 @@ def mark_message_templates_updated delegate :sync_templates, to: :provider_service delegate :media_url, to: :provider_service delegate :api_headers, to: :provider_service + delegate :message_path, to: :provider_service + delegate :message_update_payload, to: :provider_service + delegate :message_update_http_method, to: :provider_service private def ensure_webhook_verify_token - provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider == 'whatsapp_cloud' + provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if %w[whatsapp_cloud unoapi].include?(provider) end def validate_provider_config errors.add(:provider_config, 'Invalid Credentials') unless provider_service.validate_provider_config? + rescue HTTParty::Error => e + errors.add(:provider_config, e.message) + rescue SocketError, Errno::ECONNREFUSED + errors.add(:provider_config, 'Conection refused, verify Whatsapp Cloud API URL field') end end diff --git a/app/models/concerns/brazilian_number_validator.rb b/app/models/concerns/brazilian_number_validator.rb new file mode 100644 index 0000000000000..f4bd5b6659038 --- /dev/null +++ b/app/models/concerns/brazilian_number_validator.rb @@ -0,0 +1,13 @@ +require 'phonelib' + +class BrazilianNumberValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if value.blank? + + phone = Phonelib.parse(value) + + return if phone.country != 'BR' || phone.valid? || (phone.type == :mobile && phone.local_number.scan(/\d/).join.length = 11) + + record.errors.add(attribute, options[:message] || I18n.t('errors.contacts.phone_number.invalid')) + end +end diff --git a/app/models/contact.rb b/app/models/contact.rb index c79dd3e9ea5da..d4f641953fe3f 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -49,7 +49,8 @@ class Contact < ApplicationRecord validates :identifier, allow_blank: true, uniqueness: { scope: [:account_id] } validates :phone_number, allow_blank: true, uniqueness: { scope: [:account_id] }, - format: { with: /\+[1-9]\d{1,14}\z/, message: I18n.t('errors.contacts.phone_number.invalid') } + format: { with: /\+[1-9]\d{1,14}\z/, message: I18n.t('errors.contacts.phone_number.invalid') }, + brazilian_number: true belongs_to :account has_many :conversations, dependent: :destroy_async diff --git a/app/models/contact_inbox.rb b/app/models/contact_inbox.rb index 6d034880e2d41..9448cb6848545 100644 --- a/app/models/contact_inbox.rb +++ b/app/models/contact_inbox.rb @@ -67,7 +67,7 @@ def validate_twilio_source_id end def validate_whatsapp_source_id - return if WHATSAPP_CHANNEL_REGEX.match?(source_id) + return if source_id.include?('@g.us') || WHATSAPP_CHANNEL_REGEX.match?(source_id) errors.add(:source_id, "invalid source id for whatsapp inbox. valid Regex #{WHATSAPP_CHANNEL_REGEX}") end diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 8158ab733e9f8..439b68bba6541 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -124,6 +124,10 @@ def whatsapp? channel_type == 'Channel::Whatsapp' end + def notifica_me? + channel_type == 'Channel::NotificaMe' + end + def assignable_agents (account.users.where(id: members.select(:user_id)) + account.administrators).uniq end @@ -162,6 +166,8 @@ def callback_webhook_url "#{ENV.fetch('FRONTEND_URL', nil)}/webhooks/line/#{channel.line_channel_id}" when 'Channel::Whatsapp' "#{ENV.fetch('FRONTEND_URL', nil)}/webhooks/whatsapp/#{channel.phone_number}" + when 'Channel::NotificaMe' + "#{ENV.fetch('FRONTEND_URL', nil)}/webhooks/notifica_me/#{channel.notifica_me_id}" end end diff --git a/app/models/macro.rb b/app/models/macro.rb index 64065a3421dd7..a27f758f85701 100644 --- a/app/models/macro.rb +++ b/app/models/macro.rb @@ -30,7 +30,7 @@ class Macro < ApplicationRecord validate :json_actions_format - ACTIONS_ATTRS = %w[send_message add_label assign_team assign_agent mute_conversation change_status remove_label remove_assigned_team + ACTIONS_ATTRS = %w[send_message add_label assign_team assign_agent mute_conversation change_status remove_label remove_assigned_team send_webhook_event resolve_conversation snooze_conversation change_priority send_email_transcript send_attachment add_private_note].freeze def set_visibility(user, params) diff --git a/app/models/message.rb b/app/models/message.rb index 6244d050ca532..6fa8b4ddb3d45 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -20,7 +20,7 @@ # conversation_id :integer not null # inbox_id :integer not null # sender_id :bigint -# source_id :string +# source_id :string(510) # # Indexes # @@ -74,6 +74,7 @@ class Message < ApplicationRecord validates :content_type, presence: true validates :content, length: { maximum: 150_000 } + validates :source_id, length: { maximum: 510 } validates :processed_message_content, length: { maximum: 150_000 } # when you have a temperory id in your frontend and want it echoed back via action cable @@ -94,7 +95,7 @@ class Message < ApplicationRecord integrations: 10, sticker: 11 } - enum status: { sent: 0, delivered: 1, read: 2, failed: 3 } + enum status: { progress: -1, sent: 0, delivered: 1, read: 2, failed: 3 } # [:submitted_email, :items, :submitted_values] : Used for bot message types # [:email] : Used by conversation_continuity incoming email messages # [:in_reply_to] : Used to reply to a particular tweet in threads @@ -108,6 +109,11 @@ class Message < ApplicationRecord store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id scope :created_since, ->(datetime) { where('created_at > ?', datetime) } + # .succ is a hack to avoid https://makandracards.com/makandra/1057-why-two-ruby-time-objects-are-not-equal-although-they-appear-to-be + scope :unread_since, ->(datetime) { where('EXTRACT(EPOCH FROM created_at) > (?)', datetime.to_i.succ) } + scope :to_read, lambda { |datetime| + where('EXTRACT(EPOCH FROM updated_at) <= (?) and message_type = 0 and status < 2', datetime.to_i.succ) + } scope :chat, -> { where.not(message_type: :activity).where(private: false) } scope :non_activity_messages, -> { where.not(message_type: :activity).reorder('id desc') } scope :today, -> { where("date_trunc('day', created_at) = ?", Date.current) } @@ -170,6 +176,10 @@ def merge_sender_attributes(data) data end + def sender_name + sender&.try(:available_name) || sender&.try(:name) + end + def webhook_data data = { account: account.webhook_data, @@ -182,6 +192,7 @@ def webhook_data id: id, inbox: inbox.webhook_data, message_type: message_type, + status: status, private: private, sender: sender.try(:webhook_data), source_id: source_id @@ -318,6 +329,14 @@ def dispatch_update_event Rails.configuration.dispatcher.dispatch(MESSAGE_UPDATED, Time.zone.now, message: self, performed_by: Current.executed_by, previous_changes: previous_changes) + + if additional_attributes['campaign_id'].present? + CampaignMessageUpdateJob.perform_later( + additional_attributes['campaign_id'], + additional_attributes['audience_id'], + status + ) + end end def send_reply diff --git a/app/services/contacts/contactable_inboxes_service.rb b/app/services/contacts/contactable_inboxes_service.rb index c5cde516fbefa..8a92b1cdae38d 100644 --- a/app/services/contacts/contactable_inboxes_service.rb +++ b/app/services/contacts/contactable_inboxes_service.rb @@ -22,9 +22,18 @@ def get_contactable_inbox(inbox) api_contactable_inbox(inbox) when 'Channel::WebWidget' website_contactable_inbox(inbox) + when 'Channel::NotificaMe' + notifica_me_contactable_inbox(inbox) end end + def notifica_me_contactable_inbox(inbox) + source_id = @contact.additional_attributes[inbox.channel.notifica_me_type] || @contact.phone_number + return unless source_id + + { source_id: source_id, inbox: inbox } + end + def website_contactable_inbox(inbox) latest_contact_inbox = inbox.contact_inboxes.where(contact: @contact).last return unless latest_contact_inbox diff --git a/app/services/macros/execution_service.rb b/app/services/macros/execution_service.rb index 81da53fca1802..30ac9a3b4a28d 100644 --- a/app/services/macros/execution_service.rb +++ b/app/services/macros/execution_service.rb @@ -22,6 +22,11 @@ def perform private + def send_webhook_event(webhook_url) + payload = @conversation.webhook_data.merge(event: "macro_event.#{@macro.name}") + WebhookJob.perform_later(webhook_url[0], payload) + end + def assign_agent(agent_ids) agent_ids = agent_ids.map { |id| id == 'self' ? @user.id : id } super(agent_ids) diff --git a/app/services/notifica_me/send_on_notifica_me_service.rb b/app/services/notifica_me/send_on_notifica_me_service.rb new file mode 100644 index 0000000000000..1ecd5ae489391 --- /dev/null +++ b/app/services/notifica_me/send_on_notifica_me_service.rb @@ -0,0 +1,105 @@ +class NotificaMe::SendOnNotificaMeService < Base::SendOnChannelService + private + + def channel_class + Channel::NotificaMe + end + + def perform_reply + begin + url = "https://hub.notificame.com.br/v1/channels/#{channel.notifica_me_path}/messages" + body = message_params.to_json + Rails.logger.error("NotificaMe message params #{body}") + response = HTTParty.post( + url, + body: body, + headers: { + 'X-API-Token' => channel.notifica_me_token, + 'Content-Type' => 'application/json' + }, + format: :json + ) + Rails.logger.error("Response from NotificaMe #{response}, code: #{response.code}") + if response.success? + # {"error":{"message":"Unknown path components: ","type":"OAuthException","code":"Hub404"}} + if response.parsed_response['error'] + error = "message: #{response.parsed_response['error']['message']}" + error = "#{error}, code: #{response.parsed_response['error']['code']}" + error = "#{error}, type: #{response.parsed_response['error']['type']}" + message.update!(status: :failed, external_error: error) + else + message.update!(source_id: response.parsed_response['id']) + end + else + raise "Error on send mensagem to NotificaMe: #{response.parsed_response}" + end + rescue StandardError => e + Rails.logger.error('Error on send do NotificaMe') + Rails.logger.error(e) + message.update!(status: :failed, external_error: e.message) + end + end + + def message_params + contents = message.attachments.length > 0 ? message_params_media : message_params_text + { + from: channel.notifica_me_id, + to: contact_inbox.source_id, + contents: contents + } + end + + def message_params_text + [ + { + type: :text, + text: message.content || '' + } + ] + end + + def message_params_media + message.attachments.map { |a| + file_type = file_type(a) + data = { + type: :file, + fileMimeType: file_type, + fileUrl: a.download_url + } + if message.content + data[:fileCaption] = message.content + end + + data + } + end + + def inbox + @inbox ||= message.inbox + end + + def channel + @channel ||= inbox.channel + end + + def file_type(attachment) + if attachment.file_type == 'image' + return 'photo' if channel.notifica_me_type == 'telegram' + elsif attachment.file_type == 'file' + extension = extension(attachment.download_url) + if ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'csv', 'txt'].include?(extension) + return 'document' + elsif attachment.file_type == 'file' && ['mov', 'mp4'].include?(extension) + return 'video' + elsif attachment.file_type == 'file' && ['ogg', 'mp3', 'wav'].include?(extension) + return 'audio' + end + end + return attachment.file_type + end + + def extension(url) + split = url.split('.') + split[split.length - 1] + end +end diff --git a/app/services/notifica_me/webhook_setup_service.rb b/app/services/notifica_me/webhook_setup_service.rb new file mode 100644 index 0000000000000..8ffaf47e34897 --- /dev/null +++ b/app/services/notifica_me/webhook_setup_service.rb @@ -0,0 +1,36 @@ +class NotificaMe::WebhookSetupService + include Rails.application.routes.url_helpers + + pattr_initialize [:inbox!] + + def perform + url = 'https://hub.notificame.com.br/v1/subscriptions' + callback_webhook_url = inbox.callback_webhook_url + Rails.logger.debug("NotificaMe webhook_url #{callback_webhook_url} criteria channel #{channel.notifica_me_id}") + response = HTTParty.post( + url, + body: { + webhook: { + url: inbox.callback_webhook_url + }, + criteria: { + channel: channel.notifica_me_id + } + }.to_json, + headers: { + 'X-API-Token' => channel.notifica_me_token, + 'Content-Type' => 'application/json' + } + ) + + if !response.success? + raise "Error on setup NotificaMe webhook: #{response.parsed_response}" + end + end + + private + + def channel + @channel ||= inbox.channel + end +end diff --git a/app/services/whatsapp/incoming_message_base_service.rb b/app/services/whatsapp/incoming_message_base_service.rb index f057fadbd11b6..dd9b5abe3b408 100644 --- a/app/services/whatsapp/incoming_message_base_service.rb +++ b/app/services/whatsapp/incoming_message_base_service.rb @@ -4,6 +4,10 @@ class Whatsapp::IncomingMessageBaseService include ::Whatsapp::IncomingMessageServiceHelpers + # rubocop:disable Style/ClassVars + @@microsecond = 0 + # rubocop:enable Style/ClassVars + pattr_initialize [:inbox!, :params!] def perform @@ -29,12 +33,17 @@ def process_messages return if find_message_by_source_id(@processed_params[:messages].first[:id]) || message_under_process? cache_message_source_id_in_redis - set_contact - return unless @contact - set_conversation - create_messages - clear_message_source_id_from_redis + begin + set_message_type + set_contact + return clear_message_source_id_from_redis unless @contact + + set_conversation + create_messages + ensure + clear_message_source_id_from_redis + end end def process_statuses @@ -46,10 +55,15 @@ def process_statuses end def update_message_with_status(message, status) - message.status = status[:status] + if status[:status] == 'deleted' + message.assign_attributes(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true }) + else + message.status = status[:status] + end if status[:status] == 'failed' && status[:errors].present? error = status[:errors]&.first message.external_error = "#{error[:code]}: #{error[:title]}" + message.conversation.open! unless message.conversation.open? end message.save! end @@ -82,16 +96,18 @@ def set_contact contact_params = @processed_params[:contacts]&.first return if contact_params.blank? - waid = processed_waid(contact_params[:wa_id]) + waid = brazil_phone_number?(contact_params[:wa_id]) ? normalised_brazil_mobile_number(contact_params[:wa_id]) : contact_params[:wa_id] + waid = processed_waid(waid) contact_inbox = ::ContactInboxWithContactBuilder.new( source_id: waid, inbox: inbox, - contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" } + contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{waid}", avatar_url: contact_params.dig(:profile, :picture) } ).perform @contact_inbox = contact_inbox @contact = contact_inbox.contact + @sender = outgoing_message_type? ? nil : contact_inbox.contact end def set_conversation @@ -141,15 +157,18 @@ def attach_location end def create_message(message) + timestamp = message[:timestamp] ? Time.at(message[:timestamp].to_i, microsecond, :microsecond, in: 'UTC') : Time.current.utc @message = @conversation.messages.build( content: message_content(message), account_id: @inbox.account_id, inbox_id: @inbox.id, - message_type: :incoming, - sender: @contact, + message_type: @message_type, + sender: @sender, source_id: message[:id].to_s, + created_at: timestamp, in_reply_to_external_id: @in_reply_to_external_id ) + @message end def attach_contact(contact) @@ -164,4 +183,16 @@ def attach_contact(contact) ) end end + + def set_message_type + @message_type = :incoming + end + + def microsecond + # rubocop:disable Style/ClassVars + @@microsecond = 0 if @@microsecond > 999_999 + @@microsecond += 1 + @@microsecond + # rubocop:enable Style/ClassVars + end end diff --git a/app/services/whatsapp/incoming_message_service_helpers.rb b/app/services/whatsapp/incoming_message_service_helpers.rb index c5474314bc326..460ff45837226 100644 --- a/app/services/whatsapp/incoming_message_service_helpers.rb +++ b/app/services/whatsapp/incoming_message_service_helpers.rb @@ -30,7 +30,8 @@ def message_content(message) message.dig(:button, :text) || message.dig(:interactive, :button_reply, :title) || message.dig(:interactive, :list_reply, :title) || - message.dig(:name, :formatted_name) + message.dig(:name, :formatted_name) || + message.dig(message_type.to_sym, :caption) end def file_content_type(file_type) @@ -60,7 +61,7 @@ def normalised_brazil_mobile_number(phone_number) number = phone_number[4, phone_number.length - 4] normalised_number = "55#{ddd}#{number}" # insert 9 to convert the number to the new mobile number format - normalised_number = "55#{ddd}9#{number}" if normalised_number.length != 13 + normalised_number = "55#{ddd}9#{number}" if %w[6 7 8 9].include?(number[0]) && normalised_number.length != 13 normalised_number end diff --git a/app/services/whatsapp/incoming_message_unoapi_service.rb b/app/services/whatsapp/incoming_message_unoapi_service.rb new file mode 100644 index 0000000000000..6f911a4076b49 --- /dev/null +++ b/app/services/whatsapp/incoming_message_unoapi_service.rb @@ -0,0 +1,2 @@ +class Whatsapp::IncomingMessageUnoapiService < Whatsapp::IncomingMessageWhatsappCloudService +end diff --git a/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb b/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb index 97051a2cf7fea..747dc3d2e6b87 100644 --- a/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb +++ b/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb @@ -4,6 +4,30 @@ class Whatsapp::IncomingMessageWhatsappCloudService < Whatsapp::IncomingMessageBaseService private + def set_contact + contact_params = @processed_params[:contacts]&.first + return if contact_params.blank? + + super + + return unless group_message? + + @sender = outgoing_message_type? ? nil : @contact + + contact_inbox = ::ContactInboxWithContactBuilder.new( + source_id: contact_params[:group_id], + inbox: inbox, + contact_attributes: { + email: contact_params[:group_id], + name: contact_params[:group_subject] || contact_params[:group_id], + avatar_url: contact_params[:group_picture] + } + ).perform + + @contact_inbox = contact_inbox + @contact = contact_inbox.contact + end + def processed_params @processed_params ||= params[:entry].try(:first).try(:[], 'changes').try(:first).try(:[], 'value') end @@ -14,4 +38,39 @@ def download_attachment_file(attachment_payload) inbox.channel.authorization_error! if url_response.unauthorized? Down.download(url_response.parsed_response['url'], headers: inbox.channel.api_headers) if url_response.success? end + + def message_content(message) + content = super(message) + group_message? && !outgoing_message_type? && @sender ? "*#{@sender.name}*: #{content}" : content + end + + def group_message? + contact_params = @processed_params[:contacts]&.first + contact_params.present? && contact_params[:group_id].present? + end + + def set_message_type + @message_type = :activity + return if activity_message_type? + + @message_type = outgoing_message_type? ? :outgoing : :incoming + end + + def outgoing_message_type? + message = @processed_params[:messages]&.first + return if message.blank? + + message[:from] == @processed_params['metadata']['display_phone_number'].sub('+', '') + end + + def activity_message_type? + message = @processed_params[:messages]&.first + return if message.blank? + + contact_params = @processed_params[:contacts]&.first + return if contact_params.blank? + + !group_message? && + @processed_params['metadata']['display_phone_number'].sub('+', '') == contact_params[:wa_id] && contact_params[:wa_id] == message[:from] + end end diff --git a/app/services/whatsapp/oneoff_unoapi_campaign_service.rb b/app/services/whatsapp/oneoff_unoapi_campaign_service.rb new file mode 100644 index 0000000000000..c957e9ed6f80d --- /dev/null +++ b/app/services/whatsapp/oneoff_unoapi_campaign_service.rb @@ -0,0 +1,49 @@ +class Whatsapp::OneoffUnoapiCampaignService + pattr_initialize [:campaign!] + + def perform + raise "Invalid campaign #{campaign.id}" if inbox.inbox_type != 'Whatsapp' || channel.provider != 'unoapi' || !campaign.one_off? + raise 'Completed Campaign' if campaign.completed? + + # marks campaign completed so that other jobs won't pick it up + campaign.completed! + + process_audience(campaign.audience) + end + + private + + delegate :inbox, to: :campaign + delegate :channel, to: :inbox + + def process_audience(audience) + Rails.logger.debug { "Process campaign #{campaign.id} and #{audience.length} audience record(s)" } + interval = 0 + new_audience = audience.map do |a| + aa = update_audience(a.symbolize_keys) + interval = schedule_job(campaign, aa, interval) if aa[:status] == :scheduled + aa + end + # rubocop:disable Rails/SkipsModelValidations + campaign.update_column(:audience, new_audience) + # rubocop:enable Rails/SkipsModelValidations + end + + def update_audience(audience) + audience[:status] = audience[:phone_number].present? ? :scheduled : :error + audience[:audience_id] = audience[:audience_id] || SecureRandom.uuid + audience.symbolize_keys + end + + def schedule_job(campaign, audience, interval) + interval = audience[:wait_for_seconds] || (interval + rand(1..10)) + CampaignMessageJob.set(wait: interval.seconds).perform_later( + campaign.account_id, + campaign.inbox_id, + campaign.id, + campaign.message, + audience + ) + interval + end +end diff --git a/app/services/whatsapp/providers/base_service.rb b/app/services/whatsapp/providers/base_service.rb index 9923017ddfdff..c40af88aedb3b 100644 --- a/app/services/whatsapp/providers/base_service.rb +++ b/app/services/whatsapp/providers/base_service.rb @@ -19,6 +19,18 @@ def send_template(_phone_number, _template_info) raise 'Overwrite this method in child class' end + def message_update_payload(_message) + raise 'Overwrite this method in child class' + end + + def message_update_http_method + :put + end + + def message_path(_message) + raise 'Overwrite this method in child class' + end + def sync_template raise 'Overwrite this method in child class' end diff --git a/app/services/whatsapp/providers/unoapi_service.rb b/app/services/whatsapp/providers/unoapi_service.rb new file mode 100644 index 0000000000000..e15a86aa61b67 --- /dev/null +++ b/app/services/whatsapp/providers/unoapi_service.rb @@ -0,0 +1,6 @@ +class Whatsapp::Providers::UnoapiService < Whatsapp::Providers::WhatsappCloudService + def validate_provider_config? + url = "#{business_account_path}/message_templates?access_token=#{ENV.fetch('UNOAPI_AUTH_TOKEN', whatsapp_channel.provider_config['api_key'])}" + return Whatsapp::UnoapiWebhookSetupService.new.perform(whatsapp_channel) if HTTParty.get(url).success? + end +end diff --git a/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb b/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb index 66816290578a0..0b54d0486ffa4 100644 --- a/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb +++ b/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb @@ -9,7 +9,7 @@ def send_message(phone_number, message) end end - def send_template(phone_number, template_info) + def send_template(message, phone_number, template_info) response = HTTParty.post( "#{api_base_path}/messages", headers: api_headers, @@ -20,7 +20,7 @@ def send_template(phone_number, template_info) }.to_json ) - process_response(response) + process_response(message, response) end def sync_templates @@ -49,6 +49,14 @@ def media_url(media_id) "#{api_base_path}/media/#{media_id}" end + def message_update_payload(message) + { status: message[:status] } + end + + def message_path(message) + "#{api_base_path}/messages/#{message[:source_id]}" + end + private def api_base_path @@ -67,7 +75,7 @@ def send_text_message(phone_number, message) }.to_json ) - process_response(response) + process_response(message, response) end def send_attachment_message(phone_number, message) @@ -88,14 +96,15 @@ def send_attachment_message(phone_number, message) }.to_json ) - process_response(response) + process_response(message, response) end - def process_response(response) + def process_response(message, response) if response.success? response['messages'].first['id'] else Rails.logger.error response.body + message.update!(status: :failed, external_error: response.body) nil end end @@ -128,6 +137,6 @@ def send_interactive_text_message(phone_number, message) }.to_json ) - process_response(response) + process_response(message, response) end end diff --git a/app/services/whatsapp/providers/whatsapp_cloud_service.rb b/app/services/whatsapp/providers/whatsapp_cloud_service.rb index f1af38e36dda3..eb9eded4906ae 100644 --- a/app/services/whatsapp/providers/whatsapp_cloud_service.rb +++ b/app/services/whatsapp/providers/whatsapp_cloud_service.rb @@ -9,7 +9,7 @@ def send_message(phone_number, message) end end - def send_template(phone_number, template_info) + def send_template(message, phone_number, template_info) response = HTTParty.post( "#{phone_id_path}/messages", headers: api_headers, @@ -21,7 +21,7 @@ def send_template(phone_number, template_info) }.to_json ) - process_response(response) + process_response(message, response) end def sync_templates @@ -59,8 +59,31 @@ def media_url(media_id) "#{api_base_path}/v13.0/#{media_id}" end + def message_update_payload(message) + payload = { + messaging_product: 'whatsapp', + status: message[:status], + message_id: message[:source_id], + recipient_id: message[:sender][:phone_number] + } + if message[:conversation][:contact_inbox][:source_id].include?('@g.us') + payload.merge({ group_id: message[:conversation][:contact_inbox][:source_id] }) + end + payload + end + + def message_update_http_method + :post + end + + def message_path(_message) + messages_path + end + + private + def api_base_path - ENV.fetch('WHATSAPP_CLOUD_BASE_URL', 'https://graph.facebook.com') + whatsapp_channel.provider_config['url'] || ENV.fetch('WHATSAPP_CLOUD_BASE_URL', 'https://graph.facebook.com') end # TODO: See if we can unify the API versions and for both paths and make it consistent with out facebook app API versions @@ -68,24 +91,36 @@ def phone_id_path "#{api_base_path}/v13.0/#{whatsapp_channel.provider_config['phone_number_id']}" end + def messages_path + "#{phone_id_path}/messages" + end + def business_account_path "#{api_base_path}/v14.0/#{whatsapp_channel.provider_config['business_account_id']}" end def send_text_message(phone_number, message) response = HTTParty.post( - "#{phone_id_path}/messages", + messages_path, headers: api_headers, body: { messaging_product: 'whatsapp', context: whatsapp_reply_context(message), to: phone_number, - text: { body: message.content }, + text: { body: format_content(message) }, type: 'text' }.to_json ) - process_response(response) + process_response(message, response) + end + + def format_content(message) + feature = whatsapp_channel.inbox.account.feature_enabled?('send_agent_name_in_whatsapp_message') + config = whatsapp_channel.provider_config['send_agent_name'] + return message.content if !feature && !config + + message.sender_name&.present? ? "*#{message&.sender_name}*: #{message.content}" : message.content end def send_attachment_message(phone_number, message) @@ -108,14 +143,15 @@ def send_attachment_message(phone_number, message) }.to_json ) - process_response(response) + process_response(message, response) end - def process_response(response) + def process_response(message, response) if response.success? response['messages'].first['id'] else Rails.logger.error response.body + message.update!(status: :failed, external_error: response.body) nil end end @@ -157,6 +193,6 @@ def send_interactive_text_message(phone_number, message) }.to_json ) - process_response(response) + process_response(message, response) end end diff --git a/app/services/whatsapp/send_on_whatsapp_service.rb b/app/services/whatsapp/send_on_whatsapp_service.rb index 3d843e64b82e3..744221e05f8df 100644 --- a/app/services/whatsapp/send_on_whatsapp_service.rb +++ b/app/services/whatsapp/send_on_whatsapp_service.rb @@ -6,6 +6,8 @@ def channel_class end def perform_reply + return if message.message_type == :outgoing && message.source_id&.is_present? # is message send by own + should_send_template_message = template_params.present? || !message.conversation.can_reply? if should_send_template_message send_template_message @@ -19,7 +21,7 @@ def send_template_message return if name.blank? - message_id = channel.send_template(message.conversation.contact_inbox.source_id, { + message_id = channel.send_template(message, message.conversation.contact_inbox.source_id, { name: name, namespace: namespace, lang_code: lang_code, @@ -92,7 +94,13 @@ def validated_body_object(template) end def send_session_message - message_id = channel.send_message(message.conversation.contact_inbox.source_id, message) + uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + phone_number = if uuid_regex.match?(message.conversation.contact_inbox.source_id) + message.conversation.contact_inbox.contact.phone_number.sub('+', '') + else + message.conversation.contact_inbox.source_id + end + message_id = channel.send_message(phone_number, message) message.update!(source_id: message_id) if message_id.present? end diff --git a/app/services/whatsapp/unoapi_webhook_setup_service.rb b/app/services/whatsapp/unoapi_webhook_setup_service.rb new file mode 100644 index 0000000000000..aacf60ffcf6d9 --- /dev/null +++ b/app/services/whatsapp/unoapi_webhook_setup_service.rb @@ -0,0 +1,123 @@ +class Whatsapp::UnoapiWebhookSetupService + def perform(whatsapp_channel) + if whatsapp_channel.provider_config['disconnect'] + whatsapp_channel.provider_config.delete('connect') + whatsapp_channel.provider_config.delete('disconnect') + return disconnect(whatsapp_channel) + end + return true unless whatsapp_channel.provider_config['connect'] + + whatsapp_channel.provider_config.delete('connect') + whatsapp_channel.provider_config.delete('disconnect') + connect(whatsapp_channel) + end + + private + + def disconnect(whatsapp_channel) + phone_number = whatsapp_channel.provider_config['business_account_id'] + Rails.logger.debug { "Disconnecting #{phone_number} from unoapi" } + response = HTTParty.post("#{url(whatsapp_channel)}/deregister", headers: headers(whatsapp_channel)) + if response.success? + true + else + whatsapp_channel.errors.add(:provider_config, response.body) + false + end + end + + def connect(whatsapp_channel) + phone_number = whatsapp_channel.provider_config['business_account_id'] + url = url(whatsapp_channel) + Rails.logger.debug { "Connecting #{phone_number} from unoapi with url #{url}" } + body = params(whatsapp_channel.provider_config, phone_number) + response = HTTParty.post("#{url}/register", headers: headers(whatsapp_channel), body: body.to_json) + Rails.logger.debug { "Response #{response}" } + return send_message(whatsapp_channel) if response.success? + + whatsapp_channel.errors.add(:provider_config, response.body) + true + end + + def send_message(whatsapp_channel) + phone_number = whatsapp_channel.provider_config['business_account_id'] + Rails.logger.debug { "Save #{phone_number} configuration unoapi" } + body = { + messaging_product: :whatsapp, + to: phone_number, + type: :text, + text: { + body: 'connect...' + } + } + Rails.logger.debug { "Sending message to #{phone_number} unoapi" } + response = HTTParty.post("#{url(whatsapp_channel)}/messages", headers: headers(whatsapp_channel), body: body.to_json) + Rails.logger.debug { "Response #{response}" } + return true if response.success? + + whatsapp_channel.errors.add(:provider_config, response.body) + false + end + + def url(whatsapp_channel) + "#{whatsapp_channel.provider_config['url']}/v15.0/#{whatsapp_channel.provider_config['business_account_id']}" + end + + def headers(whatsapp_channel) + { + Authorization: ENV.fetch('UNOAPI_AUTH_TOKEN', whatsapp_channel.provider_config['api_key']), + 'Content-Type': 'application/json' + } + end + + # rubocop:disable Metrics/MethodLength + + # Função que adiciona webhooks sem duplicar + def create_webhook(provider_config, phone_number, id_base, url_base) + webhooks = provider_config['webhooks'] + + # Verifica duplicidade por id e urlAbsolute + id_exists = webhooks.any? { |wh| wh['id'] == id_base } + url_exists = webhooks.any? { |wh| wh['urlAbsolute'] == url_base } + + return if id_exists || url_exists # Evita duplicidade + + # Adiciona o webhook no provider_config + webhooks << { + sendNewMessages: provider_config['webhook_send_new_messages'] || true, + id: id_base, + urlAbsolute: url_base, + token: provider_config['webhook_verify_token'], + header: 'Authorization' + } + end + + # Exemplo de configuração para múltiplos webhooks + def params(provider_config, phone_number) + # Webhook padrão + create_webhook(provider_config, phone_number, 'default', "#{ENV.fetch('FRONTEND_URL', '')}/webhooks/whatsapp/#{phone_number}") + + # Webhook Typebot + # create_webhook(provider_config, phone_number, 'typebot', "#{ENV.fetch('FRONTEND_URL', '')}/webhooks/typebot/#{phone_number}") + + { + ignoreGroupMessages: provider_config['ignore_group_messages'], + ignoreBroadcastStatuses: provider_config['ignore_broadcast_statuses'], + ignoreBroadcastMessages: provider_config['ignore_broadcast_messages'], + ignoreHistoryMessages: provider_config['ignore_history_messages'], + ignoreOwnMessages: provider_config['ignore_own_messages'], + ignoreYourselfMessages: provider_config['ignore_yourself_messages'], + sendConnectionStatus: provider_config['send_connection_status'], + notifyFailedMessages: provider_config['notify_failed_messages'], + composingMessage: provider_config['composing_message'], + rejectCalls: provider_config['reject_calls'], + messageCallsWebhook: provider_config['message_calls_webhook'], + webhooks: provider_config['webhooks'], + sendReactionAsReply: provider_config['send_reaction_as_reply'], + sendProfilePicture: provider_config['send_profile_picture'], + authToken: provider_config['api_key'], + useRejectCalls: provider_config['use_reject_calls'] + } + end + # rubocop:enable Metrics/MethodLength +end diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index 88a029680b80a..ba9a8fab1fd99 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -108,3 +108,6 @@ if resource.whatsapp? json.message_templates resource.channel.try(:message_templates) json.provider_config resource.channel.try(:provider_config) if Current.account_user&.administrator? end + +### NotificaMe Channel +json.message_templates resource.channel.try(:message_templates) if resource.notifica_me? && resource.channel.try(:whatsapp?) diff --git a/app/views/installation/onboarding/index.html.erb b/app/views/installation/onboarding/index.html.erb index a6eb409934ef5..5d9740def2a50 100644 --- a/app/views/installation/onboarding/index.html.erb +++ b/app/views/installation/onboarding/index.html.erb @@ -1,7 +1,7 @@ - SuperAdmin | Chatwoot + <%= (@global_config && @global_config['INSTALLATION_NAME']) || 'SuperAdmin | Chatwoot' %> <%= javascript_pack_tag 'superadmin' %> <%= stylesheet_pack_tag 'superadmin' %> diff --git a/app/views/super_admin/settings/show.html.erb b/app/views/super_admin/settings/show.html.erb index db8db06e5a12d..45cfee0614030 100644 --- a/app/views/super_admin/settings/show.html.erb +++ b/app/views/super_admin/settings/show.html.erb @@ -50,7 +50,7 @@
- <% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %> + <% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %>

You have <%= User.count %> agents. Please add more licenses to add more users.

@@ -99,9 +99,9 @@ <% end %>
-

<%= attrs[:name] %>

+

<%= attrs[:name] %>

<% if attrs[:enterprise] %> - EE + EE <% end %> <% if attrs[:config_key].present? && attrs[:enabled] %> diff --git a/config/features.yml b/config/features.yml index 4f62694f8e3cc..274813e289d5a 100644 --- a/config/features.yml +++ b/config/features.yml @@ -9,6 +9,12 @@ help_url: https://chwt.app/hc/fb - name: channel_twitter enabled: true +- name: channel_whatsapp + enabled: false +- name: channel_api + enabled: false +- name: channel_notifica_me + enabled: false - name: ip_lookup enabled: false - name: disable_branding @@ -92,3 +98,21 @@ - name: custom_roles enabled: false premium: true +- name: hide_all_chats_for_agent + enabled: false +- name: hide_contacts_for_agent + enabled: false +- name: hide_filters_for_agent + enabled: false +- name: send_agent_name_in_whatsapp_message + enabled: true +- name: read_message + enabled: true +- name: disable_whatsapp_messaging_window + enabled: false +- name: agent_conversation_viewed + enabled: true +- name: hide_unassigned_for_agent + enabled: false +- name: hide_delete_message_for_agent + enabled: false diff --git a/config/installation_config.yml b/config/installation_config.yml index 26f05211eb4fd..0ab91eaf1e26f 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -233,3 +233,17 @@ display_title: 'Captain App URL' description: 'The App URL for Captain' ## ----- End of Captain Configs ----- ## + +## ------ Configs custom ------ ## +- name: CONVERSATION_STYLE_CSS + display_title: 'Conversation Style CSS' + value: + description: 'Customize conversation, put your valid css' +## ------ End of Configs custom ------ ## + +# MARK: UNOAPI Channel Config +- name: UNOAPI_AUTH_TOKEN + value: + display_title: 'UNO API Auth Token' + description: 'The Auth Token for UNO API. Find more details on how to configure UNOAPI here: https://github.com/clairton/unoapi-cloud/tree/main' +# End of UNOAPI Channel Config diff --git a/config/routes.rb b/config/routes.rb index 7006e8f4bc225..5235bc8ddda7a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,6 +79,9 @@ resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy] namespace :channels do resource :twilio_channel, only: [:create] + resource :notifica_me_channel, only: [:index, :create] do + get :index + end end resources :conversations, only: [:index, :create, :show, :update] do collection do @@ -439,6 +442,7 @@ post 'webhooks/twitter', to: 'api/v1/webhooks#twitter_events' post 'webhooks/line/:line_channel_id', to: 'webhooks/line#process_payload' post 'webhooks/telegram/:bot_token', to: 'webhooks/telegram#process_payload' + post 'webhooks/notifica_me/:channel_id', to: 'webhooks/notifica_me#process_payload' post 'webhooks/sms/:phone_number', to: 'webhooks/sms#process_payload' get 'webhooks/whatsapp/:phone_number', to: 'webhooks/whatsapp#verify' post 'webhooks/whatsapp/:phone_number', to: 'webhooks/whatsapp#process_payload' diff --git a/db/migrate/20240325215901_create_channel_notifica_me.rb b/db/migrate/20240325215901_create_channel_notifica_me.rb new file mode 100644 index 0000000000000..1dddb0532f23b --- /dev/null +++ b/db/migrate/20240325215901_create_channel_notifica_me.rb @@ -0,0 +1,14 @@ +class CreateChannelNotificaMe < ActiveRecord::Migration[7.0] + def change + create_table :channel_notifica_me do |t| + t.string :notifica_me_id, null: false + t.string :notifica_me_type, null: false + t.string :notifica_me_token, null: false + t.integer :account_id, null: false + + t.index [:notifica_me_id, :account_id], name: :index_channel_notifica_me, unique: true + + t.timestamps + end + end +end diff --git a/db/migrate/20240528173755_alter_message_source_id_length.rb b/db/migrate/20240528173755_alter_message_source_id_length.rb new file mode 100644 index 0000000000000..1a2c211d35e53 --- /dev/null +++ b/db/migrate/20240528173755_alter_message_source_id_length.rb @@ -0,0 +1,5 @@ +class AlterMessageSourceIdLength < ActiveRecord::Migration[7.0] + def change + change_column :messages, :source_id, :string, limit: 512 + end +end diff --git a/db/migrate/20240914115913_add_templates_to_notifica_me.rb b/db/migrate/20240914115913_add_templates_to_notifica_me.rb new file mode 100644 index 0000000000000..936960da49d53 --- /dev/null +++ b/db/migrate/20240914115913_add_templates_to_notifica_me.rb @@ -0,0 +1,6 @@ +class AddTemplatesToNotificaMe < ActiveRecord::Migration[7.0] + def change + add_column :channel_notifica_me, :message_templates, :jsonb, default: '{}', null: false + add_column :channel_notifica_me, :message_templates_last_updated, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 3ad9562b53011..795df82da9b1e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_07_26_220747) do +ActiveRecord::Schema[7.0].define(version: 2024_09_14_115913) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -21,8 +21,8 @@ t.string "owner_type" t.bigint "owner_id" t.string "token" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["owner_type", "owner_id"], name: "index_access_tokens_on_owner_type_and_owner_id" t.index ["token"], name: "index_access_tokens_on_token", unique: true end @@ -32,8 +32,8 @@ t.bigint "user_id" t.integer "role", default: 0 t.bigint "inviter_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.datetime "active_at", precision: nil t.integer "availability", default: 0, null: false t.boolean "auto_offline", default: true, null: false @@ -63,8 +63,8 @@ t.integer "status", default: 0, null: false t.string "message_id", null: false t.string "message_checksum", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["message_id", "message_checksum"], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true end @@ -100,8 +100,8 @@ t.integer "inbox_id" t.integer "agent_bot_id" t.integer "status", default: 0 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "account_id" end @@ -109,8 +109,8 @@ t.string "name" t.string "description" t.string "outgoing_url" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.bigint "account_id" t.integer "bot_type", default: 0 t.jsonb "bot_config", default: {} @@ -140,8 +140,8 @@ t.text "content" t.integer "status" t.integer "views" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.bigint "author_id" t.bigint "associated_article_id" t.jsonb "meta", default: {} @@ -196,8 +196,8 @@ t.string "event_name", null: false t.jsonb "conditions", default: "{}", null: false t.jsonb "actions", default: "{}", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.boolean "active", default: true, null: false t.index ["account_id"], name: "index_automation_rules_on_account_id" end @@ -212,8 +212,8 @@ t.bigint "account_id", null: false t.bigint "inbox_id", null: false t.jsonb "trigger_rules", default: {} - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "campaign_type", default: 0, null: false t.integer "campaign_status", default: 0, null: false t.jsonb "audience", default: [] @@ -240,8 +240,8 @@ t.string "name" t.text "description" t.integer "position" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.string "locale", default: "en" t.string "slug", null: false t.bigint "parent_category_id" @@ -257,8 +257,8 @@ create_table "channel_api", force: :cascade do |t| t.integer "account_id", null: false t.string "webhook_url" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.string "identifier" t.string "hmac_token" t.boolean "hmac_mandatory", default: false @@ -271,8 +271,8 @@ t.integer "account_id", null: false t.string "email", null: false t.string "forward_to_email", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.boolean "imap_enabled", default: false t.string "imap_address", default: "" t.integer "imap_port", default: 0 @@ -312,9 +312,21 @@ t.string "line_channel_id", null: false t.string "line_channel_secret", null: false t.string "line_channel_token", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false + t.index ["line_channel_id"], name: "index_channel_line_on_line_channel_id", unique: true + end + + create_table "channel_notifica_me", force: :cascade do |t| + t.string "notifica_me_id", null: false + t.string "notifica_me_type", null: false + t.string "notifica_me_token", null: false + t.integer "account_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["line_channel_id"], name: "index_channel_line_on_line_channel_id", unique: true + t.jsonb "message_templates", default: "{}", null: false + t.datetime "message_templates_last_updated" + t.index ["notifica_me_id", "account_id"], name: "index_channel_notifica_me", unique: true end create_table "channel_sms", force: :cascade do |t| @@ -322,8 +334,8 @@ t.string "phone_number", null: false t.string "provider", default: "default" t.jsonb "provider_config", default: {} - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["phone_number"], name: "index_channel_sms_on_phone_number", unique: true end @@ -331,8 +343,8 @@ t.string "bot_name" t.integer "account_id", null: false t.string "bot_token", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["bot_token"], name: "index_channel_telegram_on_bot_token", unique: true end @@ -341,8 +353,8 @@ t.string "auth_token", null: false t.string "account_sid", null: false t.integer "account_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "medium", default: 0 t.string "messaging_service_sid" t.string "api_key_sid" @@ -356,8 +368,8 @@ t.string "twitter_access_token", null: false t.string "twitter_access_token_secret", null: false t.integer "account_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.boolean "tweets_enabled", default: true t.index ["account_id", "profile_id"], name: "index_channel_twitter_profiles_on_account_id_and_profile_id", unique: true end @@ -387,8 +399,8 @@ t.string "phone_number", null: false t.string "provider", default: "default" t.jsonb "provider_config", default: {} - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.jsonb "message_templates", default: {} t.datetime "message_templates_last_updated", precision: nil t.index ["phone_number"], name: "index_channel_whatsapp_on_phone_number", unique: true @@ -398,8 +410,8 @@ t.bigint "contact_id" t.bigint "inbox_id" t.string "source_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.boolean "hmac_verified", default: false t.string "pubsub_token" t.index ["contact_id"], name: "index_contact_inboxes_on_contact_id" @@ -442,8 +454,8 @@ t.bigint "account_id", null: false t.bigint "user_id", null: false t.bigint "conversation_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_conversation_participants_on_account_id" t.index ["conversation_id"], name: "index_conversation_participants_on_conversation_id" t.index ["user_id", "conversation_id"], name: "index_conversation_participants_on_user_id_and_conversation_id", unique: true @@ -502,8 +514,8 @@ t.text "feedback_message" t.bigint "contact_id", null: false t.bigint "assigned_agent_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_csat_survey_responses_on_account_id" t.index ["assigned_agent_id"], name: "index_csat_survey_responses_on_assigned_agent_id" t.index ["contact_id"], name: "index_csat_survey_responses_on_contact_id" @@ -518,8 +530,8 @@ t.integer "default_value" t.integer "attribute_model", default: 0 t.bigint "account_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.text "attribute_description" t.jsonb "attribute_values", default: [] t.string "regex_pattern" @@ -534,8 +546,8 @@ t.jsonb "query", default: "{}", null: false t.bigint "account_id", null: false t.bigint "user_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_custom_filters_on_account_id" t.index ["user_id"], name: "index_custom_filters_on_user_id" end @@ -555,8 +567,8 @@ t.jsonb "content", default: [] t.bigint "account_id", null: false t.bigint "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_dashboard_apps_on_account_id" t.index ["user_id"], name: "index_dashboard_apps_on_user_id" end @@ -568,8 +580,8 @@ t.text "processing_errors" t.integer "total_records" t.integer "processed_records" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_data_imports_on_account_id" end @@ -579,8 +591,8 @@ t.integer "account_id" t.integer "template_type", default: 1 t.integer "locale", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["name", "account_id"], name: "index_email_templates_on_name_and_account_id", unique: true end @@ -588,8 +600,8 @@ t.integer "account_id", null: false t.integer "category_id", null: false t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false end create_table "inbox_members", id: :serial, force: :cascade do |t| @@ -631,8 +643,8 @@ create_table "installation_configs", force: :cascade do |t| t.string "name", null: false t.jsonb "serialized_value", default: {}, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.boolean "locked", default: true, null: false t.index ["name", "created_at"], name: "index_installation_configs_on_name_and_created_at", unique: true t.index ["name"], name: "index_installation_configs_on_name", unique: true @@ -646,8 +658,8 @@ t.integer "hook_type", default: 0 t.string "reference_id" t.string "access_token" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.jsonb "settings", default: {} end @@ -657,8 +669,8 @@ t.string "color", default: "#1f93ff", null: false t.boolean "show_on_sidebar" t.bigint "account_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_labels_on_account_id" t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true end @@ -670,8 +682,8 @@ t.bigint "created_by_id" t.bigint "updated_by_id" t.jsonb "actions", default: {}, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_macros_on_account_id" end @@ -680,8 +692,8 @@ t.bigint "conversation_id", null: false t.bigint "account_id", null: false t.datetime "mentioned_at", precision: nil, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_mentions_on_account_id" t.index ["conversation_id"], name: "index_mentions_on_conversation_id" t.index ["user_id", "conversation_id"], name: "index_mentions_on_user_id_and_conversation_id", unique: true @@ -698,7 +710,7 @@ t.datetime "updated_at", precision: nil, null: false t.boolean "private", default: false, null: false t.integer "status", default: 0 - t.string "source_id" + t.string "source_id", limit: 510 t.integer "content_type", default: 0, null: false t.json "content_attributes", default: {} t.string "sender_type" @@ -725,8 +737,8 @@ t.bigint "account_id", null: false t.bigint "contact_id", null: false t.bigint "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_notes_on_account_id" t.index ["contact_id"], name: "index_notes_on_contact_id" t.index ["user_id"], name: "index_notes_on_user_id" @@ -736,8 +748,8 @@ t.integer "account_id" t.integer "user_id" t.integer "email_flags", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "push_flags", default: 0, null: false t.index ["account_id", "user_id"], name: "by_account_user", unique: true end @@ -746,8 +758,8 @@ t.bigint "user_id", null: false t.integer "subscription_type", null: false t.jsonb "subscription_attributes", default: {}, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.text "identifier" t.index ["identifier"], name: "index_notification_subscriptions_on_identifier", unique: true t.index ["user_id"], name: "index_notification_subscriptions_on_user_id" @@ -762,8 +774,8 @@ t.string "secondary_actor_type" t.bigint "secondary_actor_id" t.datetime "read_at", precision: nil - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.datetime "snoozed_until" t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" } t.jsonb "meta", default: {} @@ -778,8 +790,8 @@ t.bigint "platform_app_id", null: false t.string "permissible_type", null: false t.bigint "permissible_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["permissible_type", "permissible_id"], name: "index_platform_app_permissibles_on_permissibles" t.index ["platform_app_id", "permissible_id", "permissible_type"], name: "unique_permissibles_index", unique: true t.index ["platform_app_id"], name: "index_platform_app_permissibles_on_platform_app_id" @@ -787,15 +799,15 @@ create_table "platform_apps", force: :cascade do |t| t.string "name", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false end create_table "portal_members", force: :cascade do |t| t.bigint "portal_id" t.bigint "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["portal_id", "user_id"], name: "index_portal_members_on_portal_id_and_user_id", unique: true t.index ["user_id", "portal_id"], name: "index_portal_members_on_user_id_and_portal_id", unique: true end @@ -809,8 +821,8 @@ t.string "homepage_link" t.string "page_title" t.text "header_text" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.jsonb "config", default: {"allowed_locales"=>["en"]} t.boolean "archived", default: false t.bigint "channel_web_widget_id" @@ -830,8 +842,8 @@ create_table "related_categories", force: :cascade do |t| t.bigint "category_id" t.bigint "related_category_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["category_id", "related_category_id"], name: "index_related_categories_on_category_id_and_related_category_id", unique: true t.index ["related_category_id", "category_id"], name: "index_related_categories_on_related_category_id_and_category_id", unique: true end @@ -843,8 +855,8 @@ t.integer "inbox_id" t.integer "user_id" t.integer "conversation_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.float "value_in_business_hours" t.datetime "event_start_time", precision: nil t.datetime "event_end_time", precision: nil @@ -916,8 +928,8 @@ create_table "team_members", force: :cascade do |t| t.bigint "team_id", null: false t.bigint "user_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["team_id", "user_id"], name: "index_team_members_on_team_id_and_user_id", unique: true t.index ["team_id"], name: "index_team_members_on_team_id" t.index ["user_id"], name: "index_team_members_on_user_id" @@ -928,8 +940,8 @@ t.text "description" t.boolean "allow_auto_assign", default: true t.bigint "account_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["account_id"], name: "index_teams_on_account_id" t.index ["name", "account_id"], name: "index_teams_on_name_and_account_id", unique: true end @@ -980,8 +992,8 @@ t.integer "account_id" t.integer "inbox_id" t.string "url" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "webhook_type", default: 0 t.jsonb "subscriptions", default: ["conversation_status_changed", "conversation_updated", "conversation_created", "contact_created", "contact_updated", "message_created", "message_updated", "webwidget_triggered"] t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true @@ -996,8 +1008,8 @@ t.integer "open_minutes" t.integer "close_hour" t.integer "close_minutes" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.boolean "open_all_day", default: false t.index ["account_id"], name: "index_working_hours_on_account_id" t.index ["inbox_id"], name: "index_working_hours_on_inbox_id" diff --git a/docker-compose.yaml b/docker-compose.yaml index 37c21c0695be3..0b2824179da9f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -91,9 +91,9 @@ services: volumes: - postgres:/data/postgres environment: - - POSTGRES_DB=chatwoot - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD= + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} redis: image: redis:alpine diff --git a/docker/Dockerfile b/docker/Dockerfile index 418b5a516adfb..0668188f7270a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -49,7 +49,7 @@ RUN if [ "$RAILS_ENV" = "production" ]; then \ fi COPY package.json yarn.lock ./ -RUN yarn install +RUN yarn install --network-concurrency 1 --network-timeout 600000 COPY . /app diff --git a/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb b/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb index a9c8ee1f07810..3faf645da9e63 100644 --- a/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb +++ b/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb @@ -2,7 +2,7 @@ module Enterprise::SuperAdmin::AppConfigsController private def allowed_configs - return super if ChatwootHub.pricing_plan == 'community' + # return super if ChatwootHub.pricing_plan == 'community' case @config when 'custom_branding' @@ -28,6 +28,7 @@ def custom_branding_options TERMS_URL PRIVACY_URL DISPLAY_MANIFEST + CONVERSATION_STYLE_CSS ] end diff --git a/enterprise/app/helpers/super_admin/features.yml b/enterprise/app/helpers/super_admin/features.yml index 4529fb02b21e9..4cd6b6df2b4f2 100644 --- a/enterprise/app/helpers/super_admin/features.yml +++ b/enterprise/app/helpers/super_admin/features.yml @@ -3,26 +3,26 @@ custom_branding: name: 'Custom Branding' description: 'Apply your own branding to this installation.' - enabled: <%= (ChatwootHub.pricing_plan != 'community') %> + enabled: true icon: 'icon-paint-brush-line' config_key: 'custom_branding' enterprise: true agent_capacity: name: 'Agent Capacity' description: 'Set limits to auto-assigning conversations to your agents.' - enabled: <%= (ChatwootHub.pricing_plan != 'community') %> + enabled: true icon: 'icon-hourglass-line' enterprise: true audit_logs: name: 'Audit Logs' description: 'Track and trace account activities with ease with detailed audit logs.' - enabled: <%= (ChatwootHub.pricing_plan != 'community') %> + enabled: true icon: 'icon-menu-search-line' enterprise: true disable_branding: name: 'Disable Branding' description: 'Disable branding on live-chat widget and external emails.' - enabled: <%= (ChatwootHub.pricing_plan != 'community') %> + enabled: true icon: 'icon-sailbot-fill' enterprise: true live_chat: @@ -72,3 +72,9 @@ microsoft: enabled: true icon: 'icon-microsoft' config_key: 'microsoft' +unoapi: + name: 'Unoapi' + description: 'Configuration for setting up Unoapi' + enabled: true + icon: 'icon-chat-smile-3-line' + config_key: 'unoapi' diff --git a/lib/filters/filter_keys.yml b/lib/filters/filter_keys.yml index 902b5f2195d44..c0bcd6ecc0851 100644 --- a/lib/filters/filter_keys.yml +++ b/lib/filters/filter_keys.yml @@ -4,7 +4,7 @@ # 3. Automation Filters (app/services/automation_rules/conditions_filter_service.rb), (app/services/automation_rules/condition_validation_service.rb) -# Format +# Format # - Parent Key (conversation, contact, messages) # - Key (attribute_name) # - attribute_type: "standard" : supported ["standard", "additional_attributes (only for conversations and messages)"] @@ -50,6 +50,12 @@ conversations: filter_operators: - "equal_to" - "not_equal_to" + contact_id: + attribute_type: "standard" + data_type: "number" + filter_operators: + - "equal_to" + - "not_equal_to" display_id: attribute_type: "standard" data_type: "Number" diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb index f2e66f99728c3..fc56104c4b6d1 100644 --- a/lib/webhooks/trigger.rb +++ b/lib/webhooks/trigger.rb @@ -1,14 +1,16 @@ class Webhooks::Trigger SUPPORTED_ERROR_HANDLE_EVENTS = %w[message_created message_updated].freeze - def initialize(url, payload, webhook_type) + def initialize(url, payload, webhook_type, method, headers) @url = url @payload = payload @webhook_type = webhook_type + @method = method + @headers = headers end - def self.execute(url, payload, webhook_type) - new(url, payload, webhook_type).execute + def self.execute(url, payload, webhook_type, method = :post, headers = { content_type: :json, accept: :json }) + new(url, payload, webhook_type, method, headers).execute end def execute @@ -21,11 +23,12 @@ def execute private def perform_request + Rails.logger.debug { "Webhook Trigger @method: #{@method} @url #{@url} @payload #{ @payload.to_json} @headers #{@headers}" } RestClient::Request.execute( - method: :post, + method: @method, url: @url, payload: @payload.to_json, - headers: { content_type: :json, accept: :json }, + headers: @headers, timeout: 5 ) end diff --git a/package.json b/package.json index c2736d2277b1a..63ce1235b184c 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "ionicons": "~2.0.1", "js-cookie": "^3.0.5", "lamejs": "1.2.0", - "libphonenumber-js": "^1.10.24", + "libphonenumber-js": "^1.11.5", "markdown-it": "^13.0.2", "markdown-it-link-attributes": "^4.0.1", "md5": "^2.3.0", @@ -73,6 +73,7 @@ "postcss": "^8.4.31", "postcss-loader": "^4.2.0", "semver": "7.5.3", + "socket.io-client": "^4.7.5", "tailwindcss": "^3.3.2", "tinykeys": "^2.1.0", "turbolinks": "^5.2.0", @@ -99,6 +100,7 @@ "vuex": "~2.1.1", "vuex-router-sync": "~4.1.2", "wavesurfer.js": "^6.0.4", + "wavoip-api": "2.0.0", "webpack": "^4.46.0", "webpack-cli": "^3.3.12" }, @@ -148,7 +150,7 @@ "webpack-dev-server": "^3" }, "engines": { - "node": "20.x", + "node": ">= 20.x", "npm": ">=6.x", "yarn": "1.22.x" }, diff --git a/public/assets/images/dashboard/channels/notifica_me.png b/public/assets/images/dashboard/channels/notifica_me.png new file mode 100644 index 0000000000000..7edddd010742f Binary files /dev/null and b/public/assets/images/dashboard/channels/notifica_me.png differ diff --git a/public/integrations/channels/badges/notifica_me.png b/public/integrations/channels/badges/notifica_me.png new file mode 100644 index 0000000000000..7edddd010742f Binary files /dev/null and b/public/integrations/channels/badges/notifica_me.png differ diff --git a/spec/controllers/api/v1/accounts/campaigns_controller_spec.rb b/spec/controllers/api/v1/accounts/campaigns_controller_spec.rb index 1d9908652ca5d..95b2edeb3354e 100644 --- a/spec/controllers/api/v1/accounts/campaigns_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/campaigns_controller_spec.rb @@ -149,6 +149,27 @@ expect(response_data[:scheduled_at]).to eq(scheduled_at.to_i) expect(response_data[:audience].pluck(:id)).to include(label1.id, label2.id) end + + it 'creates a new oneoff unoapi campaign' do + unoapi_channel = create(:channel_whatsapp, provider: 'unoapi', sync_templates: false, validate_provider_config: false) + unoapi_inbox = create(:inbox, channel: unoapi_channel) + phone_number = Faker::PhoneNumber.cell_phone_in_e164 + audience = [ + { phone_number: phone_number, name: Faker::Name.name, identifier: rand(1000...1100) } + ] + + post "/api/v1/accounts/#{account.id}/campaigns", + params: { + inbox_id: unoapi_inbox.id, title: 'test', message: 'test message', audience: audience + }, + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body, symbolize_names: true) + expect(response_data[:campaign_type]).to eq('one_off') + expect(response_data[:audience]).to eq(audience) + end end end diff --git a/spec/jobs/agent_bots/webhook_job_spec.rb b/spec/jobs/agent_bots/webhook_job_spec.rb index a8117d84e3177..b3427cea9c26a 100644 --- a/spec/jobs/agent_bots/webhook_job_spec.rb +++ b/spec/jobs/agent_bots/webhook_job_spec.rb @@ -16,7 +16,7 @@ end it 'executes perform' do - expect(Webhooks::Trigger).to receive(:execute).with(url, payload, webhook_type) - perform_enqueued_jobs { job } + expect(Webhooks::Trigger).to receive(:execute) + .with(url, payload, webhook_type, :post, { accept: :json, content_type: :json }) end end diff --git a/spec/jobs/campaign_message_job_spec.rb b/spec/jobs/campaign_message_job_spec.rb new file mode 100644 index 0000000000000..e6f105b914198 --- /dev/null +++ b/spec/jobs/campaign_message_job_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' + +RSpec.describe CampaignMessageJob do + subject(:job) do + described_class.perform_later( + campaign.account_id, + campaign.inbox_id, + campaign.id, + phone_number, + campaign.message + ) + end + + let(:account) { create(:account) } + let!(:unoapi_channel) { create(:channel_whatsapp, provider: 'unoapi', sync_templates: false, validate_provider_config: false) } + let!(:unoapi_inbox) { create(:inbox, channel: unoapi_channel) } + let(:phone_number) { Faker::PhoneNumber.cell_phone_in_e164 } + let(:name) { Faker::Name.name } + let(:identifier) { rand(999..1000).to_s } + let(:audience_1) { { phone_number: phone_number, name: name } } + let(:audience) { [audience_1] } + let!(:campaign) do + create(:campaign, inbox: unoapi_inbox, account: account, audience: [audience], message: 'hello #name') + end + + it 'enqueues the job' do + expect { job }.to have_enqueued_job(described_class) + .with( + campaign.account_id, + campaign.inbox_id, + campaign.id, + phone_number, + campaign.message + ) + .on_queue('low') + end + + context 'when the job is triggered on a new message' do + let(:process_service) { double } + + before do + allow(process_service).to receive(:perform) + end + + it 'calls create a message' do + count = Message.count + described_class.perform_now( + campaign.account_id, + campaign.inbox_id, + campaign.id, + campaign.message, + audience_1 + ) + expect(Message.count).to be count + 1 + end + + it 'bind message content' do + described_class.perform_now( + campaign.account_id, + campaign.inbox_id, + campaign.id, + campaign.message, + audience_1 + ) + expect(Message.last.content).to eq("hello #{name}") + end + + it 'update contact with identifier' do + described_class.perform_now( + campaign.account_id, + campaign.inbox_id, + campaign.id, + campaign.message, + audience_1 + ) + expect(Contact.where(identifier: identifier).count).to eq(0) + expect(Message.last.content).to eq("hello #{name}") + audience = { phone_number: phone_number, name: name, identifier: identifier } + described_class.perform_now( + campaign.account_id, + campaign.inbox_id, + campaign.id, + campaign.message, + audience + ) + expect(Contact.where(identifier: identifier).count).to eq(1) + end + end +end diff --git a/spec/jobs/webhook_job_spec.rb b/spec/jobs/webhook_job_spec.rb index 81802a3c0057c..19154bd2b47b6 100644 --- a/spec/jobs/webhook_job_spec.rb +++ b/spec/jobs/webhook_job_spec.rb @@ -15,8 +15,9 @@ .on_queue('medium') end - it 'executes perform with default webhook type' do - expect(Webhooks::Trigger).to receive(:execute).with(url, payload, webhook_type) + it 'executes performm with default webhook type' do + expect(Webhooks::Trigger).to receive(:execute) + .with(url, payload, webhook_type, :post, { accept: :json, content_type: :json }) perform_enqueued_jobs { job } end diff --git a/spec/services/whatsapp/oneoff_unoapi_campaign_service_spec.rb b/spec/services/whatsapp/oneoff_unoapi_campaign_service_spec.rb new file mode 100644 index 0000000000000..f4d489994528b --- /dev/null +++ b/spec/services/whatsapp/oneoff_unoapi_campaign_service_spec.rb @@ -0,0 +1,76 @@ +require 'rails_helper' + +describe Whatsapp::OneoffUnoapiCampaignService do + subject(:unoapi_campaign_service) { described_class.new(campaign: campaign) } + + let(:account) { create(:account) } + let!(:unoapi_channel) { create(:channel_whatsapp, provider: 'unoapi', sync_templates: false, validate_provider_config: false) } + let!(:unoapi_inbox) { create(:inbox, channel: unoapi_channel) } + let!(:contact) { create(:contact, phone_number: Faker::PhoneNumber.cell_phone_in_e164, account: account) } + let(:phone_number) { Faker::PhoneNumber.cell_phone_in_e164 } + let(:uuid) { rand(1..10) } + let(:audience_1) { { phone_number: phone_number, audience_id: uuid, status: :scheduled } } + let(:audience_2) { { phone_number: contact.phone_number, audience_id: uuid, status: :scheduled } } + let(:audience) { [audience_1, audience_2] } + let!(:campaign) do + create(:campaign, inbox: unoapi_inbox, account: account, audience: audience) + end + + describe 'perform' do + it 'raises error if the campaign is completed' do + campaign.completed! + + expect { unoapi_campaign_service.perform }.to raise_error 'Completed Campaign' + end + + it 'raises error invalid campaign when its not a oneoff whatsapp with unoapi provider campaign' do + whatsapp_channel = create(:channel_whatsapp, sync_templates: false, validate_provider_config: false) + whatsapp_inbox = create(:inbox, channel: whatsapp_channel) + campaign = build(:campaign, inbox: whatsapp_inbox) + + expect { described_class.new(campaign: campaign).perform }.to raise_error "Invalid campaign #{campaign.id}" + end + + it 'send messages to contacts in the audience and marks the campaign completed' do + allow(CampaignMessageJob).to receive(:perform_later) + allow(CampaignMessageJob).to receive(:set).and_return(CampaignMessageJob) + allow(SecureRandom).to receive(:uuid).and_return(uuid) + unoapi_campaign_service.perform + expect(campaign.reload.completed?).to be true + expect(CampaignMessageJob) + .to have_received(:perform_later) + .with( + campaign.account_id, + campaign.inbox_id, + campaign.id, + campaign.message, + audience_1 + ) + + expect(CampaignMessageJob) + .to have_received(:perform_later) + .with( + campaign.account_id, + campaign.inbox_id, + campaign.id, + campaign.message, + audience_2 + ) + end + + it 'send messages to contacts in the audience and wait_for wait_for_seconds' do + wait_for_seconds = rand(1..10) + audience_1[:wait_for_seconds] = wait_for_seconds + campaign.audience = [audience_1] + service = described_class.new(campaign: campaign) + allow(CampaignMessageJob).to receive(:set).and_return(CampaignMessageJob) + allow(CampaignMessageJob).to receive(:perform_later) + freeze_time do + service.perform + expect(CampaignMessageJob) + .to have_received(:set) + .with(wait: wait_for_seconds) + end + end + end +end diff --git a/swagger/definitions/request/automation_rule/create_update_payload.yml b/swagger/definitions/request/automation_rule/create_update_payload.yml index 20d362e486113..2e1c46e263536 100644 --- a/swagger/definitions/request/automation_rule/create_update_payload.yml +++ b/swagger/definitions/request/automation_rule/create_update_payload.yml @@ -13,6 +13,7 @@ properties: enum: - conversation_created - conversation_updated + - conversation_resolved - message_created example: message_created description: The event when you want to execute the automation actions diff --git a/swagger/definitions/resource/automation_rule.yml b/swagger/definitions/resource/automation_rule.yml index 4a3a03d013b18..b67a416530547 100644 --- a/swagger/definitions/resource/automation_rule.yml +++ b/swagger/definitions/resource/automation_rule.yml @@ -2,10 +2,11 @@ type: object properties: event_name: type: string - description: Automation Rule event, on which we call the actions(conversation_created, conversation_updated, message_created) + description: Automation Rule event, on which we call the actions(conversation_created, conversation_updated, conversation_resolved, message_created) enum: - conversation_created - conversation_updated + - conversation_resolved - message_created example: message_created name: diff --git a/swagger/swagger.json b/swagger/swagger.json index d5f94f730f535..6b4596a580ce9 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -6718,6 +6718,7 @@ "enum": [ "conversation_created", "conversation_updated", + "conversation_resolved", "message_created" ], "example": "message_created", diff --git a/vendor/db/sentiment-analysis.onnx b/vendor/db/sentiment-analysis.onnx deleted file mode 100644 index 1cb2623543d0c..0000000000000 Binary files a/vendor/db/sentiment-analysis.onnx and /dev/null differ diff --git a/yarn.lock b/yarn.lock index 56c7c42b30c91..d10ea69e4e9b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3222,7 +3222,7 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@discoveryjs/json-ext@^0.5.3": +"@discoveryjs/json-ext@^0.5.0", "@discoveryjs/json-ext@^0.5.3": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== @@ -3556,7 +3556,7 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24": +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -4023,6 +4023,11 @@ dependencies: semver "7.5.3" +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + "@stdlib/array-float32@^0.0.x": version "0.0.6" resolved "https://registry.yarnpkg.com/@stdlib/array-float32/-/array-float32-0.0.6.tgz#7a1c89db3c911183ec249fa32455abd9328cfa27" @@ -6011,6 +6016,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + "@types/glob@*": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" @@ -6584,6 +6594,14 @@ "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -6618,6 +6636,11 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + "@webassemblyjs/helper-buffer@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" @@ -6671,6 +6694,16 @@ "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/wasm-gen" "1.11.6" +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/helper-wasm-section@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" @@ -6747,6 +6780,20 @@ "@webassemblyjs/wasm-parser" "1.11.6" "@webassemblyjs/wast-printer" "1.11.6" +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + "@webassemblyjs/wasm-gen@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" @@ -6758,6 +6805,17 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/wasm-gen@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" @@ -6779,6 +6837,16 @@ "@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-parser" "1.11.6" +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wasm-opt@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" @@ -6801,6 +6869,18 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + "@webassemblyjs/wasm-parser@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" @@ -6833,6 +6913,14 @@ "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + "@webassemblyjs/wast-printer@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" @@ -6842,6 +6930,21 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + "@xmldom/xmldom@^0.7.2": version "0.7.9" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.9.tgz#7f9278a50e737920e21b297b8a35286e9942c056" @@ -6875,6 +6978,11 @@ acorn-import-assertions@^1.9.0: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -7467,6 +7575,18 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +audio-recorder-worklet-processor@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/audio-recorder-worklet-processor/-/audio-recorder-worklet-processor-0.0.2.tgz#e5fc8e3a21c6013af6011a721a3cd5a2c6ad8d0b" + integrity sha512-iB8o+WD0zzbMtSDDS9C5qi/9PnPhQL8ehVVFmui1DGpc5OdWIgNesGX2TJZsqv069RZ7dII8ao9HFrRFXjZfZA== + dependencies: + ts-loader "^9.5.1" + typescript "^5.3.2" + webpack "^5.89.0" + webpack-cli "^5.1.4" + worker-loader "^3.0.8" + worklet-loader "^2.0.0" + autoprefixer@^10.4.14: version "10.4.19" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" @@ -7525,6 +7645,15 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae" integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g== +axios@^1.2.0: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axios@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" @@ -7786,6 +7915,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +benchmark@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-1.0.0.tgz#2f1e2fa4c359f11122aa183082218e957e390c73" + integrity sha512-qSlOi0If8sI+icu3l/W5rd4R0etJz9orLPWpDdt1lPgEFzEHYYnkfMuotj+Lx5SyMkmfawlPoW9RmoEm19ziHA== + better-opn@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-2.1.1.tgz#94a55b4695dc79288f31d7d0e5f658320759f7c6" @@ -8023,6 +8157,16 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4 escalade "^3.1.1" node-releases "^1.1.71" +browserslist@^4.21.10: + version "4.24.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" + integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== + dependencies: + caniuse-lite "^1.0.30001663" + electron-to-chromium "^1.5.28" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + browserslist@^4.21.3: version "4.21.5" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" @@ -8302,6 +8446,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz" integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== +caniuse-lite@^1.0.30001663: + version "1.0.30001664" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz#d588d75c9682d3301956b05a3749652a80677df4" + integrity sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -8670,7 +8819,7 @@ colorette@^1.2.1, colorette@^1.2.2: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== -colorette@^2.0.16, colorette@^2.0.20: +colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.20: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== @@ -8692,6 +8841,11 @@ commander@11.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -8724,6 +8878,21 @@ company-email-validator@^1.0.8: dependencies: email-validator "^2.0.4" +component-bind@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw== + +component-emitter@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3" + integrity sha512-YhIbp3PJiznERfjlIkK0ue4obZxt2S60+0W8z24ZymOHT8sHloOqWOqZRU2eN5OlY8U08VFsP02letcu26FilA== + +component-emitter@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -9410,7 +9579,12 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: +debug@0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" + integrity sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q== + +debug@2.6.9, debug@^2.1.0, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -9438,6 +9612,13 @@ debug@^4.3.5: dependencies: ms "2.1.2" +debug@~4.3.1, debug@~4.3.2: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -9957,6 +10138,11 @@ electron-to-chromium@^1.4.477, electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz#933887165b8b6025a81663d2d97cf4b85cde27b2" integrity sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ== +electron-to-chromium@^1.5.28: + version "1.5.30" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.30.tgz#5b264b489cfe0c3dd71097c164d795444834e7c7" + integrity sha512-sXI35EBN4lYxzc/pIGorlymYNzDBOqkSlVRe6MkgBsW/hW1tpC/HDJ2fjG7XnjakzfLEuvdmux0Mjs6jHq4UOA== + elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.7" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" @@ -10012,6 +10198,33 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.4.tgz#b8bc71ed3f25d0d51d587729262486b4b33bd0d0" + integrity sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.0.0" + +engine.io-client@~6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.1.tgz#28a9cc4e90d448e1d0ba9369ad08a7af82f9956a" + integrity sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.1.1" + +engine.io-parser@~5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" + integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== + enhanced-resolve@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz#4d6e689b3725f86090927ccc86cd9f1635b89e2e" @@ -10030,6 +10243,14 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1, enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enhanced-resolve@^5.15.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -10066,6 +10287,11 @@ entities@~3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== +envinfo@^7.7.3: + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -10298,6 +10524,11 @@ escalade@^3.1.1, escalade@^3.1.2: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -10808,6 +11039,11 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" +extend.js@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/extend.js/-/extend.js-0.0.1.tgz#814c453f41201a11e05ea080a4a71f016994ad0b" + integrity sha512-+/x3RdZzXiLkDjq39SwzZ6aPT9CXjI01gerN2sjlEyWm+qo8ZWTQVZ67+YAtLLa/KQXFI5uQaL2SnRUVC7ggnQ== + extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -10896,6 +11132,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" @@ -11079,6 +11320,11 @@ flat-cache@^3.0.4: keyv "^4.5.3" rimraf "^3.0.2" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + flatted@^3.2.2: version "3.2.7" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" @@ -11112,6 +11358,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -11350,6 +11601,11 @@ gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-browser-rtc@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz#d1494e299b00f33fc8e9d6d3343ba4ba99711a2c" + integrity sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ== + get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -11673,7 +11929,7 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== -graceful-fs@^4.2.9: +graceful-fs@^4.2.11, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -11710,6 +11966,13 @@ has-bigints@^1.0.2: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== +has-binary@^0.1.5: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c" + integrity sha512-k1Umb4/jrBWZbtL+QKSji8qWeoZ7ZTkXdnDXt1wxwBKAFM0//u96wDj43mBIqCIas8rDQMYyrBEvcS8hdGd4Sg== + dependencies: + isarray "0.0.1" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -11908,6 +12171,11 @@ hastscript@^6.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" +hat@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a" + integrity sha512-zpImx2GoKXy42fVDSEad2BPKuSQdLcqsCYa48K3zHSzM/ugWuYjLDr8IXxpVuL7uCLHw56eaiLxCRthhOzf5ug== + he@^1.1.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -11937,6 +12205,11 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoek@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.3.1.tgz#1d2ea831857e4ecdce7fd5ffe07acdf8eb368cca" + integrity sha512-v7E+yIjcHECn973i0xHm4kJkEpv3C8sbYS4344WXbzYqRyiDD7rjnnKo4hsJkejQBAFdRMUGNHySeSPKSH9Rqw== + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -12248,6 +12521,14 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -12318,6 +12599,11 @@ inline-style-parser@0.1.1: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== +inline-worker@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/inline-worker/-/inline-worker-1.1.0.tgz#55e96f54915a642b00872a2daa6fe832b424c98d" + integrity sha512-2nlxBGg5Uoop6IRco9wMzlAz62690ylokrUDg3IaCi9bJaq0HykbWlRtRXgvSKiBNmCBGXnOCFBkIFjaSGJocA== + internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -12345,6 +12631,11 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + ionicons@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ionicons/-/ionicons-2.0.1.tgz#ca398113293ea870244f538f0aabbd4b5b209a3e" @@ -12933,6 +13224,11 @@ is-wsl@^2.1.1, is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -13273,6 +13569,11 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json3@3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.2.6.tgz#f6efc93c06a04de9aec53053df2559bb19e2038b" + integrity sha512-KA+GHhYTLTo7Ri4DyjwUgW8kn98AYtVZtBC94qL5yD0ZSYct8/eF8qBmTNyk+gPE578bKeIL4WBq+MUyd1I26g== + json3@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" @@ -13446,10 +13747,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.10.24: - version "1.10.44" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.44.tgz#6709722461173e744190494aaaec9c1c690d8ca8" - integrity sha512-svlRdNBI5WgBjRC20GrCfbFiclbF0Cx+sCcQob/C1r57nsoq0xg8r65QbTyVyweQIlB33P+Uahyho6EMYgcOyQ== +libphonenumber-js@^1.11.5: + version "1.11.9" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.9.tgz#e653042b11da2b50b7ea3b206fa7ca998436ae99" + integrity sha512-Zs5wf5HaWzW2/inlupe2tstl0I/Tbqo7lH20ZLr6Is58u7Dz2n+gRFGNlj9/gWxFvNfp9+YyDsiegjNhdixB9A== lilconfig@2.1.0, lilconfig@^2.0.5, lilconfig@^2.1.0: version "2.1.0" @@ -13587,7 +13888,7 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: +loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: version "1.4.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== @@ -14534,6 +14835,11 @@ node-releases@^2.0.13, node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + node-releases@^2.0.8: version "2.0.13" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" @@ -15255,6 +15561,14 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== + dependencies: + process "^0.11.1" + util "^0.10.3" + pathe@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" @@ -15296,6 +15610,11 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -15352,7 +15671,7 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0: +pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -16479,7 +16798,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.10: +process@^0.11.1, process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= @@ -17191,6 +17510,13 @@ recast@^0.23.1: source-map "~0.6.1" tslib "^2.0.1" +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + recordrtc@>=5.6.2: version "5.6.2" resolved "https://registry.yarnpkg.com/recordrtc/-/recordrtc-5.6.2.tgz#48fc214b35084973ccce82c6251198b5742bc327" @@ -17483,6 +17809,13 @@ resolve-cwd@^2.0.0: dependencies: resolve-from "^3.0.0" +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + resolve-dir@^1.0.0, resolve-dir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" @@ -17537,6 +17870,15 @@ resolve@^1.15.1, resolve@^1.19.0, resolve@^1.3.2: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.4: version "2.0.0-next.4" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" @@ -17828,6 +18170,14 @@ schema-utils@2.7.0: ajv "^6.12.2" ajv-keywords "^3.4.1" +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -18159,6 +18509,18 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +simple-peer@~5.11.0: + version "5.11.9" + resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-5.11.9.tgz#fbf2278b3f16493151ac1fa56aa98f566c7c96ee" + integrity sha512-QWqJFO07H960Due8ZMAhW2Q6wv56stDgSdXtJMKDgf1iGQz6Senk/hkydUO64YA4d9KdrUWwFo1A8Xo5k0WTng== + dependencies: + debug "^2.1.0" + get-browser-rtc "^1.0.0" + hat "0.0.3" + inherits "^2.0.1" + is-typedarray "^1.0.0" + once "^1.3.1" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -18249,6 +18611,60 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-client@^4.6.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.0.tgz#2ea0302d0032d23422bd2860f78127a800cad6a2" + integrity sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.6.1" + socket.io-parser "~4.2.4" + +socket.io-client@^4.7.5: + version "4.7.5" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.5.tgz#919be76916989758bdc20eec63f7ee0ae45c05b7" + integrity sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +"socket.io-p2p-parser@git+https://github.com/tomcartwrightuk/socket.io-p2p-parser": + version "2.2.3" + resolved "git+https://github.com/tomcartwrightuk/socket.io-p2p-parser#1091624359c870e451b1a3a7b2108969380173cd" + dependencies: + benchmark "1.0.0" + component-emitter "1.1.2" + debug "0.7.4" + isarray "0.0.1" + json3 "3.2.6" + +socket.io-p2p@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/socket.io-p2p/-/socket.io-p2p-2.2.0.tgz#a5cd935a00e1f160ae2edbd2a26adb9968c84534" + integrity sha512-UPZ6fB11cxuiY4LXTNxe1DA28L1/Vy4+5dVXGtoGtwb4Oo1xJh/hCAfOUGJQev7AwZ7df9UagBwSXtuPZOIshg== + dependencies: + component-bind "^1.0.0" + component-emitter "^1.2.0" + debug "^2.1.0" + extend.js "0.0.1" + has-binary "^0.1.5" + hat "0.0.3" + simple-peer "~5.11.0" + socket.io-p2p-parser tomcartwrightuk/socket.io-p2p-parser + to-array "^0.1.4" + webrtcsupport "^2.1.2" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + sockjs-client@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6" @@ -18336,6 +18752,11 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" @@ -19005,6 +19426,17 @@ terser-webpack-plugin@^4.2.3: terser "^5.3.4" webpack-sources "^1.4.3" +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + terser-webpack-plugin@^5.3.7: version "5.3.9" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" @@ -19035,6 +19467,16 @@ terser@^5.16.8: commander "^2.20.0" source-map-support "~0.5.20" +terser@^5.26.0: + version "5.34.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.34.1.tgz#af40386bdbe54af0d063e0670afd55c3105abeb6" + integrity sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + terser@^5.3.4: version "5.14.1" resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.1.tgz#7c95eec36436cb11cf1902cc79ac564741d19eca" @@ -19164,6 +19606,11 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== +to-array@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A== + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -19289,6 +19736,17 @@ ts-loader@^8.0.14: micromatch "^4.0.0" semver "^7.3.4" +ts-loader@^9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.1.tgz#63d5912a86312f1fbe32cef0859fb8b2193d9b89" + integrity sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + source-map "^0.7.4" + ts-map@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/ts-map/-/ts-map-1.0.3.tgz#1c4d218dec813d2103b7e04e4bcf348e1471c1ff" @@ -19459,6 +19917,11 @@ typescript@^4.9.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.3.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" + integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -19723,6 +20186,14 @@ update-browserslist-db@^1.0.11, update-browserslist-db@^1.0.13: escalade "^3.1.2" picocolors "^1.0.1" +update-browserslist-db@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -19821,6 +20292,13 @@ util@0.10.3: dependencies: inherits "2.0.1" +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + util@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" @@ -20296,11 +20774,32 @@ watchpack@^2.2.0, watchpack@^2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + wavesurfer.js@>=5.0.1, wavesurfer.js@^6.0.4: version "6.1.0" resolved "https://registry.yarnpkg.com/wavesurfer.js/-/wavesurfer.js-6.1.0.tgz#c6d4a192cbe2cb60717c88ea3f0a95bedc3dd571" integrity sha512-Ss8d4PC8r1vSE8Qtf3UhC6o4BWXL1J/tr9DFwshFW2pSZBdkzau6FzWGJUul3aoOZSFb+azC5F/m0I9F+vU72w== +wavoip-api@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wavoip-api/-/wavoip-api-2.0.0.tgz#73623e6ce76f7d29aabd5a50881e4d27f5028daf" + integrity sha512-zW1r+NA4nQICV/JXx6IiFLljdgFY/VYToZq06L8kYYFWPL8UiICwii3nC80fIlhqXek30WVU3rP+6iLXeABp/Q== + dependencies: + audio-recorder-worklet-processor "^0.0.2" + axios "^1.2.0" + inline-worker "^1.1.0" + path "^0.12.7" + socket.io-client "^4.6.0" + socket.io-p2p "^2.2.0" + worklet-loader "^2.0.0" + wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" @@ -20353,6 +20852,25 @@ webpack-cli@^3.3.12: v8-compile-cache "^2.1.1" yargs "^13.3.2" +webpack-cli@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + webpack-dev-middleware@^3.7.2, webpack-dev-middleware@^3.7.3: version "3.7.3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" @@ -20425,6 +20943,15 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + webpack-sources@^1.0.0, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" @@ -20504,6 +21031,35 @@ webpack@4, webpack@^4.46.0: watchpack "^2.4.0" webpack-sources "^3.2.3" +webpack@^5.89.0: + version "5.95.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" + integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + webrtc-adapter@>=8.0.0: version "8.1.1" resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-8.1.1.tgz#e4a4dfd1b5085d119da40c4efc0147f7d0961cba" @@ -20511,6 +21067,11 @@ webrtc-adapter@>=8.0.0: dependencies: sdp "^3.0.2" +webrtcsupport@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/webrtcsupport/-/webrtcsupport-2.2.0.tgz#32d8c9608725ada352af0ab6abf21acab5182aa6" + integrity sha512-9KvL8SxgzbMC/SUMvW+4qcvsXtAnM0iSuxjOwsCqBbA+DA5biaQq8+f4aGfb341BgoFrQX2PQNsvrL2YkuYB1w== + websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" @@ -20628,6 +21189,11 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + with@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac" @@ -20650,6 +21216,14 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-loader@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.8.tgz#5fc5cda4a3d3163d9c274a4e3a811ce8b60dbb37" + integrity sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + worker-rpc@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5" @@ -20657,6 +21231,15 @@ worker-rpc@^0.1.0: dependencies: microevent.ts "~0.1.1" +worklet-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worklet-loader/-/worklet-loader-2.0.0.tgz#c6c8f7dbed38f3d32dfc61c399f13a85f7eebe9d" + integrity sha512-zvCCyhgrn85C5g1+EQWDz4KHxkEMq/fZQXVftjFOcxTy6f+grBSRTJLoit3u8xyKIBTGEgQzYiousSm3YWt8oA== + dependencies: + hoek "^4.2.1" + loader-utils "^1.0.0" + schema-utils "^0.4.0" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -20738,7 +21321,7 @@ ws@^8.17.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -ws@^8.2.3: +ws@^8.2.3, ws@~8.17.1: version "8.17.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== @@ -20765,6 +21348,16 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + +xmlhttprequest-ssl@~2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz#0d045c3b2babad8e7db1af5af093f5d0d60df99a" + integrity sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"