Merge branch 'fazer-ai/main' into chore/merge-upstream
This commit is contained in:
commit
540f67aef6
11
.env.example
11
.env.example
@ -2,7 +2,7 @@
|
||||
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
|
||||
|
||||
# Used to verify the integrity of signed cookies. so ensure a secure value is set
|
||||
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
|
||||
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
|
||||
# Use `rake secret` to generate this variable
|
||||
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||
|
||||
@ -258,3 +258,12 @@ AZURE_APP_SECRET=
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
# NOTE: Useful when running inside docker-compose network to link to external domain
|
||||
FRONTEND_URL_EXTERNAL=
|
||||
|
||||
# Baileys API Whatsapp provider
|
||||
BAILEYS_PROVIDER_DEFAULT_CLIENT_NAME=Chatwoot
|
||||
BAILEYS_PROVIDER_DEFAULT_URL=http://localhost:3025
|
||||
BAILEYS_PROVIDER_DEFAULT_API_KEY=
|
||||
|
||||
RESEND_API_KEY=
|
||||
|
||||
6
.github/workflows/frontend-fe.yml
vendored
6
.github/workflows/frontend-fe.yml
vendored
@ -3,10 +3,8 @@ name: Frontend Lint & Test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- fazer-ai/main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
136
.github/workflows/publish_github_docker.yml
vendored
Normal file
136
.github/workflows/publish_github_docker.yml
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
name: Publish Chatwoot docker images to GitHub
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- fazer-ai/main
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GITHUB_REPO: ghcr.io/${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Set Docker Tags
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "fazer-ai/main" ]; then
|
||||
echo "GITHUB_TAG=${GITHUB_REPO}:latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "GITHUB_TAG=${GITHUB_REPO}:${SANITIZED_REF}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to GitHub Container Registry
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
|
||||
outputs: type=image,name=${{ env.GITHUB_TAG }},name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build-ghcr.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "fazer-ai/main" ]; then
|
||||
GITHUB_TAG="ghcr.io/${{ github.repository }}:latest"
|
||||
else
|
||||
GITHUB_TAG="ghcr.io/${{ github.repository }}:${SANITIZED_REF}"
|
||||
fi
|
||||
|
||||
docker buildx imagetools create -t $GITHUB_TAG \
|
||||
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "fazer-ai/main" ]; then
|
||||
GITHUB_TAG="ghcr.io/${{ github.repository }}:latest"
|
||||
else
|
||||
GITHUB_TAG="ghcr.io/${{ github.repository }}:${SANITIZED_REF}"
|
||||
fi
|
||||
|
||||
docker buildx imagetools inspect $GITHUB_TAG
|
||||
5
.github/workflows/run_foss_spec.yml
vendored
5
.github/workflows/run_foss_spec.yml
vendored
@ -1,10 +1,9 @@
|
||||
name: Run Chatwoot CE spec
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
pull_request:
|
||||
- fazer-ai/main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
# lint js and vue files
|
||||
npx --no-install lint-staged
|
||||
|
||||
# lint only staged ruby files that still exist (not deleted)
|
||||
git diff --name-only --cached | xargs -I {} sh -c 'test -f "{}" && echo "{}"' | grep '\.rb$' | xargs -I {} bundle exec rubocop --force-exclusion -a "{}" || true
|
||||
# lint only staged ruby files
|
||||
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion
|
||||
|
||||
# stage rubocop changes to files
|
||||
git diff --name-only --cached | xargs -I {} sh -c 'test -f "{}" && git add "{}"' || true
|
||||
# git diff --name-only --cached | xargs git add
|
||||
|
||||
11
.rubocop.yml
11
.rubocop.yml
@ -154,6 +154,7 @@ CustomCopLocation:
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
SuggestExtensions: false
|
||||
Exclude:
|
||||
- 'bin/**/*'
|
||||
- 'db/schema.rb'
|
||||
@ -166,3 +167,13 @@ AllCops:
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- 'db/migrate/20230426130150_init_schema.rb'
|
||||
|
||||
Layout/LeadingCommentSpace:
|
||||
Enabled: false
|
||||
|
||||
Rails/SaveBang:
|
||||
Enabled: true
|
||||
AllowedReceivers:
|
||||
- Stripe::Subscription
|
||||
- Stripe::Customer
|
||||
- FactoryBot
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -2,5 +2,6 @@
|
||||
"cSpell.words": [
|
||||
"chatwoot",
|
||||
"dompurify"
|
||||
]
|
||||
],
|
||||
"css.customData": [".vscode/tailwind.json"]
|
||||
}
|
||||
|
||||
55
.vscode/tailwind.json
vendored
Normal file
55
.vscode/tailwind.json
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@responsive",
|
||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@screen",
|
||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@variants",
|
||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
4
Gemfile
4
Gemfile
@ -40,7 +40,7 @@ gem 'down'
|
||||
# authentication type to fetch and send mail over oauth2.0
|
||||
gem 'gmail_xoauth'
|
||||
# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2
|
||||
gem 'net-smtp', '~> 0.3.4'
|
||||
gem 'net-smtp', '~> 0.3.4'
|
||||
# Prevent CSV injection
|
||||
gem 'csv-safe'
|
||||
|
||||
@ -178,6 +178,8 @@ gem 'ruby-openai'
|
||||
|
||||
gem 'shopify_api'
|
||||
|
||||
gem 'resend', '~> 0.19.0'
|
||||
|
||||
### Gems required only in specific deployment environments ###
|
||||
##############################################################
|
||||
|
||||
|
||||
18
Gemfile.lock
18
Gemfile.lock
@ -423,6 +423,7 @@ GEM
|
||||
faraday-multipart
|
||||
json (>= 1.8)
|
||||
rexml
|
||||
language_server-protocol (3.17.0.4)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
letter_opener (1.8.1)
|
||||
@ -545,9 +546,10 @@ GEM
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.1)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.1)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.3)
|
||||
pg_search (2.3.6)
|
||||
activerecord (>= 5.2)
|
||||
@ -632,6 +634,8 @@ GEM
|
||||
uber (< 0.2.0)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
resend (0.19.0)
|
||||
httparty (>= 0.21.0)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
@ -663,14 +667,15 @@ GEM
|
||||
rspec-support (3.13.1)
|
||||
rspec_junit_formatter (0.6.0)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (1.50.2)
|
||||
rubocop (1.57.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.0.0)
|
||||
parser (>= 3.2.2.4)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.28.0, < 2.0)
|
||||
rubocop-ast (>= 1.28.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.28.1)
|
||||
@ -816,7 +821,7 @@ GEM
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.4.2)
|
||||
unicode-display_width (2.6.0)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (1.0.3)
|
||||
uri_template (0.7.0)
|
||||
@ -954,6 +959,7 @@ DEPENDENCIES
|
||||
rails (~> 7.0.8.4)
|
||||
redis
|
||||
redis-namespace
|
||||
resend (~> 0.19.0)
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
|
||||
@ -69,7 +69,7 @@ class ContactIdentifyAction
|
||||
end
|
||||
|
||||
def merge_contacts?(existing_contact, key)
|
||||
return if existing_contact.blank?
|
||||
return false if existing_contact.blank?
|
||||
|
||||
return true if params[:identifier].blank?
|
||||
|
||||
|
||||
@ -160,7 +160,7 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
|
||||
end
|
||||
|
||||
def all_unsupported_files?
|
||||
return if attachments.empty?
|
||||
return false if attachments.empty?
|
||||
|
||||
attachments_type = attachments.pluck(:type).uniq.first
|
||||
unsupported_file_type?(attachments_type)
|
||||
|
||||
@ -30,7 +30,7 @@ class Messages::Instagram::MessageBuilder < Messages::Instagram::BaseMessageBuil
|
||||
# https://developers.facebook.com/docs/graph-api/guides/error-handling/ search for error code 1609005
|
||||
if error_code == 1_609_005
|
||||
@message.attachments.destroy_all
|
||||
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||
@message.update!(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||
end
|
||||
|
||||
Rails.logger.error("[InstagramStoryFetchError]: #{parsed_response.dig('error', 'message')} #{error_code}")
|
||||
|
||||
@ -14,7 +14,7 @@ class Messages::Instagram::Messenger::MessageBuilder < Messages::Instagram::Base
|
||||
rescue Koala::Facebook::ClientError => e
|
||||
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
||||
@message.attachments.destroy_all
|
||||
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||
@message.update!(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||
Rails.logger.error e
|
||||
{}
|
||||
rescue StandardError => e
|
||||
|
||||
@ -68,7 +68,6 @@ class Messages::Messenger::MessageBuilder
|
||||
message.save!
|
||||
end
|
||||
|
||||
# This is a placeholder method to be overridden by child classes
|
||||
def get_story_object_from_source_id(_source_id)
|
||||
{}
|
||||
end
|
||||
|
||||
@ -24,7 +24,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||
ActiveRecord::Base.transaction do
|
||||
automation_rule_update
|
||||
process_attachments
|
||||
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity
|
||||
|
||||
@ -39,7 +39,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
return if response['instagram_business_account'].blank?
|
||||
|
||||
instagram_id = response['instagram_business_account']['id']
|
||||
facebook_channel.update(instagram_id: instagram_id)
|
||||
facebook_channel.update!(instagram_id: instagram_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error in set_instagram_id: #{e.message}"
|
||||
end
|
||||
|
||||
@ -62,6 +62,28 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def setup_channel_provider
|
||||
channel = @inbox.channel
|
||||
|
||||
unless channel.respond_to?(:setup_channel_provider)
|
||||
render json: { error: 'Channel does not support setup' }, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
channel.setup_channel_provider
|
||||
head :ok
|
||||
end
|
||||
|
||||
def disconnect_channel_provider
|
||||
channel = @inbox.channel
|
||||
|
||||
unless channel.respond_to?(:disconnect_channel_provider)
|
||||
render json: { error: 'Channel does not support disconnect' }, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
channel.disconnect_channel_provider
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
|
||||
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
||||
|
||||
@ -16,7 +16,7 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
||||
end
|
||||
|
||||
def update
|
||||
@hook = channel_builder.update(permitted_params[:reference_id])
|
||||
@hook = channel_builder.update_reference_id(permitted_params[:reference_id])
|
||||
render json: { error: I18n.t('errors.slack.invalid_channel_id') }, status: :unprocessable_entity if @hook.blank?
|
||||
end
|
||||
|
||||
|
||||
@ -25,17 +25,17 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def update
|
||||
@notification.update(read_at: DateTime.now.utc)
|
||||
@notification.update!(read_at: DateTime.now.utc)
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
def unread
|
||||
@notification.update(read_at: nil)
|
||||
@notification.update!(read_at: nil)
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
def destroy
|
||||
@notification.destroy
|
||||
@notification.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
@ -55,7 +55,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def snooze
|
||||
updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil)
|
||||
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until]
|
||||
@notification.update!(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until]
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def archive
|
||||
@portal.update(archive: true)
|
||||
@portal.update!(archive: true)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@ -7,12 +7,12 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def create
|
||||
@webhook = Current.account.webhooks.new(webhook_params)
|
||||
@webhook = Current.account.webhooks.new(webhook_create_params)
|
||||
@webhook.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@webhook.update!(webhook_params)
|
||||
@webhook.update!(webhook_update_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@ -22,8 +22,12 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||
|
||||
private
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
|
||||
def webhook_create_params
|
||||
params.require(:webhook).permit(:inbox_id, :name, :url, subscriptions: [])
|
||||
end
|
||||
|
||||
def webhook_update_params
|
||||
params.require(:webhook).permit(:name, subscriptions: [])
|
||||
end
|
||||
|
||||
def fetch_webhook
|
||||
|
||||
@ -29,7 +29,7 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
end
|
||||
|
||||
def set_active_account
|
||||
@user.account_users.find_by(account_id: profile_params[:account_id]).update(active_at: Time.now.utc)
|
||||
@user.account_users.find_by(account_id: profile_params[:account_id]).update!(active_at: Time.now.utc)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
contact = @contact
|
||||
end
|
||||
|
||||
@contact_inbox.update(hmac_verified: true) if should_verify_hmac? && valid_hmac?
|
||||
@contact_inbox.update!(hmac_verified: true) if should_verify_hmac? && valid_hmac?
|
||||
|
||||
identify_contact(contact)
|
||||
end
|
||||
|
||||
@ -82,7 +82,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def render_not_found_if_empty
|
||||
return head :not_found if conversation.nil?
|
||||
head :not_found if conversation.nil?
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@ -12,7 +12,7 @@ class ApiController < ApplicationController
|
||||
|
||||
def redis_status
|
||||
r = Redis.new(Redis::Config.app)
|
||||
return 'ok' if r.ping
|
||||
'ok' if r.ping
|
||||
rescue Redis::CannotConnectError
|
||||
'failing'
|
||||
end
|
||||
|
||||
@ -5,7 +5,7 @@ class Platform::Api::V1::AccountsController < PlatformController
|
||||
@resource = Account.create!(account_params)
|
||||
update_resource_features
|
||||
@resource.save!
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
@ -12,7 +12,7 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
||||
@resource = AgentBot.new(agent_bot_params.except(:avatar_url))
|
||||
@resource.save!
|
||||
process_avatar_from_url
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
@ -31,7 +31,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
return if params[:identifier_hash].blank? && !@inbox_channel.hmac_mandatory
|
||||
raise StandardError, 'HMAC failed: Invalid Identifier Hash Provided' unless valid_hmac?
|
||||
|
||||
@contact_inbox.update(hmac_verified: true) if @contact_inbox.present?
|
||||
@contact_inbox.update!(hmac_verified: true) if @contact_inbox.present?
|
||||
end
|
||||
|
||||
def valid_hmac?
|
||||
|
||||
@ -6,7 +6,7 @@ class SuperAdmin::AccountUsersController < SuperAdmin::ApplicationController
|
||||
resource = resource_class.new(resource_params)
|
||||
authorize_resource(resource)
|
||||
|
||||
notice = resource.save ? translate_with_resource('create.success') : resource.errors.full_messages.first
|
||||
notice = resource.save ? translate_with_resource('create.success') : resource.errors.full_messages.first
|
||||
redirect_back(fallback_location: [namespace, resource.account], notice: notice)
|
||||
end
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
params['app_config'].each do |key, value|
|
||||
next unless @allowed_configs.include?(key)
|
||||
|
||||
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
||||
i = InstallationConfig.where(name: key).first_or_create!(value: value, locked: false)
|
||||
i.value = value
|
||||
i.save!
|
||||
end
|
||||
|
||||
@ -8,11 +8,28 @@ class Webhooks::WhatsappController < ActionController::API
|
||||
return
|
||||
end
|
||||
|
||||
perform_whatsapp_events_job
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def perform_whatsapp_events_job
|
||||
perform_sync if params[:awaitResponse].present?
|
||||
return if performed?
|
||||
|
||||
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
def perform_sync
|
||||
Webhooks::WhatsappEventsJob.perform_now(params.to_unsafe_hash)
|
||||
rescue Whatsapp::IncomingMessageBaileysService::InvalidWebhookVerifyToken
|
||||
head :unauthorized
|
||||
rescue Whatsapp::IncomingMessageBaileysService::MessageNotFoundError
|
||||
head :not_found
|
||||
rescue Whatsapp::IncomingMessageBaileysService::AttachmentNotFoundError
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
def valid_token?(token)
|
||||
channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
||||
|
||||
@ -10,6 +10,6 @@ module CacheKeysHelper
|
||||
return value_from_cache if value_from_cache.present?
|
||||
|
||||
# zero epoch time: 1970-01-01 00:00:00 UTC
|
||||
'0000000000'
|
||||
'0000000000000'
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
module FrontendUrlsHelper
|
||||
def frontend_url(path, **query_params)
|
||||
url_params = query_params.blank? ? '' : "?#{query_params.to_query}"
|
||||
"#{root_url}app/#{path}#{url_params}"
|
||||
host = ENV.fetch('FRONTEND_URL_EXTERNAL', root_url)
|
||||
host = "#{host}/" unless host.end_with?('/')
|
||||
"#{host}app/#{path}#{url_params}"
|
||||
end
|
||||
end
|
||||
|
||||
@ -52,8 +52,8 @@ module ReportingEventHelper
|
||||
end
|
||||
|
||||
def format_time(hour, minute)
|
||||
hour = hour < 10 ? "0#{hour}" : hour
|
||||
minute = minute < 10 ? "0#{minute}" : minute
|
||||
hour = "0#{hour}" if hour < 10
|
||||
minute = "0#{minute}" if minute < 10
|
||||
"#{hour}:#{minute}"
|
||||
end
|
||||
end
|
||||
|
||||
@ -14,6 +14,6 @@ module TimezoneHelper
|
||||
zone.now.utc_offset == offset_in_seconds
|
||||
end
|
||||
|
||||
return matching_zone.name if matching_zone
|
||||
matching_zone&.name
|
||||
end
|
||||
end
|
||||
|
||||
@ -28,6 +28,14 @@ class Inboxes extends CacheEnabledApiClient {
|
||||
agent_bot: botId,
|
||||
});
|
||||
}
|
||||
|
||||
setupChannelProvider(inboxId) {
|
||||
return axios.post(`${this.url}/${inboxId}/setup_channel_provider`);
|
||||
}
|
||||
|
||||
disconnectChannelProvider(inboxId) {
|
||||
return axios.post(`${this.url}/${inboxId}/disconnect_channel_provider`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
||||
@ -15,6 +15,7 @@ import WhatsAppOptions from './WhatsAppOptions.vue';
|
||||
const props = defineProps({
|
||||
attachedFiles: { type: Array, default: () => [] },
|
||||
isWhatsappInbox: { type: Boolean, default: false },
|
||||
isWhatsappBaileysInbox: { type: Boolean, default: false },
|
||||
isEmailOrWebWidgetInbox: { type: Boolean, default: false },
|
||||
isTwilioSmsInbox: { type: Boolean, default: false },
|
||||
messageTemplates: { type: Array, default: () => [] },
|
||||
@ -216,7 +217,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
@click="emit('discard')"
|
||||
/>
|
||||
<Button
|
||||
v-if="!isWhatsappInbox"
|
||||
v-if="!isWhatsappInbox || isWhatsappBaileysInbox"
|
||||
:label="sendButtonLabel"
|
||||
size="sm"
|
||||
class="!text-xs font-medium"
|
||||
|
||||
@ -66,6 +66,9 @@ const inboxTypes = computed(() => ({
|
||||
isEmail: props.targetInbox?.channelType === INBOX_TYPES.EMAIL,
|
||||
isTwilio: props.targetInbox?.channelType === INBOX_TYPES.TWILIO,
|
||||
isWhatsapp: props.targetInbox?.channelType === INBOX_TYPES.WHATSAPP,
|
||||
isWhatsappBaileys:
|
||||
props.targetInbox?.channelType === INBOX_TYPES.WHATSAPP &&
|
||||
props.targetInbox?.provider === 'baileys',
|
||||
isWebWidget: props.targetInbox?.channelType === INBOX_TYPES.WEB,
|
||||
isApi: props.targetInbox?.channelType === INBOX_TYPES.API,
|
||||
isEmailOrWebWidget:
|
||||
@ -311,7 +314,10 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
||||
/>
|
||||
|
||||
<MessageEditor
|
||||
v-if="!inboxTypes.isWhatsapp && !showNoInboxAlert"
|
||||
v-if="
|
||||
(!inboxTypes.isWhatsapp || inboxTypes.isWhatsappBaileys) &&
|
||||
!showNoInboxAlert
|
||||
"
|
||||
v-model="state.message"
|
||||
:message-signature="messageSignature"
|
||||
:send-with-signature="sendWithSignature"
|
||||
@ -329,6 +335,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
||||
<ActionButtons
|
||||
:attached-files="state.attachedFiles"
|
||||
:is-whatsapp-inbox="inboxTypes.isWhatsapp"
|
||||
:is-whatsapp-baileys-inbox="inboxTypes.isWhatsappBaileys"
|
||||
:is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget"
|
||||
:is-twilio-sms-inbox="inboxTypes.isTwilioSMS"
|
||||
:message-templates="whatsappMessageTemplates"
|
||||
|
||||
@ -44,7 +44,7 @@ const triggerClick = () => {
|
||||
<component
|
||||
:is="componentIs"
|
||||
v-bind="$attrs"
|
||||
class="flex text-left rtl:text-right items-center p-2 reset-base text-sm text-n-slate-12 w-full border-0"
|
||||
class="flex text-left rtl:text-right items-center p-2 reset-base text-sm text-n-slate-12 w-full border-0 cursor-pointer"
|
||||
:class="{
|
||||
'hover:bg-n-alpha-2 rounded-lg w-full gap-3': !$slots.default,
|
||||
}"
|
||||
|
||||
@ -7,6 +7,14 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
withPhoneNumber: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withProviderConnectionStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedInboxClass() {
|
||||
@ -14,6 +22,9 @@ export default {
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
providerConnection() {
|
||||
return this.inbox.provider_connection?.connection;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -28,5 +39,17 @@ export default {
|
||||
size="12"
|
||||
/>
|
||||
{{ inbox.name }}
|
||||
<span v-if="withPhoneNumber" class="ml-2 text-n-slate-12">{{
|
||||
inbox.phone_number
|
||||
}}</span>
|
||||
<span v-if="withProviderConnectionStatus" class="ml-2">
|
||||
<fluent-icon
|
||||
icon="circle"
|
||||
type="filled"
|
||||
:class="
|
||||
providerConnection === 'open' ? 'text-green-500' : 'text-n-slate-8'
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -5,6 +5,7 @@ import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
// components
|
||||
import ReplyBox from './ReplyBox.vue';
|
||||
@ -37,6 +38,7 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import WhatsappBaileysLinkDeviceModal from '../../../routes/dashboard/settings/inbox/components/WhatsappBaileysLinkDeviceModal.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
@ -48,6 +50,7 @@ export default {
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
NextButton,
|
||||
WhatsappBaileysLinkDeviceModal,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@ -62,6 +65,7 @@ export default {
|
||||
},
|
||||
emits: ['contactPanelToggle'],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
const isPopOutReplyBox = ref(false);
|
||||
const { isEnterprise } = useConfig();
|
||||
|
||||
@ -108,6 +112,7 @@ export default {
|
||||
fetchIntegrationsIfRequired,
|
||||
fetchLabelSuggestions,
|
||||
showNextBubbles,
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@ -119,6 +124,7 @@ export default {
|
||||
isProgrammaticScroll: false,
|
||||
messageSentSinceOpened: false,
|
||||
labelSuggestions: [],
|
||||
showBaileysLinkDeviceModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
@ -129,6 +135,9 @@ export default {
|
||||
listLoadingStatus: 'getAllMessagesLoaded',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
currentInbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.currentChat.inbox_id);
|
||||
},
|
||||
isOpen() {
|
||||
return this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
},
|
||||
@ -296,6 +305,9 @@ export default {
|
||||
|
||||
return { incoming, outgoing };
|
||||
},
|
||||
inboxProviderConnection() {
|
||||
return this.currentInbox.provider_connection?.connection;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@ -506,12 +518,48 @@ export default {
|
||||
return false;
|
||||
});
|
||||
},
|
||||
onOpenBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = true;
|
||||
},
|
||||
onCloseBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-between flex-grow h-full min-w-0 m-0">
|
||||
<template v-if="isAWhatsAppBaileysChannel">
|
||||
<WhatsappBaileysLinkDeviceModal
|
||||
v-if="showBaileysLinkDeviceModal"
|
||||
:show="showBaileysLinkDeviceModal"
|
||||
:on-close="onCloseBaileysLinkDeviceModal"
|
||||
:inbox="currentInbox"
|
||||
/>
|
||||
<Banner
|
||||
v-if="inboxProviderConnection !== 'open'"
|
||||
color-scheme="alert"
|
||||
class="mt-2 mx-2 rounded-lg overflow-hidden"
|
||||
:banner-message="
|
||||
isAdmin
|
||||
? $t(
|
||||
'CONVERSATION.INBOX.WHATSAPP_BAILEYS_PROVIDER_CONNECTION.NOT_CONNECTED'
|
||||
)
|
||||
: $t(
|
||||
'CONVERSATION.INBOX.WHATSAPP_BAILEYS_PROVIDER_CONNECTION.NOT_CONNECTED_CONTACT_ADMIN'
|
||||
)
|
||||
"
|
||||
:has-action-button="isAdmin"
|
||||
:action-button-label="
|
||||
$t(
|
||||
'CONVERSATION.INBOX.WHATSAPP_BAILEYS_PROVIDER_CONNECTION.LINK_DEVICE'
|
||||
)
|
||||
"
|
||||
action-button-icon=""
|
||||
@primary-action="onOpenBaileysLinkDeviceModal"
|
||||
/>
|
||||
</template>
|
||||
<Banner
|
||||
v-if="!currentChat.can_reply"
|
||||
color-scheme="alert"
|
||||
|
||||
@ -63,6 +63,7 @@ export default {
|
||||
<label class="input-container">
|
||||
<span v-if="label">{{ label }}</span>
|
||||
<input
|
||||
class="!mb-0"
|
||||
:value="modelValue"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
|
||||
@ -119,8 +119,8 @@ describe('useUISettings', () => {
|
||||
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is not configured', () => {
|
||||
getUISettingsMock.value.editor_message_key = undefined;
|
||||
const { isEditorHotKeyEnabled } = useUISettings();
|
||||
expect(isEditorHotKeyEnabled('enter')).toBe(false);
|
||||
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(true);
|
||||
expect(isEditorHotKeyEnabled('enter')).toBe(true);
|
||||
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(false);
|
||||
});
|
||||
|
||||
it('handles non-existent keys', () => {
|
||||
|
||||
@ -114,6 +114,13 @@ export const useInbox = () => {
|
||||
);
|
||||
});
|
||||
|
||||
const isAWhatsAppBaileysChannel = computed(() => {
|
||||
return (
|
||||
channelType.value === INBOX_TYPES.WHATSAPP &&
|
||||
whatsAppAPIProvider.value === 'baileys'
|
||||
);
|
||||
});
|
||||
|
||||
const isAWhatsAppChannel = computed(() => {
|
||||
return (
|
||||
channelType.value === INBOX_TYPES.WHATSAPP ||
|
||||
@ -140,6 +147,7 @@ export const useInbox = () => {
|
||||
isATwilioWhatsAppChannel,
|
||||
isAWhatsAppCloudChannel,
|
||||
is360DialogWhatsAppChannel,
|
||||
isAWhatsAppBaileysChannel,
|
||||
isAnEmailChannel,
|
||||
isAnInstagramChannel,
|
||||
};
|
||||
|
||||
@ -110,7 +110,7 @@ const isEditorHotKeyEnabled = (key, uiSettings) => {
|
||||
enter_to_send_enabled: enterToSendEnabled,
|
||||
} = uiSettings.value || {};
|
||||
if (!editorMessageKey) {
|
||||
return key === (enterToSendEnabled ? 'enter' : 'cmd_enter');
|
||||
return key === (enterToSendEnabled ? 'cmd_enter' : 'enter');
|
||||
}
|
||||
return editorMessageKey === key;
|
||||
};
|
||||
|
||||
@ -236,6 +236,13 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"INBOX": {
|
||||
"WHATSAPP_BAILEYS_PROVIDER_CONNECTION": {
|
||||
"NOT_CONNECTED": "WhatsApp is not connected. Please link your device again.",
|
||||
"NOT_CONNECTED_CONTACT_ADMIN": "WhatsApp is not connected. Please contact your administrator to link your device again.",
|
||||
"LINK_DEVICE": "Link device"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@ -224,7 +224,8 @@
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "Twilio",
|
||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||
"360_DIALOG": "360Dialog"
|
||||
"360_DIALOG": "360Dialog",
|
||||
"BAILEYS": "Baileys"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Inbox Name",
|
||||
@ -263,6 +264,25 @@
|
||||
"WEBHOOK_URL": "Webhook URL",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||
},
|
||||
"PROVIDER_URL": {
|
||||
"LABEL": "Provider URL",
|
||||
"PLACEHOLDER": "If provider is not running locally, please provide the URL",
|
||||
"ERROR":"Please enter a valid URL"
|
||||
},
|
||||
"ADVANCED_OPTIONS": "Advanced options",
|
||||
"BAILEYS": {
|
||||
"SUBTITLE": "Click below to setup the WhatsApp channel using Baileys.",
|
||||
"LINK_BUTTON": "Link device",
|
||||
"LINK_DEVICE_MODAL": {
|
||||
"TITLE": "Link your device",
|
||||
"SUBTITLE": "Scan the QR code to link your device. Make sure the phone number is correct before scanning.",
|
||||
"LOADING_QRCODE": "Loading QR code...",
|
||||
"RECONNECTING": "Connecting...",
|
||||
"LINK_DEVICE": "Link device",
|
||||
"DISCONNECT": "Disconnect",
|
||||
"CONNECTED": "Your device has been connected successfully. You can now start sending and receiving messages."
|
||||
}
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
@ -532,6 +552,13 @@
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_TITLE": "Manage Provider Connection",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_SUBHEADER": "Link your device and manage the provider connection.",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON": "Manage connection",
|
||||
"WHATSAPP_PROVIDER_URL_TITLE": "Provider URL",
|
||||
"WHATSAPP_PROVIDER_URL_SUBHEADER": "If the provider is not running locally, please provide the URL.",
|
||||
"WHATSAPP_PROVIDER_URL_PLACEHOLDER": "Enter the provider URL",
|
||||
"WHATSAPP_PROVIDER_URL_ERROR": "Please enter a valid URL",
|
||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
||||
},
|
||||
"HELP_CENTER": {
|
||||
|
||||
@ -44,6 +44,17 @@
|
||||
"CONTACT_UPDATED": "Contact updated"
|
||||
}
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Webhook Name",
|
||||
"PLACEHOLDER": "Enter the name of the webhook"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Inbox",
|
||||
"TITLE": "Select the inbox",
|
||||
"PLACEHOLDER": "All Inboxes",
|
||||
"NO_RESULTS": "No inboxes found",
|
||||
"INPUT_PLACEHOLDER": "Search inbox"
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Example: {webhookExampleURL}",
|
||||
|
||||
@ -234,6 +234,13 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contato",
|
||||
"COPILOT": "Copiloto"
|
||||
},
|
||||
"INBOX": {
|
||||
"WHATSAPP_BAILEYS_PROVIDER_CONNECTION": {
|
||||
"NOT_CONNECTED": "O WhatsApp não está conectado. Por favor conecte o seu dispositivo novamente.",
|
||||
"NOT_CONNECTED_CONTACT_ADMIN": "O WhatsApp não está conectado. Por favor contate o seu administrador para conectar o dispositivo novamente.",
|
||||
"LINK_DEVICE": "Conectar dispositivo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@ -214,7 +214,8 @@
|
||||
"LABEL": "Provedor de API",
|
||||
"TWILIO": "Twilio",
|
||||
"WHATSAPP_CLOUD": "Cloud do WhatsApp",
|
||||
"360_DIALOG": "360Dialog"
|
||||
"360_DIALOG": "360Dialog",
|
||||
"BAILEYS": "Baileys"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Nome da Caixa de Entrada",
|
||||
@ -253,6 +254,25 @@
|
||||
"WEBHOOK_URL": "URL do Webhook",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Token de verificação Webhook"
|
||||
},
|
||||
"PROVIDER_URL": {
|
||||
"LABEL": "URL do provedor",
|
||||
"PLACEHOLDER": "Se o provedor não está rodando localmente, por favor, insira a URL do provedor",
|
||||
"ERROR":"Por favor, insira uma URL válida"
|
||||
},
|
||||
"ADVANCED_OPTIONS": "Opções avançadas",
|
||||
"BAILEYS": {
|
||||
"SUBTITLE": "Clique abaixo para configurar o canal do WhatsApp usando o Baileys.",
|
||||
"LINK_BUTTON": "Conectar dispositivo",
|
||||
"LINK_DEVICE_MODAL": {
|
||||
"TITLE": "Conecte o seu dispositivo",
|
||||
"SUBTITLE": "Escaneie o QR code para conectar seu dispositivo. Certifique-se de que o número de telefone esteja correto antes de escanear.",
|
||||
"LOADING_QRCODE": "Carregando QR code...",
|
||||
"RECONNECTING": "Conectando...",
|
||||
"LINK_DEVICE": "Conectar dispositivo",
|
||||
"DISCONNECT": "Desconectar",
|
||||
"CONNECTED": "Seu dispositivo foi conectado com sucesso. Agora você pode começar a enviar e receber mensagens."
|
||||
}
|
||||
},
|
||||
"SUBMIT_BUTTON": "Criar canal do WhatsApp",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Não foi possível salvar o canal do WhatsApp"
|
||||
@ -522,6 +542,13 @@
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Atualizar",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Token de verificação Webhook",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Este token é usado para verificar a autenticidade do webhook endpoint.",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_TITLE": "Gerenciar Conexão do Provedor",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_SUBHEADER": "Conecte o seu dispositivo e gerencie a conexão do provedor.",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON": "Gerenciar conexão",
|
||||
"WHATSAPP_PROVIDER_URL_TITLE": "URL do provedor",
|
||||
"WHATSAPP_PROVIDER_URL_SUBHEADER": "Se o provedor não estiver rodando localmente, por favor, forneça a URL.",
|
||||
"WHATSAPP_PROVIDER_URL_PLACEHOLDER": "Digite a URL do provedor",
|
||||
"WHATSAPP_PROVIDER_URL_ERROR": "Por favor, insira uma URL válida",
|
||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat"
|
||||
},
|
||||
"HELP_CENTER": {
|
||||
|
||||
@ -44,6 +44,17 @@
|
||||
"CONTACT_UPDATED": "Contato atualizado"
|
||||
}
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Nome do Webhook",
|
||||
"PLACEHOLDER": "Insira o nome do webhook"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Caixa de Entrada",
|
||||
"TITLE": "Selecione a caixa de entrada",
|
||||
"PLACEHOLDER": "Todas as caixas de entrada",
|
||||
"NO_RESULTS": "Nenhuma caixa de entrada encontrada",
|
||||
"INPUT_PLACEHOLDER": "Buscar caixa de entrada"
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "URL do Webhook",
|
||||
"PLACEHOLDER": "Exemplo: {webhookExampleURL}",
|
||||
|
||||
@ -3,11 +3,19 @@ import EmptyState from '../../../../components/widgets/EmptyState.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import DuplicateInboxBanner from './channels/instagram/DuplicateInboxBanner.vue';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import WhatsappBaileysLinkDeviceModal from './components/WhatsappBaileysLinkDeviceModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmptyState,
|
||||
NextButton,
|
||||
DuplicateInboxBanner,
|
||||
WhatsappBaileysLinkDeviceModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showBaileysLinkDeviceModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentInbox() {
|
||||
@ -47,6 +55,12 @@ export default {
|
||||
this.currentInbox.provider === 'whatsapp_cloud'
|
||||
);
|
||||
},
|
||||
isWhatsAppBaileysInbox() {
|
||||
return (
|
||||
this.currentInbox.channel_type === 'Channel::Whatsapp' &&
|
||||
this.currentInbox.provider === 'baileys'
|
||||
);
|
||||
},
|
||||
message() {
|
||||
if (this.isATwilioInbox) {
|
||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||
@ -72,6 +86,12 @@ export default {
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.isWhatsAppBaileysInbox) {
|
||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.SUBTITLE'
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.isAEmailInbox && !this.currentInbox.provider) {
|
||||
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
||||
}
|
||||
@ -83,6 +103,14 @@ export default {
|
||||
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onOpenBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = true;
|
||||
},
|
||||
onCloseBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -130,6 +158,11 @@ export default {
|
||||
:script="currentInbox.provider_config.webhook_verify_token"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isWhatsAppBaileysInbox" class="w-[50%] max-w-[50%] ml-[25%]">
|
||||
<NextButton @click="onOpenBaileysLinkDeviceModal">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_BUTTON') }}
|
||||
</NextButton>
|
||||
</div>
|
||||
<div class="w-[50%] max-w-[50%] ml-[25%]">
|
||||
<woot-code
|
||||
v-if="isALineInbox"
|
||||
@ -178,5 +211,12 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</EmptyState>
|
||||
<WhatsappBaileysLinkDeviceModal
|
||||
v-if="showBaileysLinkDeviceModal"
|
||||
:show="showBaileysLinkDeviceModal"
|
||||
:on-close="onCloseBaileysLinkDeviceModal"
|
||||
:inbox="currentInbox"
|
||||
is-setup
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -91,6 +91,9 @@ export default {
|
||||
if (this.isATwilioWhatsAppChannel) {
|
||||
return this.$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO');
|
||||
}
|
||||
if (this.isAWhatsAppBaileysChannel) {
|
||||
return this.$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.BAILEYS');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
tabs() {
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { required, requiredIf } from '@vuelidate/validators';
|
||||
import router from '../../../../index';
|
||||
import { isPhoneE164OrEmpty } from 'shared/helpers/Validators';
|
||||
import { isValidURL } from '../../../../../helper/URLHelper';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inboxName: '',
|
||||
phoneNumber: '',
|
||||
apiKey: '',
|
||||
providerUrl: '',
|
||||
showAdvancedOptions: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
inboxName: { required },
|
||||
phoneNumber: { required, isPhoneE164OrEmpty },
|
||||
providerUrl: {
|
||||
isValidURL: value => !value || isValidURL(value),
|
||||
requiredIf: requiredIf(this.apiKey),
|
||||
},
|
||||
apiKey: { requiredIf: requiredIf(this.providerUrl) },
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async createChannel() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const whatsappChannel = await this.$store.dispatch(
|
||||
'inboxes/createChannel',
|
||||
{
|
||||
name: this.inboxName,
|
||||
channel: {
|
||||
type: 'whatsapp',
|
||||
phone_number: this.phoneNumber,
|
||||
provider: 'baileys',
|
||||
provider_config:
|
||||
this.apiKey || this.providerUrl
|
||||
? {
|
||||
api_key: this.apiKey,
|
||||
url: this.providerUrl,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
router.replace({
|
||||
name: 'settings_inboxes_add_agents',
|
||||
params: {
|
||||
page: 'new',
|
||||
inbox_id: whatsappChannel.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
setShowAdvancedOptions() {
|
||||
this.showAdvancedOptions = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="flex flex-wrap mx-0" @submit.prevent="createChannel()">
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<label :class="{ error: v$.inboxName.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.LABEL') }}
|
||||
<input
|
||||
v-model="inboxName"
|
||||
type="text"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.PLACEHOLDER')"
|
||||
@blur="v$.inboxName.$touch"
|
||||
/>
|
||||
<span v-if="v$.inboxName.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<label :class="{ error: v$.phoneNumber.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.LABEL') }}
|
||||
<input
|
||||
v-model="phoneNumber"
|
||||
type="text"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.PLACEHOLDER')"
|
||||
@blur="v$.phoneNumber.$touch"
|
||||
/>
|
||||
<span v-if="v$.phoneNumber.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!showAdvancedOptions"
|
||||
class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%] mb-4"
|
||||
>
|
||||
<NextButton icon="i-lucide-plus" sm link @click="setShowAdvancedOptions">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.ADVANCED_OPTIONS') }}
|
||||
</NextButton>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<span class="text-sm text-gray-600">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.ADVANCED_OPTIONS') }}
|
||||
</span>
|
||||
<label :class="{ error: v$.providerUrl.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDER_URL.LABEL') }}
|
||||
<input
|
||||
v-model="providerUrl"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDER_URL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<span v-if="v$.providerUrl.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDER_URL.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<label :class="{ error: v$.apiKey.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.LABEL') }}
|
||||
<input
|
||||
v-model="apiKey"
|
||||
type="text"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.PLACEHOLDER')"
|
||||
/>
|
||||
<span v-if="v$.apiKey.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="w-full">
|
||||
<NextButton
|
||||
:is-loading="uiFlags.isCreating"
|
||||
type="submit"
|
||||
solid
|
||||
blue
|
||||
:label="$t('INBOX_MGMT.ADD.WHATSAPP.SUBMIT_BUTTON')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@ -3,6 +3,7 @@ import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
import Twilio from './Twilio.vue';
|
||||
import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue';
|
||||
import CloudWhatsapp from './CloudWhatsapp.vue';
|
||||
import BaileysWhatsapp from './BaileysWhatsapp.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -10,6 +11,7 @@ export default {
|
||||
Twilio,
|
||||
ThreeSixtyDialogWhatsapp,
|
||||
CloudWhatsapp,
|
||||
BaileysWhatsapp,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -37,12 +39,16 @@ export default {
|
||||
<option value="twilio">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO') }}
|
||||
</option>
|
||||
<option value="baileys">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.BAILEYS') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Twilio v-if="provider === 'twilio'" type="whatsapp" />
|
||||
<ThreeSixtyDialogWhatsapp v-else-if="provider === '360dialog'" />
|
||||
<CloudWhatsapp v-else />
|
||||
<CloudWhatsapp v-if="provider === 'whatsapp_cloud'" />
|
||||
<BaileysWhatsapp v-if="provider === 'baileys'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -0,0 +1,161 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, onUnmounted, ref, watchEffect } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, required: true },
|
||||
onClose: { type: Function, required: true },
|
||||
isSetup: { type: Boolean, required: false },
|
||||
inbox: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const providerConnection = computed(() => props.inbox.provider_connection);
|
||||
const connection = computed(() => providerConnection.value?.connection);
|
||||
const qrDataUrl = computed(() => providerConnection.value?.qr_data_url);
|
||||
const error = computed(() => providerConnection.value?.error);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const handleError = e => {
|
||||
useAlert(e.message);
|
||||
loading.value = false;
|
||||
};
|
||||
const setup = () => {
|
||||
loading.value = true;
|
||||
store
|
||||
.dispatch('inboxes/setupChannelProvider', props.inbox.id)
|
||||
.catch(handleError);
|
||||
};
|
||||
const disconnect = () => {
|
||||
loading.value = true;
|
||||
store
|
||||
.dispatch('inboxes/disconnectChannelProvider', props.inbox.id)
|
||||
.catch(handleError);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!connection.value || connection.value === 'close') {
|
||||
setup();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (connection.value === 'connecting') {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
watchEffect(() => {
|
||||
if (connection.value) {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal :show="show" size="small" @close="onClose">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="
|
||||
$t('INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.TITLE')
|
||||
"
|
||||
:header-content="
|
||||
$t('INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.SUBTITLE')
|
||||
"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-4 p-8 pt-4">
|
||||
<div class="flex flex-col gap-4 items-center">
|
||||
<InboxName
|
||||
:inbox="inbox"
|
||||
class="!text-lg"
|
||||
with-phone-number
|
||||
with-provider-connection-status
|
||||
/>
|
||||
|
||||
<template v-if="!connection || connection === 'close' || error">
|
||||
<p v-if="error" class="text-red-500 text-center">
|
||||
{{ error }}
|
||||
</p>
|
||||
<Button :is-loading="loading" @click="setup">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.LINK_DEVICE'
|
||||
)
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="connection === 'connecting'">
|
||||
<div v-if="!qrDataUrl" class="flex flex-col gap-4 items-center">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.LOADING_QRCODE'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="qrDataUrl"
|
||||
alt="QR Code"
|
||||
class="w-[276px] h-[276px]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="connection === 'reconnecting'">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.RECONNECTING'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<Spinner />
|
||||
</template>
|
||||
|
||||
<template v-else-if="connection === 'open'">
|
||||
<p v-if="isSetup" class="text-center">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.CONNECTED'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<Button ghost :is-loading="loading" @click="disconnect">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.DISCONNECT'
|
||||
)
|
||||
}}
|
||||
</Button>
|
||||
<router-link
|
||||
v-if="isSetup"
|
||||
:to="{
|
||||
name: 'inbox_dashboard',
|
||||
params: { inboxId: inbox.id },
|
||||
}"
|
||||
>
|
||||
<Button
|
||||
solid
|
||||
teal
|
||||
:label="$t('INBOX_MGMT.FINISH.BUTTON_TEXT')"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
@ -5,8 +5,11 @@ import SettingsSection from '../../../../../components/SettingsSection.vue';
|
||||
import ImapSettings from '../ImapSettings.vue';
|
||||
import SmtpSettings from '../SmtpSettings.vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
import { isValidURL } from '../../../../../helper/URLHelper';
|
||||
import WhatsappBaileysLinkDeviceModal from '../components/WhatsappBaileysLinkDeviceModal.vue';
|
||||
import InboxName from '../../../../../components/widgets/InboxName.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -14,6 +17,8 @@ export default {
|
||||
ImapSettings,
|
||||
SmtpSettings,
|
||||
NextButton,
|
||||
WhatsappBaileysLinkDeviceModal,
|
||||
InboxName,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@ -29,10 +34,17 @@ export default {
|
||||
return {
|
||||
hmacMandatory: false,
|
||||
whatsAppInboxAPIKey: '',
|
||||
whatsAppProviderUrl: '',
|
||||
showBaileysLinkDeviceModal: false,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
whatsAppInboxAPIKey: { required },
|
||||
validations() {
|
||||
return {
|
||||
whatsAppInboxAPIKey: {
|
||||
requiredIf: requiredIf(!this.isAWhatsAppBaileysChannel),
|
||||
},
|
||||
whatsAppProviderUrl: { isValidURL: value => !value || isValidURL(value) },
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
inbox() {
|
||||
@ -83,6 +95,31 @@ export default {
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
async updateWhatsAppProviderUrl() {
|
||||
try {
|
||||
const payload = {
|
||||
id: this.inbox.id,
|
||||
formData: false,
|
||||
channel: {
|
||||
provider_config: {
|
||||
...this.inbox.provider_config,
|
||||
provider_url: this.whatsAppProviderUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await this.$store.dispatch('inboxes/updateInbox', payload);
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
onOpenBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = true;
|
||||
},
|
||||
onCloseBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -194,8 +231,8 @@ export default {
|
||||
<ImapSettings :inbox="inbox" />
|
||||
<SmtpSettings v-if="inbox.imap_enabled" :inbox="inbox" />
|
||||
</div>
|
||||
<div v-else-if="isAWhatsAppChannel && !isATwilioChannel">
|
||||
<div v-if="inbox.provider_config" class="mx-8">
|
||||
<div v-else-if="isAWhatsAppCloudChannel && inbox.provider_config">
|
||||
<div class="mx-8">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_WEBHOOK_TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_WEBHOOK_SUBHEADER')"
|
||||
@ -237,6 +274,117 @@ export default {
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isAWhatsAppBaileysChannel">
|
||||
<WhatsappBaileysLinkDeviceModal
|
||||
v-if="showBaileysLinkDeviceModal"
|
||||
:show="showBaileysLinkDeviceModal"
|
||||
:on-close="onCloseBaileysLinkDeviceModal"
|
||||
:inbox="inbox"
|
||||
/>
|
||||
<div class="mx-8">
|
||||
<SettingsSection
|
||||
:title="
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_TITLE'
|
||||
)
|
||||
"
|
||||
:sub-title="
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_SUBHEADER'
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<InboxName
|
||||
:inbox="inbox"
|
||||
class="!text-lg !m-0"
|
||||
with-phone-number
|
||||
with-provider-connection-status
|
||||
/>
|
||||
<NextButton class="w-fit" @click="onOpenBaileysLinkDeviceModal">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON'
|
||||
)
|
||||
}}
|
||||
</NextButton>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_TITLE')"
|
||||
:sub-title="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_SUBHEADER')
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between flex-1 mt-2 whatsapp-settings--content"
|
||||
>
|
||||
<woot-input
|
||||
v-model="whatsAppProviderUrl"
|
||||
type="text"
|
||||
class="flex-1 mr-2 items-center"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_PLACEHOLDER')
|
||||
"
|
||||
@keydown="v$.whatsAppProviderUrl.$touch"
|
||||
/>
|
||||
<NextButton
|
||||
:disabled="
|
||||
v$.whatsAppProviderUrl.$invalid ||
|
||||
whatsAppProviderUrl === inbox.provider_config.provider_url
|
||||
"
|
||||
@click="updateWhatsAppProviderUrl"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }}
|
||||
</NextButton>
|
||||
</div>
|
||||
<span v-if="v$.whatsAppProviderUrl.$error" class="text-red-400">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_ERROR') }}
|
||||
</span>
|
||||
</SettingsSection>
|
||||
<template v-if="inbox.provider_config.api_key">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_TITLE')"
|
||||
:sub-title="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_SUBHEADER')
|
||||
"
|
||||
>
|
||||
<woot-code :script="inbox.provider_config.api_key" />
|
||||
</SettingsSection>
|
||||
</template>
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_TITLE')"
|
||||
:sub-title="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_SUBHEADER')
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between flex-1 mt-2 whatsapp-settings--content"
|
||||
>
|
||||
<woot-input
|
||||
v-model="whatsAppInboxAPIKey"
|
||||
type="text"
|
||||
class="flex-1 mr-2"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<NextButton
|
||||
:disabled="
|
||||
v$.whatsAppInboxAPIKey.$invalid ||
|
||||
(!inbox.provider_config.api_key && !whatsAppInboxAPIKey) ||
|
||||
whatsAppInboxAPIKey === inbox.provider_config.api_key
|
||||
"
|
||||
@click="updateWhatsAppInboxAPIKey"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }}
|
||||
</NextButton>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -53,6 +53,7 @@ export default {
|
||||
:value="value"
|
||||
:is-submitting="uiFlags.updatingItem"
|
||||
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.EDIT_SUBMIT')"
|
||||
is-editing
|
||||
@submit="onSubmit"
|
||||
@cancel="onClose"
|
||||
/>
|
||||
|
||||
@ -4,6 +4,8 @@ import { required, url, minLength } from '@vuelidate/validators';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
const { EXAMPLE_WEBHOOK_URL } = wootConstants;
|
||||
|
||||
@ -21,6 +23,7 @@ const SUPPORTED_WEBHOOK_EVENTS = [
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
MultiselectDropdown,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
@ -35,10 +38,17 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['submit', 'cancel'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
return {
|
||||
v$: useVuelidate(),
|
||||
inboxes: useMapGetter('inboxes/getInboxes'),
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
url: {
|
||||
@ -53,11 +63,27 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
url: this.value.url || '',
|
||||
assignedInbox: this.value.inbox || null,
|
||||
name: this.value.name || '',
|
||||
subscriptions: this.value.subscriptions || [],
|
||||
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inboxesList() {
|
||||
if (this.assignedInbox?.id) {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: this.$t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.PLACEHOLDER'
|
||||
),
|
||||
},
|
||||
...this.inboxes,
|
||||
];
|
||||
}
|
||||
return this.inboxes;
|
||||
},
|
||||
webhookURLInputPlaceholder() {
|
||||
return this.$t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.PLACEHOLDER',
|
||||
@ -66,14 +92,22 @@ export default {
|
||||
}
|
||||
);
|
||||
},
|
||||
webhookNameInputPlaceholder() {
|
||||
return this.$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.PLACEHOLDER');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$emit('submit', {
|
||||
url: this.url,
|
||||
inbox_id: this.assignedInbox?.id || null,
|
||||
name: this.name,
|
||||
subscriptions: this.subscriptions,
|
||||
});
|
||||
},
|
||||
onClickAssignInbox(inbox) {
|
||||
this.assignedInbox = inbox;
|
||||
},
|
||||
getI18nKey,
|
||||
},
|
||||
};
|
||||
@ -88,6 +122,7 @@ export default {
|
||||
v-model="url"
|
||||
type="text"
|
||||
name="url"
|
||||
:disabled="isEditing"
|
||||
:placeholder="webhookURLInputPlaceholder"
|
||||
@input="v$.url.$touch"
|
||||
/>
|
||||
@ -95,6 +130,38 @@ export default {
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.LABEL') }}
|
||||
<div class="multiselect-wrap--small">
|
||||
<MultiselectDropdown
|
||||
:options="inboxesList"
|
||||
:selected-item="assignedInbox"
|
||||
:multiselector-title="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.TITLE')
|
||||
"
|
||||
:multiselector-placeholder="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.NO_RESULTS')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.INPUT_PLACEHOLDER')
|
||||
"
|
||||
:disabled="isEditing"
|
||||
@select="onClickAssignInbox"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
name="name"
|
||||
:placeholder="webhookNameInputPlaceholder"
|
||||
/>
|
||||
</label>
|
||||
<label :class="{ error: v$.url.$error }" class="mb-2">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }}
|
||||
</label>
|
||||
@ -125,7 +192,6 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
|
||||
@ -3,6 +3,7 @@ import { computed } from 'vue';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import InboxName from 'components/widgets/InboxName.vue';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
@ -37,8 +38,19 @@ const subscribedEvents = computed(() => {
|
||||
<template>
|
||||
<tr>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="font-medium break-words text-slate-700 dark:text-slate-100">
|
||||
{{ webhook.url }}
|
||||
<InboxName v-if="webhook.inbox" class="!mx-0" :inbox="webhook.inbox" />
|
||||
<div
|
||||
class="flex gap-2 font-medium break-words text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
<template v-if="webhook.name">
|
||||
{{ webhook.name }}
|
||||
<span class="text-slate-500 dark:text-slate-400">
|
||||
{{ webhook.url }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ webhook.url }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="block mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
<span class="font-medium">
|
||||
|
||||
@ -282,6 +282,20 @@ export const actions = {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
setupChannelProvider: async (_, inboxId) => {
|
||||
try {
|
||||
await InboxesAPI.setupChannelProvider(inboxId);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
disconnectChannelProvider: async (_, inboxId) => {
|
||||
try {
|
||||
await InboxesAPI.disconnectChannelProvider(inboxId);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
|
||||
@ -285,5 +285,7 @@
|
||||
"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",
|
||||
"circle-outline": "M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20Z",
|
||||
"circle-filled": "M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
|
||||
}
|
||||
|
||||
@ -36,6 +36,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'Search',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
@ -66,6 +70,8 @@ const hasValue = computed(() => {
|
||||
showSearchDropdown ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'
|
||||
"
|
||||
class="w-full !px-2"
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
@click="
|
||||
() => toggleDropdown() // ensure that the event is not passed to the button
|
||||
"
|
||||
@ -90,9 +96,11 @@ const hasValue = computed(() => {
|
||||
:username="selectedItem.name"
|
||||
/>
|
||||
</Button>
|
||||
<!-- NOTE: Without @click.prevent, the dropdown does not behave as expected when used inside a <label> tag. -->
|
||||
<div
|
||||
:class="{ 'dropdown-pane--open': showSearchDropdown }"
|
||||
class="dropdown-pane"
|
||||
@click.prevent
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h4
|
||||
|
||||
@ -86,6 +86,12 @@ export default {
|
||||
this.whatsAppAPIProvider === 'default'
|
||||
);
|
||||
},
|
||||
isAWhatsAppBaileysChannel() {
|
||||
return (
|
||||
this.channelType === INBOX_TYPES.WHATSAPP &&
|
||||
this.whatsAppAPIProvider === 'baileys'
|
||||
);
|
||||
},
|
||||
chatAdditionalAttributes() {
|
||||
const { additional_attributes: additionalAttributes } = this.chat || {};
|
||||
return additionalAttributes || {};
|
||||
|
||||
@ -269,6 +269,14 @@ describe('inboxMixin', () => {
|
||||
expect(wrapper.vm.is360DialogWhatsAppChannel).toBe(true);
|
||||
});
|
||||
|
||||
it('isAWhatsAppBaileysChannel returns true if channel type is WhatsApp and provider is baileys', () => {
|
||||
const Component = getComponentConfigForInbox('Channel::Whatsapp', {
|
||||
provider: 'baileys',
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isAWhatsAppBaileysChannel).toBe(true);
|
||||
});
|
||||
|
||||
it('isAWhatsAppChannel returns true if channel type is WhatsApp', () => {
|
||||
const Component = getComponentConfigForInbox('Channel::Whatsapp');
|
||||
const wrapper = shallowMount(Component);
|
||||
|
||||
@ -26,7 +26,7 @@ class BulkActionsJob < ApplicationJob
|
||||
records.each do |conversation|
|
||||
bulk_add_labels(conversation)
|
||||
bulk_snoozed_until(conversation)
|
||||
conversation.update(params) if params
|
||||
conversation.update!(params) if params
|
||||
end
|
||||
end
|
||||
|
||||
@ -54,7 +54,7 @@ class BulkActionsJob < ApplicationJob
|
||||
return unless @params[:labels] && @params[:labels][:remove]
|
||||
|
||||
labels = conversation.label_list - @params[:labels][:remove]
|
||||
conversation.update(label_list: labels)
|
||||
conversation.update!(label_list: labels)
|
||||
end
|
||||
|
||||
def records_to_updated(ids)
|
||||
|
||||
@ -17,7 +17,7 @@ class Conversations::UserMentionJob < ApplicationJob
|
||||
account_id: account_id
|
||||
)
|
||||
else
|
||||
mention.update(mentioned_at: Time.zone.now)
|
||||
mention.update!(mentioned_at: Time.zone.now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -2,8 +2,7 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(params = {})
|
||||
channel = find_channel_from_whatsapp_business_payload(params)
|
||||
|
||||
channel = find_channel(params)
|
||||
if channel_is_inactive?(channel)
|
||||
Rails.logger.warn("Inactive WhatsApp channel: #{channel&.phone_number || "unknown - #{params[:phone_number]}"}")
|
||||
return
|
||||
@ -12,6 +11,8 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
case channel.provider
|
||||
when 'whatsapp_cloud'
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params).perform
|
||||
when 'baileys'
|
||||
Whatsapp::IncomingMessageBaileysService.new(inbox: channel.inbox, params: params).perform
|
||||
else
|
||||
Whatsapp::IncomingMessageService.new(inbox: channel.inbox, params: params).perform
|
||||
end
|
||||
@ -19,6 +20,14 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
|
||||
private
|
||||
|
||||
def find_channel(params)
|
||||
return find_channel_from_whatsapp_business_payload(params) if params[:object] == 'whatsapp_business_account'
|
||||
|
||||
return unless params[:phone_number]
|
||||
|
||||
Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
||||
end
|
||||
|
||||
def channel_is_inactive?(channel)
|
||||
return true if channel.blank?
|
||||
return true if channel.reauthorization_required?
|
||||
@ -27,26 +36,14 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
false
|
||||
end
|
||||
|
||||
def find_channel_by_url_param(params)
|
||||
return unless params[:phone_number]
|
||||
|
||||
Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
||||
end
|
||||
|
||||
def find_channel_from_whatsapp_business_payload(params)
|
||||
# for the case where facebook cloud api support multiple numbers for a single app
|
||||
# https://github.com/chatwoot/chatwoot/issues/4712#issuecomment-1173838350
|
||||
# we will give priority to the phone_number in the payload
|
||||
return get_channel_from_wb_payload(params) if params[:object] == 'whatsapp_business_account'
|
||||
|
||||
find_channel_by_url_param(params)
|
||||
end
|
||||
|
||||
def get_channel_from_wb_payload(wb_params)
|
||||
phone_number = "+#{wb_params[:entry].first[:changes].first.dig(:value, :metadata, :display_phone_number)}"
|
||||
phone_number_id = wb_params[:entry].first[:changes].first.dig(:value, :metadata, :phone_number_id)
|
||||
phone_number = "+#{params[:entry].first[:changes].first.dig(:value, :metadata, :display_phone_number)}"
|
||||
phone_number_id = params[:entry].first[:changes].first.dig(:value, :metadata, :phone_number_id)
|
||||
channel = Channel::Whatsapp.find_by(phone_number: phone_number)
|
||||
# validate to ensure the phone number id matches the whatsapp channel
|
||||
return channel if channel && channel.provider_config['phone_number_id'] == phone_number_id
|
||||
channel if channel && channel.provider_config['phone_number_id'] == phone_number_id
|
||||
end
|
||||
end
|
||||
|
||||
@ -53,8 +53,8 @@ class AgentBotListener < BaseListener
|
||||
private
|
||||
|
||||
def connected_agent_bot_exist?(inbox)
|
||||
return if inbox.agent_bot_inbox.blank?
|
||||
return unless inbox.agent_bot_inbox.active?
|
||||
return false if inbox.agent_bot_inbox.blank?
|
||||
return false unless inbox.agent_bot_inbox.active?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
@ -70,7 +70,7 @@ class AutomationRuleListener < BaseListener
|
||||
end
|
||||
|
||||
def rule_present?(event_name, account)
|
||||
return if account.blank?
|
||||
return false if account.blank?
|
||||
|
||||
current_account_rules(event_name, account).any?
|
||||
end
|
||||
|
||||
@ -88,6 +88,7 @@ class WebhookListener < BaseListener
|
||||
def deliver_account_webhooks(payload, account)
|
||||
account.webhooks.account_type.each do |webhook|
|
||||
next unless webhook.subscriptions.include?(payload[:event])
|
||||
next if payload[:inbox].present? && webhook.inbox_id.present? && webhook.inbox_id != payload[:inbox][:id]
|
||||
|
||||
WebhookJob.perform_later(webhook.url, payload)
|
||||
end
|
||||
|
||||
@ -39,7 +39,7 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
init_conversation_attributes(message.conversation)
|
||||
@message = message
|
||||
reply_mail_object = prepare_mail(true)
|
||||
message.update(source_id: reply_mail_object.message_id)
|
||||
message.update!(source_id: reply_mail_object.message_id)
|
||||
end
|
||||
|
||||
def conversation_transcript(conversation, to_email)
|
||||
@ -76,8 +76,8 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
|
||||
def conversation_already_viewed?
|
||||
# whether contact already saw the message on widget
|
||||
return unless @conversation.contact_last_seen_at
|
||||
return unless last_outgoing_message&.created_at
|
||||
return false unless @conversation.contact_last_seen_at
|
||||
return false unless last_outgoing_message&.created_at
|
||||
|
||||
@conversation.contact_last_seen_at > last_outgoing_message&.created_at
|
||||
end
|
||||
|
||||
@ -107,7 +107,7 @@ class Article < ApplicationRecord
|
||||
|
||||
root_article_id = self.class.find_root_article_id(article)
|
||||
|
||||
update(associated_article_id: root_article_id) if root_article_id.present?
|
||||
update!(associated_article_id: root_article_id) if root_article_id.present?
|
||||
end
|
||||
|
||||
# Make sure we always associate the parent's associated id to avoid the deeper associations od articles.
|
||||
|
||||
@ -123,9 +123,9 @@ class Attachment < ApplicationRecord
|
||||
end
|
||||
|
||||
def should_validate_file?
|
||||
return unless file.attached?
|
||||
return false unless file.attached?
|
||||
# we are only limiting attachment types in case of website widget
|
||||
return unless message.inbox.channel_type == 'Channel::WebWidget'
|
||||
return false unless message.inbox.channel_type == 'Channel::WebWidget'
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
@ -45,7 +45,7 @@ class Channel::TwilioSms < ApplicationRecord
|
||||
params = send_message_from.merge(to: to, body: body)
|
||||
params[:media_url] = media_url if media_url.present?
|
||||
params[:status_callback] = twilio_delivery_status_index_url
|
||||
client.messages.create(**params)
|
||||
client.messages.create!(**params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
# phone_number :string not null
|
||||
# provider :string default("default")
|
||||
# provider_config :jsonb
|
||||
# provider_connection :jsonb
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
@ -25,22 +26,29 @@ 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 baileys].freeze
|
||||
before_validation :ensure_webhook_verify_token
|
||||
|
||||
validates :provider, inclusion: { in: PROVIDERS }
|
||||
validates :phone_number, presence: true, uniqueness: true
|
||||
validate :validate_provider_config
|
||||
|
||||
has_one :inbox, as: :channel, dependent: :destroy
|
||||
|
||||
after_create :sync_templates
|
||||
|
||||
before_destroy :disconnect_channel_provider, if: -> { provider == 'baileys' }
|
||||
|
||||
def name
|
||||
'Whatsapp'
|
||||
end
|
||||
|
||||
def provider_service
|
||||
if provider == 'whatsapp_cloud'
|
||||
case provider
|
||||
when 'whatsapp_cloud'
|
||||
Whatsapp::Providers::WhatsappCloudService.new(whatsapp_channel: self)
|
||||
when 'baileys'
|
||||
Whatsapp::Providers::WhatsappBaileysService.new(whatsapp_channel: self)
|
||||
else
|
||||
Whatsapp::Providers::Whatsapp360DialogService.new(whatsapp_channel: self)
|
||||
end
|
||||
@ -52,6 +60,23 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def update_provider_connection!(provider_connection)
|
||||
assign_attributes(provider_connection: provider_connection)
|
||||
# NOTE: Skip `validate_provider_config?` check
|
||||
save!(validate: false)
|
||||
end
|
||||
|
||||
def provider_connection_data
|
||||
data = { connection: provider_connection['connection'] }
|
||||
if Current.account_user&.administrator?
|
||||
data[:qr_data_url] = provider_connection['qr_data_url']
|
||||
data[:error] = provider_connection['error']
|
||||
end
|
||||
data
|
||||
end
|
||||
|
||||
delegate :setup_channel_provider, to: :provider_service
|
||||
delegate :disconnect_channel_provider, to: :provider_service
|
||||
delegate :send_message, to: :provider_service
|
||||
delegate :send_template, to: :provider_service
|
||||
delegate :sync_templates, to: :provider_service
|
||||
@ -61,7 +86,7 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
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 provider.in?(%w[whatsapp_cloud baileys])
|
||||
end
|
||||
|
||||
def validate_provider_config
|
||||
|
||||
@ -37,7 +37,8 @@ module CacheKeys
|
||||
|
||||
def update_cache_key_for_account(account_id, key)
|
||||
prefixed_cache_key = get_prefixed_cache_key(account_id, key)
|
||||
Redis::Alfred.setex(prefixed_cache_key, Time.now.utc.to_i, CACHE_KEYS_EXPIRY)
|
||||
timestamp = (Time.now.utc.to_f * 1000).to_i
|
||||
Redis::Alfred.setex(prefixed_cache_key, timestamp, CACHE_KEYS_EXPIRY)
|
||||
end
|
||||
|
||||
def dispatch_cache_update_event
|
||||
|
||||
@ -37,7 +37,7 @@ class ContentAttributeValidator < ActiveModel::Validator
|
||||
end
|
||||
|
||||
def validate_item_actions!(record)
|
||||
if record.items.select { |item| item[:actions].blank? }.present?
|
||||
if record.items.select { |item| item[:actions].blank? }.present? # rubocop:disable Style/RedundantFilterChain
|
||||
record.errors.add(:content_attributes, 'contains items missing actions') && return
|
||||
end
|
||||
|
||||
|
||||
@ -3,12 +3,12 @@ module ConversationMuteHelpers
|
||||
|
||||
def mute!
|
||||
resolved!
|
||||
contact.update(blocked: true)
|
||||
contact.update!(blocked: true)
|
||||
create_muted_message
|
||||
end
|
||||
|
||||
def unmute!
|
||||
contact.update(blocked: false)
|
||||
contact.update!(blocked: false)
|
||||
create_unmuted_message
|
||||
end
|
||||
|
||||
|
||||
@ -25,9 +25,9 @@ module Featurable
|
||||
end
|
||||
end
|
||||
|
||||
def enable_features!(*names)
|
||||
enable_features(*names)
|
||||
save
|
||||
def enable_features!(*)
|
||||
enable_features(*)
|
||||
save!
|
||||
end
|
||||
|
||||
def disable_features(*names)
|
||||
@ -36,9 +36,9 @@ module Featurable
|
||||
end
|
||||
end
|
||||
|
||||
def disable_features!(*names)
|
||||
disable_features(*names)
|
||||
save
|
||||
def disable_features!(*)
|
||||
disable_features(*)
|
||||
save!
|
||||
end
|
||||
|
||||
def feature_enabled?(name)
|
||||
|
||||
@ -134,12 +134,12 @@ class Conversation < ApplicationRecord
|
||||
# FIXME: implement state machine with aasm
|
||||
self.status = open? ? :resolved : :open
|
||||
self.status = :open if pending? || snoozed?
|
||||
save
|
||||
save!
|
||||
end
|
||||
|
||||
def toggle_priority(priority = nil)
|
||||
self.priority = priority.presence
|
||||
save
|
||||
save!
|
||||
end
|
||||
|
||||
def bot_handoff!
|
||||
@ -265,8 +265,9 @@ class Conversation < ApplicationRecord
|
||||
|
||||
def conversation_status_changed_to_open?
|
||||
return false unless open?
|
||||
|
||||
# saved_change_to_status? method only works in case of update
|
||||
return true if previous_changes.key?(:id) || saved_change_to_status?
|
||||
true if previous_changes.key?(:id) || saved_change_to_status?
|
||||
end
|
||||
|
||||
def create_label_change(user_name)
|
||||
|
||||
@ -268,7 +268,7 @@ class Message < ApplicationRecord
|
||||
end
|
||||
|
||||
def update_contact_activity
|
||||
sender.update(last_activity_at: DateTime.now) if sender.is_a?(Contact)
|
||||
sender.update!(last_activity_at: DateTime.now) if sender.is_a?(Contact)
|
||||
end
|
||||
|
||||
def update_waiting_since
|
||||
@ -276,9 +276,9 @@ class Message < ApplicationRecord
|
||||
Rails.configuration.dispatcher.dispatch(
|
||||
REPLY_CREATED, Time.zone.now, waiting_since: conversation.waiting_since, message: self
|
||||
)
|
||||
conversation.update(waiting_since: nil)
|
||||
conversation.update!(waiting_since: nil)
|
||||
end
|
||||
conversation.update(waiting_since: created_at) if incoming? && conversation.waiting_since.blank?
|
||||
conversation.update!(waiting_since: created_at) if incoming? && conversation.waiting_since.blank?
|
||||
end
|
||||
|
||||
def human_response?
|
||||
@ -296,7 +296,7 @@ class Message < ApplicationRecord
|
||||
|
||||
if valid_first_reply?
|
||||
Rails.configuration.dispatcher.dispatch(FIRST_REPLY_CREATED, Time.zone.now, message: self, performed_by: Current.executed_by)
|
||||
conversation.update(first_reply_created_at: created_at, waiting_since: nil)
|
||||
conversation.update!(first_reply_created_at: created_at, waiting_since: nil)
|
||||
else
|
||||
update_waiting_since
|
||||
end
|
||||
@ -359,9 +359,9 @@ class Message < ApplicationRecord
|
||||
end
|
||||
|
||||
def can_notify_via_mail?
|
||||
return unless email_notifiable_message?
|
||||
return unless email_notifiable_channel?
|
||||
return if conversation.contact.email.blank?
|
||||
return false unless email_notifiable_message?
|
||||
return false unless email_notifiable_channel?
|
||||
return false if conversation.contact.email.blank?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
@ -66,6 +66,6 @@ class Portal < ApplicationRecord
|
||||
def config_json_format
|
||||
config['default_locale'] = default_locale
|
||||
denied_keys = config.keys - CONFIG_JSON_KEYS
|
||||
errors.add(:cofig, "in portal on #{denied_keys.join(',')} is not supported.") if denied_keys.any?
|
||||
errors.add(:config, "in portal on #{denied_keys.join(',')} is not supported.") if denied_keys.any?
|
||||
end
|
||||
end
|
||||
|
||||
@ -36,7 +36,7 @@ class Team < ApplicationRecord
|
||||
# @return [Array<User>] Array of newly added members
|
||||
def add_members(user_ids)
|
||||
team_members_to_create = user_ids.map { |user_id| { user_id: user_id } }
|
||||
created_members = team_members.create(team_members_to_create)
|
||||
created_members = team_members.create!(team_members_to_create)
|
||||
added_users = created_members.filter_map(&:user)
|
||||
|
||||
update_account_cache
|
||||
|
||||
@ -109,8 +109,8 @@ class User < ApplicationRecord
|
||||
self.email = email.try(:downcase)
|
||||
end
|
||||
|
||||
def send_devise_notification(notification, *args)
|
||||
devise_mailer.with(account: Current.account).send(notification, self, *args).deliver_later
|
||||
def send_devise_notification(notification, *)
|
||||
devise_mailer.with(account: Current.account).send(notification, self, *).deliver_later
|
||||
end
|
||||
|
||||
def set_password_and_uid
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
# Table name: webhooks
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string
|
||||
# subscriptions :jsonb
|
||||
# url :string
|
||||
# webhook_type :integer default("account_type")
|
||||
|
||||
@ -57,4 +57,12 @@ class InboxPolicy < ApplicationPolicy
|
||||
def avatar?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def setup_channel_provider?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def disconnect_channel_provider?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
@ -46,7 +46,7 @@ class ActionService
|
||||
return if labels.empty?
|
||||
|
||||
labels = @conversation.label_list - labels
|
||||
@conversation.update(label_list: labels)
|
||||
@conversation.update!(label_list: labels)
|
||||
end
|
||||
|
||||
def assign_team(team_ids = [])
|
||||
|
||||
@ -10,7 +10,7 @@ class AutoAssignment::AgentAssignmentService
|
||||
|
||||
def perform
|
||||
new_assignee = find_assignee
|
||||
conversation.update(assignee: new_assignee) if new_assignee
|
||||
conversation.update!(assignee: new_assignee) if new_assignee
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@ -49,7 +49,7 @@ class AutoAssignment::InboxRoundRobinService
|
||||
end
|
||||
|
||||
def validate_queue?
|
||||
return true if inbox.inbox_members.map(&:user_id).sort == queue.map(&:to_i).sort
|
||||
true if inbox.inbox_members.map(&:user_id).sort == queue.map(&:to_i).sort
|
||||
end
|
||||
|
||||
def queue
|
||||
|
||||
@ -23,6 +23,8 @@ class Conversations::MessageWindowService
|
||||
when 'Channel::Instagram'
|
||||
instagram_messaging_window
|
||||
when 'Channel::Whatsapp'
|
||||
return if @conversation.inbox.channel.provider == 'baileys'
|
||||
|
||||
MESSAGING_WINDOW_24_HOURS
|
||||
when 'Channel::TwilioSms'
|
||||
twilio_messaging_window
|
||||
|
||||
@ -53,7 +53,7 @@ class DataImport::ContactManager
|
||||
contact.email = params[:email] if params[:email].present?
|
||||
contact.phone_number = format_phone_number(params[:phone_number]) if params[:phone_number].present?
|
||||
update_contact_attributes(params, contact)
|
||||
contact.save
|
||||
contact.save # rubocop:disable Rails/SaveBang
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@ -30,7 +30,7 @@ class Line::IncomingMessageService
|
||||
end
|
||||
|
||||
def message_created?(event)
|
||||
return unless event_type_message?(event)
|
||||
return false unless event_type_message?(event)
|
||||
|
||||
@message = @conversation.messages.build(
|
||||
content: message_content(event),
|
||||
|
||||
@ -66,10 +66,10 @@ class MessageTemplates::HookExecutionService
|
||||
end
|
||||
|
||||
def should_send_csat_survey?
|
||||
return unless csat_enabled_conversation?
|
||||
return false unless csat_enabled_conversation?
|
||||
|
||||
# only send CSAT once in a conversation
|
||||
return if conversation.messages.where(content_type: :input_csat).present?
|
||||
return false if conversation.messages.where(content_type: :input_csat).present?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
@ -29,7 +29,7 @@ class Twilio::WebhookSetupService
|
||||
else
|
||||
twilio_client
|
||||
.incoming_phone_numbers(phonenumber_sid)
|
||||
.update(sms_method: 'POST', sms_url: twilio_callback_index_url)
|
||||
.update(sms_method: 'POST', sms_url: twilio_callback_index_url) # rubocop:disable Rails/SaveBang
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
326
app/services/whatsapp/incoming_message_baileys_service.rb
Normal file
326
app/services/whatsapp/incoming_message_baileys_service.rb
Normal file
@ -0,0 +1,326 @@
|
||||
class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseService # rubocop:disable Metrics/ClassLength
|
||||
class InvalidWebhookVerifyToken < StandardError; end
|
||||
class MessageNotFoundError < StandardError; end
|
||||
class AttachmentNotFoundError < StandardError; end
|
||||
|
||||
def perform
|
||||
raise InvalidWebhookVerifyToken if processed_params[:webhookVerifyToken] != inbox.channel.provider_config['webhook_verify_token']
|
||||
return if processed_params[:event].blank? || processed_params[:data].blank?
|
||||
|
||||
event_prefix = processed_params[:event].gsub(/[\.-]/, '_')
|
||||
method_name = "process_#{event_prefix}"
|
||||
if respond_to?(method_name, true)
|
||||
# TODO: Implement the methods for all expected events
|
||||
send(method_name)
|
||||
else
|
||||
Rails.logger.warn "Baileys unsupported event: #{processed_params[:event]}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_connection_update
|
||||
data = processed_params[:data]
|
||||
|
||||
# NOTE: `connection` values
|
||||
# - `close`: Never opened, or closed and no longer able to send/receive messages
|
||||
# - `connecting`: In the process of connecting, expecting QR code to be read
|
||||
# - `reconnecting`: Connection has been established, but not open (i.e. device is being linked for the first time, or Baileys server restart)
|
||||
# - `open`: Open and ready to send/receive messages
|
||||
inbox.channel.update_provider_connection!({
|
||||
connection: data[:connection] || inbox.channel.provider_connection['connection'],
|
||||
qr_data_url: data[:qrDataUrl] || nil,
|
||||
error: data[:error] ? I18n.t("errors.inboxes.channel.provider_connection.#{data[:error]}") : nil
|
||||
}.compact)
|
||||
|
||||
Rails.logger.error "Baileys connection error: #{data[:error]}" if data[:error].present?
|
||||
end
|
||||
|
||||
def process_messages_upsert
|
||||
messages = processed_params[:data][:messages]
|
||||
messages.each do |message|
|
||||
@message = nil
|
||||
@contact_inbox = nil
|
||||
@contact = nil
|
||||
@raw_message = message
|
||||
handle_message
|
||||
end
|
||||
end
|
||||
|
||||
def handle_message
|
||||
return if jid_type != 'user'
|
||||
return if find_message_by_source_id(message_id) || message_under_process?
|
||||
|
||||
cache_message_source_id_in_redis
|
||||
set_contact
|
||||
|
||||
unless @contact
|
||||
Rails.logger.warn "Contact not found for message: #{message_id}"
|
||||
return
|
||||
end
|
||||
|
||||
set_conversation
|
||||
handle_create_message
|
||||
clear_message_source_id_from_redis
|
||||
end
|
||||
|
||||
def set_contact
|
||||
push_name = contact_name
|
||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: phone_number_from_jid,
|
||||
inbox: inbox,
|
||||
contact_attributes: { name: push_name, phone_number: "+#{phone_number_from_jid}" }
|
||||
).perform
|
||||
|
||||
@contact_inbox = contact_inbox
|
||||
@contact = contact_inbox.contact
|
||||
|
||||
@contact.update!(name: push_name) if @contact.name == phone_number_from_jid
|
||||
end
|
||||
|
||||
def phone_number_from_jid
|
||||
# NOTE: jid shape is `<user>_<agent>:<device>@<server>`
|
||||
# https://github.com/WhiskeySockets/Baileys/blob/v6.7.16/src/WABinary/jid-utils.ts#L19
|
||||
@phone_number_from_jid ||= @raw_message[:key][:remoteJid].split('@').first.split(':').first.split('_').first
|
||||
end
|
||||
|
||||
def contact_name
|
||||
# NOTE: `verifiedBizName` is only available for business accounts and has a higher priority than `pushName`.
|
||||
name = @raw_message[:verifiedBizName].presence || @raw_message[:pushName]
|
||||
return name if self_message? || incoming?
|
||||
|
||||
phone_number_from_jid
|
||||
end
|
||||
|
||||
def self_message?
|
||||
phone_number_from_jid == inbox.channel.phone_number.delete('+')
|
||||
end
|
||||
|
||||
def handle_create_message
|
||||
case message_type
|
||||
when 'text'
|
||||
create_message
|
||||
when 'image', 'file', 'video', 'audio', 'sticker'
|
||||
create_message
|
||||
attach_media
|
||||
else
|
||||
create_unsupported_message
|
||||
Rails.logger.warn "Baileys unsupported message type: #{message_type}"
|
||||
end
|
||||
end
|
||||
|
||||
def jid_type # rubocop:disable Metrics/CyclomaticComplexity
|
||||
jid = @raw_message[:key][:remoteJid]
|
||||
server = jid.split('@').last
|
||||
|
||||
# NOTE: Based on Baileys internal functions
|
||||
# https://github.com/WhiskeySockets/Baileys/blob/v6.7.16/src/WABinary/jid-utils.ts#L48-L58
|
||||
case server
|
||||
when 's.whatsapp.net', 'c.us'
|
||||
'user'
|
||||
when 'g.us'
|
||||
'group'
|
||||
when 'lid'
|
||||
'lid'
|
||||
when 'broadcast'
|
||||
jid.start_with?('status@') ? 'status' : 'broadcast'
|
||||
when 'newsletter'
|
||||
'newsletter'
|
||||
when 'call'
|
||||
'call'
|
||||
else
|
||||
'unknown'
|
||||
end
|
||||
end
|
||||
|
||||
def message_type # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
||||
msg = @raw_message[:message]
|
||||
|
||||
return 'text' if msg.key?(:conversation) || msg.dig(:extendedTextMessage, :text).present?
|
||||
return 'contacts' if msg.key?(:contactMessage)
|
||||
return 'image' if msg.key?(:imageMessage)
|
||||
return 'audio' if msg.key?(:audioMessage)
|
||||
return 'video' if msg.key?(:videoMessage)
|
||||
return 'video_note' if msg.key?(:ptvMessage)
|
||||
return 'location' if msg.key?(:locationMessage)
|
||||
return 'live_location' if msg.key?(:liveLocationMessage)
|
||||
return 'file' if msg.key?(:documentMessage)
|
||||
return 'poll' if msg.key?(:pollCreationMessageV3)
|
||||
return 'event' if msg.key?(:eventMessage)
|
||||
return 'sticker' if msg.key?(:stickerMessage)
|
||||
|
||||
'unsupported'
|
||||
end
|
||||
|
||||
def create_message
|
||||
sender = incoming? ? @contact : @inbox.account.account_users.first.user
|
||||
sender_type = incoming? ? 'Contact' : 'User'
|
||||
message_type = incoming? ? :incoming : :outgoing
|
||||
|
||||
@message = @conversation.messages.create!(
|
||||
content: message_content,
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
source_id: message_id,
|
||||
sender: sender,
|
||||
sender_type: sender_type,
|
||||
message_type: message_type,
|
||||
in_reply_to_external_id: nil
|
||||
)
|
||||
end
|
||||
|
||||
def incoming?
|
||||
!@raw_message[:key][:fromMe]
|
||||
end
|
||||
|
||||
def create_unsupported_message
|
||||
create_message
|
||||
@message.update!(
|
||||
content: I18n.t('errors.messages.unsupported'),
|
||||
message_type: 'template',
|
||||
status: 'failed'
|
||||
)
|
||||
end
|
||||
|
||||
def attach_media
|
||||
media = processed_params.dig(:extra, :media)
|
||||
return if media.blank?
|
||||
|
||||
attachment_payload = media[message_id]
|
||||
if attachment_payload.blank?
|
||||
Rails.logger.error "Attachment not found for message: #{message_id}"
|
||||
raise AttachmentNotFoundError
|
||||
end
|
||||
|
||||
begin
|
||||
decoded_data = Base64.decode64(attachment_payload)
|
||||
io = StringIO.new(decoded_data)
|
||||
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_content_type.to_s,
|
||||
file: { io: io, filename: filename }
|
||||
)
|
||||
|
||||
@message.save!
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to attach media for message #{message_id} (#{e.message}) payload: #{attachment_payload}"
|
||||
end
|
||||
end
|
||||
|
||||
def file_content_type
|
||||
return :image if message_type.in?(%w[image sticker])
|
||||
return :video if message_type.in?(%w[video video_note])
|
||||
return :audio if message_type == 'audio'
|
||||
|
||||
:file
|
||||
end
|
||||
|
||||
def filename
|
||||
filename = @raw_message.dig(:message, :documentMessage, :fileName)
|
||||
return filename if filename.present?
|
||||
|
||||
"#{file_content_type}_#{@message[:id]}_#{Time.current.strftime('%Y%m%d')}"
|
||||
end
|
||||
|
||||
def message_content
|
||||
case message_type
|
||||
when 'text'
|
||||
@raw_message.dig(:message, :conversation) || @raw_message.dig(:message, :extendedTextMessage, :text)
|
||||
when 'image'
|
||||
@raw_message.dig(:message, :imageMessage, :caption)
|
||||
when 'video'
|
||||
@raw_message.dig(:message, :videoMessage, :caption)
|
||||
end
|
||||
end
|
||||
|
||||
def message_id
|
||||
@raw_message[:key][:id]
|
||||
end
|
||||
|
||||
def message_under_process?
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||
Redis::Alfred.get(key)
|
||||
end
|
||||
|
||||
def cache_message_source_id_in_redis
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||
::Redis::Alfred.setex(key, true)
|
||||
end
|
||||
|
||||
def clear_message_source_id_from_redis
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||
::Redis::Alfred.delete(key)
|
||||
end
|
||||
|
||||
def process_messages_update
|
||||
updates = processed_params[:data]
|
||||
updates.each do |update|
|
||||
@message = nil
|
||||
@raw_message = update
|
||||
handle_update
|
||||
end
|
||||
end
|
||||
|
||||
def handle_update
|
||||
raise MessageNotFoundError unless valid_update_message?
|
||||
|
||||
update_status if @raw_message.dig(:update, :status).present?
|
||||
update_message_content if @raw_message.dig(:update, :message).present?
|
||||
end
|
||||
|
||||
def valid_update_message?
|
||||
@message = find_message_by_source_id(message_id)
|
||||
@message.present?
|
||||
end
|
||||
|
||||
def update_status
|
||||
status = status_mapper
|
||||
@message.update!(status: status) if status.present? && status_transition_allowed?(status)
|
||||
end
|
||||
|
||||
def status_mapper
|
||||
# NOTE: Baileys status codes vs. Chatwoot support:
|
||||
# - (0) ERROR → (3) failed
|
||||
# - (1) PENDING → (0) sent
|
||||
# - (2) SERVER_ACK → (0) sent
|
||||
# - (3) DELIVERY_ACK → (1) delivered
|
||||
# - (4) READ → (2) read
|
||||
# - (5) PLAYED → (unsupported: PLAYED)
|
||||
# For details: https://github.com/WhiskeySockets/Baileys/blob/v6.7.16/WAProto/index.d.ts#L36694
|
||||
status = @raw_message.dig(:update, :status)
|
||||
case status
|
||||
when 0
|
||||
'failed'
|
||||
when 1, 2
|
||||
'sent'
|
||||
when 3
|
||||
'delivered'
|
||||
when 4
|
||||
'read'
|
||||
when 5
|
||||
Rails.logger.warn 'Baileys unsupported message update status: PLAYED(5)'
|
||||
else
|
||||
Rails.logger.warn "Baileys unsupported message update status: #{status}"
|
||||
end
|
||||
end
|
||||
|
||||
def status_transition_allowed?(new_status)
|
||||
return false if @message.status == 'read'
|
||||
return false if @message.status == 'delivered' && new_status == 'sent'
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def update_message_content
|
||||
message = @raw_message.dig(:update, :message, :editedMessage, :message)
|
||||
if message.blank?
|
||||
Rails.logger.warn 'No valid message content found in the update event'
|
||||
return
|
||||
end
|
||||
|
||||
content = message[:conversation] || message.dig(:extendedTextMessage, :text)
|
||||
|
||||
@message.update!(content: content) if content.present?
|
||||
end
|
||||
end
|
||||
@ -28,7 +28,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
|
||||
# ensuring that channels with wrong provider config wouldn't keep trying to sync templates
|
||||
whatsapp_channel.mark_message_templates_updated
|
||||
response = HTTParty.get("#{api_base_path}/configs/templates", headers: api_headers)
|
||||
whatsapp_channel.update(message_templates: response['waba_templates'], message_templates_last_updated: Time.now.utc) if response.success?
|
||||
whatsapp_channel.update!(message_templates: response['waba_templates'], message_templates_last_updated: Time.now.utc) if response.success?
|
||||
end
|
||||
|
||||
def validate_provider_config?
|
||||
|
||||
151
app/services/whatsapp/providers/whatsapp_baileys_service.rb
Normal file
151
app/services/whatsapp/providers/whatsapp_baileys_service.rb
Normal file
@ -0,0 +1,151 @@
|
||||
class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseService
|
||||
class MessageContentTypeNotSupported < StandardError; end
|
||||
class MessageNotSentError < StandardError; end
|
||||
|
||||
DEFAULT_CLIENT_NAME = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_CLIENT_NAME', nil)
|
||||
DEFAULT_URL = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_URL', nil)
|
||||
DEFAULT_API_KEY = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_API_KEY', nil)
|
||||
|
||||
def setup_channel_provider
|
||||
response = HTTParty.post(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
clientName: DEFAULT_CLIENT_NAME,
|
||||
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
|
||||
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token']
|
||||
}.to_json
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
def disconnect_channel_provider
|
||||
response = HTTParty.delete(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}",
|
||||
headers: api_headers
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
def send_message(phone_number, message)
|
||||
@message = message
|
||||
@phone_number = phone_number
|
||||
if message.attachments.present?
|
||||
send_attachment_message
|
||||
elsif message.content.present?
|
||||
send_text_message
|
||||
else
|
||||
message.update!(content: I18n.t('errors.messages.send.unsupported'), status: 'failed')
|
||||
end
|
||||
end
|
||||
|
||||
def send_template(phone_number, template_info); end
|
||||
|
||||
def sync_templates; end
|
||||
|
||||
def media_url(media_id); end
|
||||
|
||||
def api_headers
|
||||
{ 'x-api-key' => api_key, 'Content-Type' => 'application/json' }
|
||||
end
|
||||
|
||||
def validate_provider_config?
|
||||
response = HTTParty.get(
|
||||
"#{provider_url}/status/auth",
|
||||
headers: api_headers
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def provider_url
|
||||
whatsapp_channel.provider_config['provider_url'].presence || DEFAULT_URL
|
||||
end
|
||||
|
||||
def api_key
|
||||
whatsapp_channel.provider_config['api_key'].presence || DEFAULT_API_KEY
|
||||
end
|
||||
|
||||
def send_attachment_message
|
||||
@attachment = @message.attachments.first
|
||||
|
||||
response = HTTParty.post(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-message",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
recipient: @phone_number,
|
||||
messageContent: message_content
|
||||
}.to_json
|
||||
)
|
||||
|
||||
return response.parsed_response.dig('data', 'key', 'id') if process_response(response)
|
||||
|
||||
raise MessageNotSentError
|
||||
end
|
||||
|
||||
def message_content
|
||||
buffer = Base64.strict_encode64(@attachment.file.download)
|
||||
|
||||
content = {
|
||||
fileName: @attachment.file.filename,
|
||||
caption: @message.content
|
||||
}
|
||||
case @attachment.file_type
|
||||
when 'image'
|
||||
content[:image] = buffer
|
||||
when 'audio'
|
||||
content[:audio] = buffer
|
||||
when 'file'
|
||||
content[:document] = buffer
|
||||
when 'sticker'
|
||||
content[:sticker] = buffer
|
||||
when 'video'
|
||||
content[:video] = buffer
|
||||
end
|
||||
|
||||
content
|
||||
end
|
||||
|
||||
def send_text_message
|
||||
response = HTTParty.post(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-message",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
recipient: @phone_number,
|
||||
messageContent: { text: @message.content }
|
||||
}.to_json
|
||||
)
|
||||
|
||||
return response.parsed_response.dig('data', 'key', 'id') if process_response(response)
|
||||
|
||||
raise MessageNotSentError
|
||||
end
|
||||
|
||||
def process_response(response)
|
||||
Rails.logger.error response.body unless response.success?
|
||||
response.success?
|
||||
end
|
||||
|
||||
private_class_method def self.with_error_handling(*method_names)
|
||||
method_names.each do |method_name|
|
||||
original_method = instance_method(method_name)
|
||||
|
||||
define_method(method_name) do |*args, &block|
|
||||
original_method.bind_call(self, *args, &block)
|
||||
rescue StandardError => e
|
||||
handle_channel_error
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_channel_error
|
||||
whatsapp_channel.update_provider_connection!(connection: 'close')
|
||||
end
|
||||
|
||||
with_error_handling :setup_channel_provider, :disconnect_channel_provider, :send_message
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user