From 588e8a4ee7e23f94a31def403bb6f165296ce382 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Fri, 9 May 2025 08:35:08 +0530 Subject: [PATCH 1/5] fix: throttle stalecontacts job (#11430) - throttle stale contacts job - process 20% accounts every day - reduce batch size from 100 to 20 - add delay between jobs --- .../internal/process_stale_contacts_job.rb | 31 +++++-- .../process_stale_contacts_job_spec.rb | 88 +++++++++++-------- 2 files changed, 78 insertions(+), 41 deletions(-) diff --git a/app/jobs/internal/process_stale_contacts_job.rb b/app/jobs/internal/process_stale_contacts_job.rb index 4c9990415..28143cd6a 100644 --- a/app/jobs/internal/process_stale_contacts_job.rb +++ b/app/jobs/internal/process_stale_contacts_job.rb @@ -1,5 +1,5 @@ # housekeeping -# remove stale contacts for all accounts +# remove stale contacts for subset of accounts each day # - have no identification (email, phone_number, and identifier are NULL) # - have no conversations # - are older than 30 days @@ -7,14 +7,33 @@ class Internal::ProcessStaleContactsJob < ApplicationJob queue_as :housekeeping + # Number of day-based groups to split accounts into + DISTRIBUTION_GROUPS = 5 + # Max accounts to process in one batch + MAX_ACCOUNTS_PER_BATCH = 20 + + # Process only a subset of accounts per day to avoid flooding the queue def perform return unless ChatwootApp.chatwoot_cloud? - Account.find_in_batches(batch_size: 100) do |accounts| - accounts.each do |account| - Rails.logger.info "Enqueuing RemoveStaleContactsJob for account #{account.id}" - Internal::RemoveStaleContactsJob.perform_later(account) - end + # Use the day of the month to determine which accounts to process + day_of_month = Date.current.day + remainder = day_of_month % DISTRIBUTION_GROUPS + + # Count total accounts for logging + total_accounts = Account.count + log_message = "ProcessStaleContactsJob: Processing accounts with ID % #{DISTRIBUTION_GROUPS} = " + log_message += "#{remainder} (out of #{total_accounts} total accounts)" + Rails.logger.info log_message + + # Process only accounts where ID % 5 = remainder for today + # This ensures each account is processed approximately once every 5 days + Account.where("id % #{DISTRIBUTION_GROUPS} = ?", remainder).find_each(batch_size: MAX_ACCOUNTS_PER_BATCH) do |account| + Rails.logger.info "Enqueuing RemoveStaleContactsJob for account #{account.id}" + + # Add a small delay between jobs to further reduce queue pressure + delay = rand(1..10).minutes + Internal::RemoveStaleContactsJob.set(wait: delay).perform_later(account) end end end diff --git a/spec/jobs/internal/process_stale_contacts_job_spec.rb b/spec/jobs/internal/process_stale_contacts_job_spec.rb index f7f17bf78..ca873756c 100644 --- a/spec/jobs/internal/process_stale_contacts_job_spec.rb +++ b/spec/jobs/internal/process_stale_contacts_job_spec.rb @@ -3,44 +3,62 @@ require 'rails_helper' RSpec.describe Internal::ProcessStaleContactsJob do subject(:job) { described_class.perform_later } - it 'enqueues the job' do - allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) - expect { job }.to have_enqueued_job(described_class) - .on_queue('housekeeping') + context 'when in cloud environment' do + before do + allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) + end + + it 'processes accounts based on the day of month' do + # Set a fixed day for testing + day_of_month = 16 + remainder = day_of_month % described_class::DISTRIBUTION_GROUPS + allow(Date).to receive(:current).and_return(Date.new(2025, 5, day_of_month)) + + # Create an account and set its ID to match today's pattern + account = create(:account) + allow(account).to receive(:id).and_return(remainder) + + # Mock the Account.where to return our filtered accounts + account_relation = double + allow(Account).to receive(:where).with("id % #{described_class::DISTRIBUTION_GROUPS} = ?", remainder).and_return(account_relation) + allow(account_relation).to receive(:find_each).and_yield(account) + + # Mock the delay setting + allow(Internal::RemoveStaleContactsJob).to receive(:set).and_return(Internal::RemoveStaleContactsJob) + expect(Internal::RemoveStaleContactsJob).to receive(:perform_later).with(account) + + described_class.perform_now + end + + it 'adds a delay between jobs' do + day_of_month = 15 + remainder = day_of_month % described_class::DISTRIBUTION_GROUPS + allow(Date).to receive(:current).and_return(Date.new(2025, 5, day_of_month)) + + account = create(:account) + + account_relation = double + allow(Account).to receive(:where).with("id % #{described_class::DISTRIBUTION_GROUPS} = ?", remainder).and_return(account_relation) + allow(account_relation).to receive(:find_each).and_yield(account) + + expect(Internal::RemoveStaleContactsJob).to receive(:set) do |args| + expect(args[:wait]).to be_between(1.minute, 10.minutes) + Internal::RemoveStaleContactsJob + end + expect(Internal::RemoveStaleContactsJob).to receive(:perform_later).with(account) + + described_class.perform_now + end end - it 'enqueues RemoveStaleContactsJob for each account' do - allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) - account1 = create(:account) - account2 = create(:account) - account3 = create(:account) + context 'when not in cloud environment' do + it 'does not process any accounts' do + allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false) - expect { described_class.perform_now }.to have_enqueued_job(Internal::RemoveStaleContactsJob) - .with(account1) - .on_queue('housekeeping') - expect { described_class.perform_now }.to have_enqueued_job(Internal::RemoveStaleContactsJob) - .with(account2) - .on_queue('housekeeping') - expect { described_class.perform_now }.to have_enqueued_job(Internal::RemoveStaleContactsJob) - .with(account3) - .on_queue('housekeeping') - end + expect(Account).not_to receive(:where) + expect(Internal::RemoveStaleContactsJob).not_to receive(:perform_later) - it 'processes accounts in batches' do - allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) - account = create(:account) - allow(Account).to receive(:find_in_batches).with(batch_size: 100).and_yield([account]) - - expect(Internal::RemoveStaleContactsJob).to receive(:perform_later).with(account) - described_class.perform_now - end - - it 'does not process accounts when not in cloud environment' do - allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false) - create(:account) - - expect(Account).not_to receive(:find_in_batches) - expect(Internal::RemoveStaleContactsJob).not_to receive(:perform_later) - described_class.perform_now + described_class.perform_now + end end end From 4d684da2881e95a82680a2a638c044214f2a5a19 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 9 May 2025 08:38:12 +0530 Subject: [PATCH 2/5] fix: Update the copy for excluding the unattended conversations (#11450) --- app/javascript/dashboard/i18n/locale/en/generalSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index e6800820b..7da76df18 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -70,7 +70,7 @@ }, "AUTO_RESOLVE_IGNORE_WAITING": { "LABEL": "Exclude unattended conversations", - "HELP": "If toggled, the system will not resolve conversations that have been waiting for an agent reply." + "HELP": "When enabled, the system will skip resolving conversations that are still waiting for an agent’s reply." }, "AUTO_RESOLVE_DURATION": { "LABEL": "Inactivity duration for resolution", From 27430752b5e1ad89d322ce7240aa17f6c34d2f93 Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 8 May 2025 20:11:02 -0700 Subject: [PATCH 3/5] feat: Allow agent bots to update custom attributes in accessible conversations (#11447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, agent bots weren’t allowed to edit custom attributes in conversations. But with AI, it’s now more feasible to return accurate and useful attributes. Since there’s no strong reason to block this, this PR enables bots to update custom attributes. Fixes https://github.com/chatwoot/chatwoot/issues/11378 --- .../concerns/access_token_auth_helper.rb | 2 +- .../accounts/conversations_controller_spec.rb | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index 2ee9f9854..9b0f9021f 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -1,6 +1,6 @@ module AccessTokenAuthHelper BOT_ACCESSIBLE_ENDPOINTS = { - 'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update], + 'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update custom_attributes], 'api/v1/accounts/conversations/messages' => ['create'], 'api/v1/accounts/conversations/assignments' => ['create'] }.freeze diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index d93886fc3..a3697be84 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -843,7 +843,7 @@ RSpec.describe 'Conversations API', type: :request do create(:inbox_member, user: agent, inbox: conversation.inbox) end - it 'updates last seen' do + it 'updates custom attributes' do post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/custom_attributes", headers: agent.create_new_auth_token, params: valid_params, @@ -854,6 +854,27 @@ RSpec.describe 'Conversations API', type: :request do expect(conversation.reload.custom_attributes.count).to eq 3 end end + + context 'when it is a bot' do + let(:agent_bot) { create(:agent_bot, account: account) } + let(:custom_attributes) { { bot_id: 1001, flow_name: 'support_flow', step: 'greeting' } } + let(:valid_params) { { custom_attributes: custom_attributes } } + + before do + create(:agent_bot_inbox, agent_bot: agent_bot, inbox: conversation.inbox) + end + + it 'updates custom attributes' do + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/custom_attributes", + headers: { api_access_token: agent_bot.access_token.token }, + params: valid_params, + as: :json + + expect(response).to have_http_status(:success) + expect(conversation.reload.custom_attributes).not_to be_nil + expect(conversation.reload.custom_attributes.count).to eq 3 + end + end end describe 'GET /api/v1/accounts/{account.id}/conversations/:id/attachments' do From a57dfe44785f5c5efc7ac6a1042a020e196bc631 Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 8 May 2025 20:12:05 -0700 Subject: [PATCH 4/5] fix: Allow resource access without filter type in custom_filters API (#11445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The custom filters API previously required a filter_type attribute, even when accessing a resource by its ID, which isn’t necessary. This PR removes that condition. Fixes https://github.com/chatwoot/chatwoot/issues/11384 --- .../api/v1/accounts/custom_filters_controller.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/accounts/custom_filters_controller.rb b/app/controllers/api/v1/accounts/custom_filters_controller.rb index f458c018f..b4345bb8a 100644 --- a/app/controllers/api/v1/accounts/custom_filters_controller.rb +++ b/app/controllers/api/v1/accounts/custom_filters_controller.rb @@ -1,6 +1,6 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController before_action :check_authorization - before_action :fetch_custom_filters, except: [:create] + before_action :fetch_custom_filters, only: [:index] before_action :fetch_custom_filter, only: [:show, :update, :destroy] DEFAULT_FILTER_TYPE = 'conversation'.freeze @@ -9,8 +9,8 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro def show; end def create - @custom_filter = current_user.custom_filters.create!( - permitted_payload.merge(account_id: Current.account.id) + @custom_filter = Current.account.custom_filters.create!( + permitted_payload.merge(user: Current.user) ) render json: { error: @custom_filter.errors.messages }, status: :unprocessable_entity and return unless @custom_filter.valid? end @@ -27,14 +27,16 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro private def fetch_custom_filters - @custom_filters = current_user.custom_filters.where( - account_id: Current.account.id, + @custom_filters = Current.account.custom_filters.where( + user: Current.user, filter_type: permitted_params[:filter_type] || DEFAULT_FILTER_TYPE ) end def fetch_custom_filter - @custom_filter = @custom_filters.find(permitted_params[:id]) + @custom_filter = Current.account.custom_filters.where( + user: Current.user + ).find(permitted_params[:id]) end def permitted_payload From 61c5d751fca98f1d64e816b35d126f39413dd07f Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 8 May 2025 20:14:44 -0700 Subject: [PATCH 5/5] chore: Make the table of contents in help center sticky (#11448) --- app/javascript/portal/components/TableOfContents.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/portal/components/TableOfContents.vue b/app/javascript/portal/components/TableOfContents.vue index 16da81f71..ec7ccd895 100644 --- a/app/javascript/portal/components/TableOfContents.vue +++ b/app/javascript/portal/components/TableOfContents.vue @@ -94,8 +94,8 @@ export default {