diff --git a/.env.example b/.env.example index cc7e1c2dd..d46acac9f 100644 --- a/.env.example +++ b/.env.example @@ -161,13 +161,15 @@ USE_INBOX_AVATAR_FOR_BOT=true ## NewRelic # https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/ # NEW_RELIC_LICENSE_KEY= +# Set this to true to allow newrelic apm to send logs. +# This is turned off by default. +# NEW_RELIC_APPLICATION_LOGGING_ENABLED= ## Datadog ## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables # DD_TRACE_AGENT_URL= - ## IP look up configuration ## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md ## works only on accounts with ip look up feature enabled diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index 3a6ff5f7b..395e063e7 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -42,7 +42,8 @@ jobs: steps: - uses: actions/checkout@v3 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} - uses: ruby/setup-ruby@v1 with: diff --git a/Gemfile b/Gemfile index e94303f45..55d02d87c 100644 --- a/Gemfile +++ b/Gemfile @@ -97,14 +97,14 @@ gem 'brakeman' gem 'ddtrace' gem 'newrelic_rpm' gem 'scout_apm' -gem 'sentry-rails' -gem 'sentry-ruby' -gem 'sentry-sidekiq' +gem 'sentry-rails', '~> 5.3' +gem 'sentry-ruby', '~> 5.3' +gem 'sentry-sidekiq', '~> 5.3' ##-- background job processing --## gem 'sidekiq', '~> 6.4.0' # We want cron jobs -gem 'sidekiq-cron' +gem 'sidekiq-cron', '~> 1.3' ##-- Push notification service --## gem 'fcm' diff --git a/Gemfile.lock b/Gemfile.lock index 2789f09da..253c35da6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,63 +9,63 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.7) - actionpack (= 6.1.4.7) - activesupport (= 6.1.4.7) + actioncable (6.1.5.1) + actionpack (= 6.1.5.1) + activesupport (= 6.1.5.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.7) - actionpack (= 6.1.4.7) - activejob (= 6.1.4.7) - activerecord (= 6.1.4.7) - activestorage (= 6.1.4.7) - activesupport (= 6.1.4.7) + actionmailbox (6.1.5.1) + actionpack (= 6.1.5.1) + activejob (= 6.1.5.1) + activerecord (= 6.1.5.1) + activestorage (= 6.1.5.1) + activesupport (= 6.1.5.1) mail (>= 2.7.1) - actionmailer (6.1.4.7) - actionpack (= 6.1.4.7) - actionview (= 6.1.4.7) - activejob (= 6.1.4.7) - activesupport (= 6.1.4.7) + actionmailer (6.1.5.1) + actionpack (= 6.1.5.1) + actionview (= 6.1.5.1) + activejob (= 6.1.5.1) + activesupport (= 6.1.5.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.7) - actionview (= 6.1.4.7) - activesupport (= 6.1.4.7) + actionpack (6.1.5.1) + actionview (= 6.1.5.1) + activesupport (= 6.1.5.1) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.7) - actionpack (= 6.1.4.7) - activerecord (= 6.1.4.7) - activestorage (= 6.1.4.7) - activesupport (= 6.1.4.7) + actiontext (6.1.5.1) + actionpack (= 6.1.5.1) + activerecord (= 6.1.5.1) + activestorage (= 6.1.5.1) + activesupport (= 6.1.5.1) nokogiri (>= 1.8.5) - actionview (6.1.4.7) - activesupport (= 6.1.4.7) + actionview (6.1.5.1) + activesupport (= 6.1.5.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (6.1.4.7) - activesupport (= 6.1.4.7) + activejob (6.1.5.1) + activesupport (= 6.1.5.1) globalid (>= 0.3.6) - activemodel (6.1.4.7) - activesupport (= 6.1.4.7) - activerecord (6.1.4.7) - activemodel (= 6.1.4.7) - activesupport (= 6.1.4.7) + activemodel (6.1.5.1) + activesupport (= 6.1.5.1) + activerecord (6.1.5.1) + activemodel (= 6.1.5.1) + activesupport (= 6.1.5.1) activerecord-import (1.3.0) activerecord (>= 4.2) - activestorage (6.1.4.7) - actionpack (= 6.1.4.7) - activejob (= 6.1.4.7) - activerecord (= 6.1.4.7) - activesupport (= 6.1.4.7) - marcel (~> 1.0.0) + activestorage (6.1.5.1) + actionpack (= 6.1.5.1) + activejob (= 6.1.5.1) + activerecord (= 6.1.5.1) + activesupport (= 6.1.5.1) + marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.7) + activesupport (6.1.5.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -136,7 +136,7 @@ GEM climate_control (1.0.1) coderay (1.1.3) commonmarker (0.23.4) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) connection_pool (2.2.5) crack (0.4.5) rexml @@ -183,7 +183,7 @@ GEM email_reply_trimmer (0.1.13) erubi (1.10.0) erubis (2.7.0) - et-orbi (1.2.6) + et-orbi (1.2.7) tzinfo execjs (2.8.1) facebook-messenger (2.0.1) @@ -210,8 +210,8 @@ GEM ruby_parser (~> 3.0) sexp_processor (~> 4.0) foreman (0.87.2) - fugit (1.5.2) - et-orbi (~> 1.1, >= 1.1.8) + fugit (1.5.3) + et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) gapic-common (0.3.4) google-protobuf (~> 3.12, >= 3.12.2) @@ -349,7 +349,7 @@ GEM listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.14.0) + loofah (2.17.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -376,16 +376,16 @@ GEM net-http-persistent (4.0.1) connection_pool (~> 2.2) netrc (0.11.0) - newrelic_rpm (8.4.0) + newrelic_rpm (8.7.0) nio4r (2.5.8) - nokogiri (1.13.4) + nokogiri (1.13.5) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.4-arm64-darwin) + nokogiri (1.13.5-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.4-x86_64-darwin) + nokogiri (1.13.5-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.4-x86_64-linux) + nokogiri (1.13.5-x86_64-linux) racc (~> 1.4) oauth (0.5.8) orm_adapter (0.5.0) @@ -419,31 +419,31 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) rack-timeout (0.6.0) - rails (6.1.4.7) - actioncable (= 6.1.4.7) - actionmailbox (= 6.1.4.7) - actionmailer (= 6.1.4.7) - actionpack (= 6.1.4.7) - actiontext (= 6.1.4.7) - actionview (= 6.1.4.7) - activejob (= 6.1.4.7) - activemodel (= 6.1.4.7) - activerecord (= 6.1.4.7) - activestorage (= 6.1.4.7) - activesupport (= 6.1.4.7) + rails (6.1.5.1) + actioncable (= 6.1.5.1) + actionmailbox (= 6.1.5.1) + actionmailer (= 6.1.5.1) + actionpack (= 6.1.5.1) + actiontext (= 6.1.5.1) + actionview (= 6.1.5.1) + activejob (= 6.1.5.1) + activemodel (= 6.1.5.1) + activerecord (= 6.1.5.1) + activestorage (= 6.1.5.1) + activesupport (= 6.1.5.1) bundler (>= 1.15.0) - railties (= 6.1.4.7) + railties (= 6.1.5.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - railties (6.1.4.7) - actionpack (= 6.1.4.7) - activesupport (= 6.1.4.7) + railties (6.1.5.1) + actionpack (= 6.1.5.1) + activesupport (= 6.1.5.1) method_source - rake (>= 0.13) + rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) rake (13.0.6) @@ -533,16 +533,16 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.1.0) + sentry-rails (5.3.0) railties (>= 5.0) - sentry-ruby-core (~> 5.1.0) - sentry-ruby (5.1.0) + sentry-ruby-core (~> 5.3.0) + sentry-ruby (5.3.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-ruby-core (= 5.1.0) - sentry-ruby-core (5.1.0) + sentry-ruby-core (= 5.3.0) + sentry-ruby-core (5.3.0) concurrent-ruby - sentry-sidekiq (5.1.0) - sentry-ruby-core (~> 5.1.0) + sentry-sidekiq (5.3.0) + sentry-ruby-core (~> 5.3.0) sidekiq (>= 3.0) sexp_processor (4.16.0) shoulda-matchers (5.1.0) @@ -551,8 +551,8 @@ GEM connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) - sidekiq-cron (1.2.0) - fugit (~> 1.1) + sidekiq-cron (1.4.0) + fugit (~> 1) sidekiq (>= 4.2.1) signet (0.16.0) addressable (~> 2.8) @@ -726,12 +726,12 @@ DEPENDENCIES rubocop-rspec scout_apm seed_dump - sentry-rails - sentry-ruby - sentry-sidekiq + sentry-rails (~> 5.3) + sentry-ruby (~> 5.3) + sentry-sidekiq (~> 5.3) shoulda-matchers sidekiq (~> 6.4.0) - sidekiq-cron + sidekiq-cron (~> 1.3) simplecov (= 0.17.1) slack-ruby-client spring @@ -755,4 +755,4 @@ RUBY VERSION ruby 3.0.2p107 BUNDLED WITH - 2.3.8 + 2.3.10 diff --git a/app/builders/campaigns/campaign_conversation_builder.rb b/app/builders/campaigns/campaign_conversation_builder.rb index 48a3cebd4..b04c6d077 100644 --- a/app/builders/campaigns/campaign_conversation_builder.rb +++ b/app/builders/campaigns/campaign_conversation_builder.rb @@ -1,5 +1,5 @@ class Campaigns::CampaignConversationBuilder - pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes] + pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes, :custom_attributes] def perform @contact_inbox = ContactInbox.find(@contact_inbox_id) @@ -21,7 +21,8 @@ class Campaigns::CampaignConversationBuilder def message_params ActionController::Parameters.new({ - content: @campaign.message + content: @campaign.message, + campaign_id: @campaign.id }) end @@ -32,7 +33,8 @@ class Campaigns::CampaignConversationBuilder contact_id: @contact_inbox.contact_id, contact_inbox_id: @contact_inbox.id, campaign_id: @campaign.id, - additional_attributes: conversation_additional_attributes + additional_attributes: conversation_additional_attributes, + custom_attributes: custom_attributes || {} } end end diff --git a/app/builders/contact_builder.rb b/app/builders/contact_builder.rb index b63d72627..10ce8ee26 100644 --- a/app/builders/contact_builder.rb +++ b/app/builders/contact_builder.rb @@ -15,11 +15,10 @@ class ContactBuilder end def create_contact_inbox(contact) - ::ContactInbox.create!( + ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!( contact_id: contact.id, inbox_id: inbox.id, - source_id: source_id, - hmac_verified: hmac_verified || false + source_id: source_id ) end diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index 204b452d6..3b3248ed1 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -29,7 +29,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder rescue Koala::Facebook::AuthenticationError Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}" rescue StandardError => e - Sentry.capture_exception(e) + ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception true end @@ -43,7 +43,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder return if contact.present? @contact = Contact.create!(contact_params.except(:remote_avatar_url)) - @contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id) + @contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id) end def build_message @@ -128,10 +128,10 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder result = {} # OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user # We don't need to capture this error as we don't care about contact params in case of echo messages - Sentry.capture_exception(e) unless @outgoing_echo + ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo rescue StandardError => e result = {} - Sentry.capture_exception(e) + ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception end process_contact_params_result(result) end diff --git a/app/builders/messages/instagram/message_builder.rb b/app/builders/messages/instagram/message_builder.rb index 69475fb16..3b8ead18c 100644 --- a/app/builders/messages/instagram/message_builder.rb +++ b/app/builders/messages/instagram/message_builder.rb @@ -24,7 +24,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder @inbox.channel.authorization_error! raise rescue StandardError => e - Sentry.capture_exception(e) + ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception true end diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 9c26ccca1..5c8cadbcd 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -9,6 +9,7 @@ class Messages::MessageBuilder @user = user @message_type = params[:message_type] || 'outgoing' @attachments = params[:attachments] + @automation_rule = @params&.dig(:content_attributes, :automation_rule_id) return unless params.instance_of?(ActionController::Parameters) @in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to) @@ -64,6 +65,14 @@ class Messages::MessageBuilder @params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {} end + def automation_rule_id + @automation_rule.present? ? { content_attributes: { automation_rule_id: @automation_rule } } : {} + end + + def campaign_id + @params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {} + end + def message_sender return if @params[:sender_type] != 'AgentBot' @@ -82,6 +91,6 @@ class Messages::MessageBuilder items: @items, in_reply_to: @in_reply_to, echo_id: @params[:echo_id] - }.merge(external_created_at) + }.merge(external_created_at).merge(automation_rule_id).merge(campaign_id) end end diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index 2e58825f4..6d3ed0179 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -53,16 +53,7 @@ class Messages::Messenger::MessageBuilder def fetch_story_link(attachment) message = attachment.message - begin - k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? - result = k.get_object(message.source_id, fields: %w[story from]) || {} - rescue Koala::Facebook::AuthenticationError - @inbox.channel.authorization_error! - raise - rescue StandardError => e - result = {} - Sentry.capture_exception(e) - end + result = get_story_object_from_source_id(message.source_id) story_id = result['story']['mention']['id'] story_sender = result['from']['username'] message.content_attributes[:story_sender] = story_sender @@ -70,4 +61,15 @@ class Messages::Messenger::MessageBuilder message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender) message.save! end + + def get_story_object_from_source_id(source_id) + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? + k.get_object(source_id, fields: %w[story from]) || {} + rescue Koala::Facebook::AuthenticationError + @inbox.channel.authorization_error! + raise + rescue StandardError => e + ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception + {} + end end diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index 67b80b7a5..5cd8b4a63 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -4,6 +4,7 @@ class V2::ReportBuilder attr_reader :account, :params DEFAULT_GROUP_BY = 'day'.freeze + AGENT_RESULTS_PER_PAGE = 25 def initialize(account, params) @account = account @@ -45,7 +46,7 @@ class V2::ReportBuilder if params[:type].equal?(:account) conversations else - agent_metrics + agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse end end @@ -79,20 +80,23 @@ class V2::ReportBuilder end def agent_metrics - users = @account.users - users = users.where(id: params[:user_id]) if params[:user_id].present? - users.each_with_object([]) do |user, arr| - @user = user + account_users = @account.account_users.page(params[:page]).per(AGENT_RESULTS_PER_PAGE) + account_users.each_with_object([]) do |account_user, arr| + @user = account_user.user arr << { - user: { id: user.id, name: user.name, thumbnail: user.avatar_url }, + id: @user.id, + name: @user.name, + email: @user.email, + thumbnail: @user.avatar_url, + availability: account_user.availability_status, metric: conversations } end end def conversations - @open_conversations = scope.conversations.open - first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count + @open_conversations = scope.conversations.where(account_id: @account.id).open + first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count metric = { open: @open_conversations.count, unattended: @open_conversations.count - first_response_count diff --git a/app/channels/room_channel.rb b/app/channels/room_channel.rb index 05bff5d86..0680f1458 100644 --- a/app/channels/room_channel.rb +++ b/app/channels/room_channel.rb @@ -45,6 +45,8 @@ class RoomChannel < ApplicationCable::Channel end def current_account + return if current_user.blank? + @current_account ||= if @current_user.is_a? Contact @current_user.account else diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb index 41296db62..5e649b6e0 100644 --- a/app/controllers/api/v1/accounts/automation_rules_controller.rb +++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb @@ -17,6 +17,16 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont @automation_rule end + def attach_file + file_blob = ActiveStorage::Blob.create_and_upload!( + key: nil, + io: params[:attachment].tempfile, + filename: params[:attachment].original_filename, + content_type: params[:attachment].content_type + ) + render json: { blob_key: file_blob.key, blob_id: file_blob.id } + end + def show; end def update @@ -25,6 +35,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont @automation_rule.actions = params[:actions] if params[:actions] @automation_rule.save! process_attachments + rescue StandardError => e Rails.logger.error e render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity @@ -43,17 +54,19 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont @automation_rule = new_rule end - private - def process_attachments - return if params[:attachments].blank? + actions = @automation_rule.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' } + return if actions.blank? - params[:attachments].each do |uploaded_attachment| - @automation_rule.files.attach(uploaded_attachment) + actions.each do |action| + blob_id = action['action_params'] + blob = ActiveStorage::Blob.find_by(id: blob_id) + @automation_rule.files.attach(blob) end - @automation_rule end + private + def automation_rules_permit params.permit( :name, :description, :event_name, :account_id, :active, diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index 842930874..8a163f011 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -15,7 +15,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController set_instagram_id(page_access_token, facebook_channel) set_avatar(@facebook_inbox, page_id) rescue StandardError => e - Sentry.capture_exception(e) + ChatwootExceptionTracker.new(e).capture_exception end end @@ -60,7 +60,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController set_instagram_id(access_token, fb_page) fb_page&.reauthorized! rescue StandardError => e - Sentry.capture_exception(e) + ChatwootExceptionTracker.new(e).capture_exception end end diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index a53650e65..f5a3c6a6d 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -7,7 +7,6 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: build_inbox setup_webhooks if @twilio_channel.sms? rescue StandardError => e - Sentry.capture_exception(e) render_could_not_create_error(e.message) end end diff --git a/app/controllers/api/v1/accounts/conversations/base_controller.rb b/app/controllers/api/v1/accounts/conversations/base_controller.rb index b55b72013..da821a3e5 100644 --- a/app/controllers/api/v1/accounts/conversations/base_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/base_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::Base private def conversation - @conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id]) + @conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id]) authorize @conversation.inbox, :show? end end diff --git a/app/controllers/api/v1/accounts/kbase/base_controller.rb b/app/controllers/api/v1/accounts/kbase/base_controller.rb index f50140883..4f62cd858 100644 --- a/app/controllers/api/v1/accounts/kbase/base_controller.rb +++ b/app/controllers/api/v1/accounts/kbase/base_controller.rb @@ -4,6 +4,6 @@ class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseControll private def portal - @portal ||= Current.account.kbase_portals.find_by(id: params[:portal_id]) + @portal ||= Current.account.kbase_portals.find_by(slug: params[:portal_id]) end end diff --git a/app/controllers/api/v1/accounts/kbase/portals_controller.rb b/app/controllers/api/v1/accounts/kbase/portals_controller.rb index 804b2d421..5ec1b4a83 100644 --- a/app/controllers/api/v1/accounts/kbase/portals_controller.rb +++ b/app/controllers/api/v1/accounts/kbase/portals_controller.rb @@ -1,10 +1,12 @@ -class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::BaseController +class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseController before_action :fetch_portal, except: [:index, :create] def index @portals = Current.account.kbase_portals end + def show; end + def create @portal = Current.account.kbase_portals.create!(portal_params) end @@ -21,7 +23,11 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::Ba private def fetch_portal - @portal = current_account.kbase_portals.find(params[:id]) + @portal = Current.account.kbase_portals.find_by(slug: permitted_params[:id]) + end + + def permitted_params + params.permit(:id) end def portal_params diff --git a/app/controllers/api/v1/accounts/webhooks_controller.rb b/app/controllers/api/v1/accounts/webhooks_controller.rb index 0add18047..7ea257ed2 100644 --- a/app/controllers/api/v1/accounts/webhooks_controller.rb +++ b/app/controllers/api/v1/accounts/webhooks_controller.rb @@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController private def webhook_params - params.require(:webhook).permit(:inbox_id, :url) + params.require(:webhook).permit(:inbox_id, :url, subscriptions: []) end def fetch_webhook diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb index c96ae9a96..4f9f6d55b 100644 --- a/app/controllers/api/v1/webhooks_controller.rb +++ b/app/controllers/api/v1/webhooks_controller.rb @@ -10,7 +10,7 @@ class Api::V1::WebhooksController < ApplicationController twitter_consumer.consume head :ok rescue StandardError => e - Sentry.capture_exception(e) + ChatwootExceptionTracker.new(e).capture_exception head :ok end diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 67132869d..646d70acf 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -71,7 +71,7 @@ class Api::V1::Widget::BaseController < ApplicationController end def contact_email - permitted_params[:contact][:email].downcase if permitted_params[:contact].present? + permitted_params.dig(:contact, :email)&.downcase end def contact_name @@ -79,7 +79,7 @@ class Api::V1::Widget::BaseController < ApplicationController end def contact_phone_number - params[:contact][:phone_number] + permitted_params.dig(:contact, :phone_number) end def browser_params diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index ef2422bef..cfe107ac6 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -69,7 +69,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id], + params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number], + message: [:content, :referer_url, :timestamp, :echo_id], custom_attributes: {}) end end diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index 227ba2500..c2117583a 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -82,7 +82,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController def conversation_params { type: params[:type].to_sym, - user_id: params[:user_id] + user_id: params[:user_id], + page: params[:page].presence || 1 } end diff --git a/app/controllers/concerns/request_exception_handler.rb b/app/controllers/concerns/request_exception_handler.rb index 51061017e..6e9ed04cc 100644 --- a/app/controllers/concerns/request_exception_handler.rb +++ b/app/controllers/concerns/request_exception_handler.rb @@ -9,8 +9,7 @@ module RequestExceptionHandler def handle_with_exception yield - rescue ActiveRecord::RecordNotFound => e - Sentry.capture_exception(e) + rescue ActiveRecord::RecordNotFound render_not_found_error('Resource could not be found') rescue Pundit::NotAuthorizedError render_unauthorized('You are not authorized to do this action') diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 2bb8e9847..107aec56e 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -38,9 +38,12 @@ class DashboardController < ActionController::Base end def app_config - { APP_VERSION: Chatwoot.config[:version], + { + APP_VERSION: Chatwoot.config[:version], VAPID_PUBLIC_KEY: VapidService.public_key, ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), - FB_APP_ID: GlobalConfigService.load('FB_APP_ID', '') } + FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), + FACEBOOK_API_VERSION: 'v13.0' + } end end diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index c9f256c6f..4a7eafc9c 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -21,6 +21,10 @@ class Platform::Api::V1::UsersController < PlatformController def update @resource.assign_attributes(user_update_params) + + # We are using devise's reconfirmable flow for changing emails + # But in case of platform APIs we don't want user to go through this extra step + @resource.skip_reconfirmation! if user_update_params[:email].present? @resource.save! end diff --git a/app/controllers/public/api/v1/inboxes/contacts_controller.rb b/app/controllers/public/api/v1/inboxes/contacts_controller.rb index 6932386fd..eb794f2a0 100644 --- a/app/controllers/public/api/v1/inboxes/contacts_controller.rb +++ b/app/controllers/public/api/v1/inboxes/contacts_controller.rb @@ -43,6 +43,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon end def permitted_params - params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {}) + params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {}) end end diff --git a/app/finders/message_finder.rb b/app/finders/message_finder.rb index 63c72bd8e..e2581e0ad 100644 --- a/app/finders/message_finder.rb +++ b/app/finders/message_finder.rb @@ -22,7 +22,7 @@ class MessageFinder def current_messages if @params[:before].present? - messages.reorder('created_at desc').where('id < ?', @params[:before]).limit(20).reverse + messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse else messages.reorder('created_at desc').limit(20).reverse end diff --git a/app/helpers/report_helper.rb b/app/helpers/report_helper.rb index 9f37295cb..5fdb34170 100644 --- a/app/helpers/report_helper.rb +++ b/app/helpers/report_helper.rb @@ -17,30 +17,30 @@ module ReportHelper end def conversations_count - (get_grouped_values scope.conversations).count + (get_grouped_values scope.conversations.where(account_id: account.id)).count end def incoming_messages_count - (get_grouped_values scope.messages.incoming.unscope(:order)).count + (get_grouped_values scope.messages.where(account_id: account.id).incoming.unscope(:order)).count end def outgoing_messages_count - (get_grouped_values scope.messages.outgoing.unscope(:order)).count + (get_grouped_values scope.messages.where(account_id: account.id).outgoing.unscope(:order)).count end def resolutions_count - (get_grouped_values scope.conversations.resolved).count + (get_grouped_values scope.conversations.where(account_id: account.id).resolved).count end def avg_first_response_time - grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response')) + grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response', account_id: account.id)) return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours] grouped_reporting_events.average(:value) end def avg_resolution_time - grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')) + grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved', account_id: account.id)) return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours] grouped_reporting_events.average(:value) @@ -48,7 +48,7 @@ module ReportHelper def avg_resolution_time_summary reporting_events = scope.reporting_events - .where(name: 'conversation_resolved', created_at: range) + .where(name: 'conversation_resolved', account_id: account.id, created_at: range) avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) return 0 if avg_rt.blank? @@ -58,7 +58,7 @@ module ReportHelper def avg_first_response_time_summary reporting_events = scope.reporting_events - .where(name: 'first_response', created_at: range) + .where(name: 'first_response', account_id: account.id, created_at: range) avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value) return 0 if avg_frt.blank? diff --git a/app/javascript/dashboard/api/automation.js b/app/javascript/dashboard/api/automation.js index e83ece3d1..eef39d12c 100644 --- a/app/javascript/dashboard/api/automation.js +++ b/app/javascript/dashboard/api/automation.js @@ -9,6 +9,14 @@ class AutomationsAPI extends ApiClient { clone(automationId) { return axios.post(`${this.url}/${automationId}/clone`); } + + attachment(file) { + return axios.post(`${this.url}/attach_file`, file, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + } } export default new AutomationsAPI(); diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js index 5c39ddbe6..ca50c062f 100644 --- a/app/javascript/dashboard/api/reports.js +++ b/app/javascript/dashboard/api/reports.js @@ -44,6 +44,15 @@ class ReportsAPI extends ApiClient { }); } + getConversationMetric(type = 'account', page = 1) { + return axios.get(`${this.url}/conversations`, { + params: { + type, + page, + }, + }); + } + getAgentReports(since, until) { return axios.get(`${this.url}/agents`, { params: { since, until }, diff --git a/app/javascript/dashboard/api/specs/reports.spec.js b/app/javascript/dashboard/api/specs/reports.spec.js index efde84fe4..b51c87db5 100644 --- a/app/javascript/dashboard/api/specs/reports.spec.js +++ b/app/javascript/dashboard/api/specs/reports.spec.js @@ -97,5 +97,18 @@ describe('#Reports API', () => { } ); }); + + it('#getConversationMetric', () => { + reportsAPI.getConversationMetric('account'); + expect(context.axiosMock.get).toHaveBeenCalledWith( + '/api/v2/reports/conversations', + { + params: { + type: 'account', + page: 1, + }, + } + ); + }); }); }); diff --git a/app/javascript/dashboard/assets/scss/_utility-helpers.scss b/app/javascript/dashboard/assets/scss/_utility-helpers.scss index abea0e28c..71977cf2b 100644 --- a/app/javascript/dashboard/assets/scss/_utility-helpers.scss +++ b/app/javascript/dashboard/assets/scss/_utility-helpers.scss @@ -2,6 +2,10 @@ margin-right: var(--space-small); } +.margin-bottom-small { + margin-bottom: var(--space-small); +} + .margin-right-smaller { margin-right: var(--space-smaller); } diff --git a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss index 8a53545fe..1d2c3f63d 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss @@ -27,6 +27,16 @@ padding: 0 $space-small; } + .video-js { + background: transparent; + // Override min-height : 50px in foundation + // + max-height: $space-mega * 2.4; + min-height: 4.8rem; + padding: var(--space-normal) 0 0; + resize: none; + } + >textarea { @include ghost-input(); @include margin(0); diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index c7fda74f1..5017b3d4b 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -19,6 +19,7 @@ :menu-config="activeSecondaryMenu" :current-role="currentRole" @add-label="showAddLabelPopup" + @toggle-accounts="toggleAccountModal" /> diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js index a9c703c44..967ee44ed 100644 --- a/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js +++ b/app/javascript/dashboard/components/layout/config/sidebarItems/reports.js @@ -3,7 +3,8 @@ import { frontendURL } from '../../../../helper/URLHelper'; const reports = accountId => ({ parentNav: 'reports', routes: [ - 'settings_account_reports', + 'account_overview_reports', + 'conversation_reports', 'csat_reports', 'agent_reports', 'label_reports', @@ -16,7 +17,14 @@ const reports = accountId => ({ label: 'REPORTS_OVERVIEW', hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/reports/overview`), - toStateName: 'settings_account_reports', + toStateName: 'account_overview_reports', + }, + { + icon: 'chat', + label: 'REPORTS_CONVERSATION', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/reports/conversation`), + toStateName: 'conversation_reports', }, { icon: 'emoji', diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue b/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue index 1438ac7e8..71382d5e6 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue @@ -1,14 +1,34 @@ diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue index 8d486a454..724576347 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue @@ -1,6 +1,6 @@