diff --git a/.env.example b/.env.example index b7ba0920d..443baeb57 100644 --- a/.env.example +++ b/.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= diff --git a/.github/workflows/frontend-fe.yml b/.github/workflows/frontend-fe.yml index 45ff25203..a456d7092 100644 --- a/.github/workflows/frontend-fe.yml +++ b/.github/workflows/frontend-fe.yml @@ -3,10 +3,8 @@ name: Frontend Lint & Test on: push: branches: - - develop - pull_request: - branches: - - develop + - fazer-ai/main + workflow_dispatch: jobs: test: diff --git a/.github/workflows/publish_github_docker.yml b/.github/workflows/publish_github_docker.yml new file mode 100644 index 000000000..b971dfff1 --- /dev/null +++ b/.github/workflows/publish_github_docker.yml @@ -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 diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index 385feddfc..10732d018 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -1,10 +1,9 @@ name: Run Chatwoot CE spec + on: push: branches: - - develop - - master - pull_request: + - fazer-ai/main workflow_dispatch: jobs: diff --git a/.husky/pre-commit b/.husky/pre-commit index b3aceacd6..9c350db88 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -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 diff --git a/.nvmrc b/.nvmrc index 6f7af3750..b88575e38 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.5.1 \ No newline at end of file +23.7.0 \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index 1cdfbc713..2dcf6eec4 100644 --- a/.rubocop.yml +++ b/.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 diff --git a/.vscode/settings.json b/.vscode/settings.json index b3cfaee50..b663561bb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "cSpell.words": [ "chatwoot", "dompurify" - ] + ], + "css.customData": [".vscode/tailwind.json"] } diff --git a/.vscode/tailwind.json b/.vscode/tailwind.json new file mode 100644 index 000000000..e47372fec --- /dev/null +++ b/.vscode/tailwind.json @@ -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" + } + ] + } + ] +} diff --git a/Gemfile b/Gemfile index 1e8605379..769022c1d 100644 --- a/Gemfile +++ b/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 ### ############################################################## diff --git a/Gemfile.lock b/Gemfile.lock index 80ef15b49..533c88f1a 100644 --- a/Gemfile.lock +++ b/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 diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index a88d3535b..8e7e1bf2d 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -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? diff --git a/app/builders/messages/instagram/base_message_builder.rb b/app/builders/messages/instagram/base_message_builder.rb index 767115bc6..7fab7bc4d 100644 --- a/app/builders/messages/instagram/base_message_builder.rb +++ b/app/builders/messages/instagram/base_message_builder.rb @@ -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) diff --git a/app/builders/messages/instagram/message_builder.rb b/app/builders/messages/instagram/message_builder.rb index 4e7150894..e306c7b70 100644 --- a/app/builders/messages/instagram/message_builder.rb +++ b/app/builders/messages/instagram/message_builder.rb @@ -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}") diff --git a/app/builders/messages/instagram/messenger/message_builder.rb b/app/builders/messages/instagram/messenger/message_builder.rb index 1263dee90..41ba59f2c 100644 --- a/app/builders/messages/instagram/messenger/message_builder.rb +++ b/app/builders/messages/instagram/messenger/message_builder.rb @@ -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 diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index 4e7f2849d..9449ef084 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb index 3d894808d..05d3cb1ab 100644 --- a/app/controllers/api/v1/accounts/automation_rules_controller.rb +++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index 6e119ca3d..960fc5725 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 011faaf28..2d390bba9 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -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') } diff --git a/app/controllers/api/v1/accounts/integrations/slack_controller.rb b/app/controllers/api/v1/accounts/integrations/slack_controller.rb index eaa18c2de..eadd46e4a 100644 --- a/app/controllers/api/v1/accounts/integrations/slack_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/slack_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/notifications_controller.rb b/app/controllers/api/v1/accounts/notifications_controller.rb index 52035ce64..874fb67a4 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 6cfed161f..73bba60eb 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/webhooks_controller.rb b/app/controllers/api/v1/accounts/webhooks_controller.rb index 7ea257ed2..a32f1aa3c 100644 --- a/app/controllers/api/v1/accounts/webhooks_controller.rb +++ b/app/controllers/api/v1/accounts/webhooks_controller.rb @@ -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 diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index ae1a1fe30..7eabf686f 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -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 diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index 5138fe675..32d89f450 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -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 diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index fe5facc1a..64b32db3c 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -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 diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index eb6f776f1..769bb1b18 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -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 diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb index e11cf9d4a..4b3c7cfee 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -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 diff --git a/app/controllers/platform/api/v1/agent_bots_controller.rb b/app/controllers/platform/api/v1/agent_bots_controller.rb index dd70a1ba5..a3ed639fd 100644 --- a/app/controllers/platform/api/v1/agent_bots_controller.rb +++ b/app/controllers/platform/api/v1/agent_bots_controller.rb @@ -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 diff --git a/app/controllers/public/api/v1/inboxes/contacts_controller.rb b/app/controllers/public/api/v1/inboxes/contacts_controller.rb index 835c2596b..246c95d25 100644 --- a/app/controllers/public/api/v1/inboxes/contacts_controller.rb +++ b/app/controllers/public/api/v1/inboxes/contacts_controller.rb @@ -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? diff --git a/app/controllers/super_admin/account_users_controller.rb b/app/controllers/super_admin/account_users_controller.rb index b210dea19..057128c98 100644 --- a/app/controllers/super_admin/account_users_controller.rb +++ b/app/controllers/super_admin/account_users_controller.rb @@ -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 diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 550b6c893..4ff461672 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -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 diff --git a/app/controllers/webhooks/whatsapp_controller.rb b/app/controllers/webhooks/whatsapp_controller.rb index c4c376e5c..709486048 100644 --- a/app/controllers/webhooks/whatsapp_controller.rb +++ b/app/controllers/webhooks/whatsapp_controller.rb @@ -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]) diff --git a/app/helpers/cache_keys_helper.rb b/app/helpers/cache_keys_helper.rb index aab33e44c..366c87383 100644 --- a/app/helpers/cache_keys_helper.rb +++ b/app/helpers/cache_keys_helper.rb @@ -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 diff --git a/app/helpers/frontend_urls_helper.rb b/app/helpers/frontend_urls_helper.rb index 1867c77ea..67b117f06 100644 --- a/app/helpers/frontend_urls_helper.rb +++ b/app/helpers/frontend_urls_helper.rb @@ -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 diff --git a/app/helpers/reporting_event_helper.rb b/app/helpers/reporting_event_helper.rb index e08b5691c..94c7f8cde 100644 --- a/app/helpers/reporting_event_helper.rb +++ b/app/helpers/reporting_event_helper.rb @@ -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 diff --git a/app/helpers/timezone_helper.rb b/app/helpers/timezone_helper.rb index b016cc9d9..7b88ceae6 100644 --- a/app/helpers/timezone_helper.rb +++ b/app/helpers/timezone_helper.rb @@ -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 diff --git a/app/javascript/dashboard/api/inboxes.js b/app/javascript/dashboard/api/inboxes.js index 8c09791c8..7af82a720 100644 --- a/app/javascript/dashboard/api/inboxes.js +++ b/app/javascript/dashboard/api/inboxes.js @@ -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(); diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 90cb67c4f..1623d4c04 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -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')" />