From dd1f93d425fda7ddb6a6a8cd9c267290076f7fa2 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Fri, 10 May 2024 22:22:04 +0530 Subject: [PATCH 01/21] feat: Switch Heroku Postgres basic to essential-0 (#9452) --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index 4ee025f06..d50040814 100644 --- a/app.json +++ b/app.json @@ -55,7 +55,7 @@ "plan": "heroku-redis:mini" }, { - "plan": "heroku-postgresql:mini" + "plan": "heroku-postgresql:essential-0" } ], "stack": "heroku-20", From 9a8442fe0e08e252d566d56744ef8c624a9b4249 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 10 May 2024 12:21:23 -0700 Subject: [PATCH 02/21] chore: Handle conversation participation creation race condition error (#9449) We observed some race condition errors in the conversation participation listener while trying to create a conversation participation assignment. This PR handles this error and also adds additional debug information for future. fixes: https://linear.app/chatwoot/issue/CW-3296/activerecordrecordnotunique-pguniqueviolation-error-duplicate-key ## Changelog - handles `ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvald` errors so that they won't pollute sentry - Adds a debug statement to log the cases - Add previous_changes into the dispatcher so that we know the exact attribute changes which trigger `assignee_changed, team_changed` events ( would be handy in future ) --- app/listeners/participation_listener.rb | 9 ++++++++- app/models/concerns/assignment_handler.rb | 2 +- spec/listeners/participation_listener_spec.rb | 16 ++++++++++++++-- spec/models/conversation_spec.rb | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/listeners/participation_listener.rb b/app/listeners/participation_listener.rb index d0a5f48c2..b9f94e252 100644 --- a/app/listeners/participation_listener.rb +++ b/app/listeners/participation_listener.rb @@ -3,6 +3,13 @@ class ParticipationListener < BaseListener def assignee_changed(event) conversation, _account = extract_conversation_and_account(event) - conversation.conversation_participants.find_or_create_by!(user_id: conversation.assignee_id) if conversation.assignee_id.present? + return if conversation.assignee_id.blank? + + conversation.conversation_participants.find_or_create_by!(user_id: conversation.assignee_id) + # We have observed race conditions triggering these errors + # example: Assignment happening via automation, while auto assignment is also configured. + rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid + Rails.logger.warn "Failed to create conversation participant for account #{conversation.account.id} " \ + ": user #{conversation.assignee_id} : conversation #{conversation.id}" end end diff --git a/app/models/concerns/assignment_handler.rb b/app/models/concerns/assignment_handler.rb index 0fab737ed..5dc89f779 100644 --- a/app/models/concerns/assignment_handler.rb +++ b/app/models/concerns/assignment_handler.rb @@ -32,7 +32,7 @@ module AssignmentHandler ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? }, TEAM_CHANGED => -> { saved_change_to_team_id? } }.each do |event, condition| - condition.call && dispatcher_dispatch(event) + condition.call && dispatcher_dispatch(event, previous_changes) end end diff --git a/spec/listeners/participation_listener_spec.rb b/spec/listeners/participation_listener_spec.rb index 69c030de6..9ed70fc33 100644 --- a/spec/listeners/participation_listener_spec.rb +++ b/spec/listeners/participation_listener_spec.rb @@ -9,8 +9,6 @@ describe ParticipationListener do before do create(:inbox_member, inbox: inbox, user: agent) - Current.user = nil - Current.account = nil end describe '#assignee_changed' do @@ -22,5 +20,19 @@ describe ParticipationListener do listener.assignee_changed(event) expect(conversation.conversation_participants.map(&:user_id)).to include(agent.id) end + + it 'does not fail if the conversation participant already exists' do + conversation.conversation_participants.create!(user: agent) + expect { listener.assignee_changed(event) }.not_to raise_error + end + + it 'logs a debug message if participant save fails due to a race condition' do + allow(Rails.logger).to receive(:warn) + allow(conversation).to receive(:conversation_participants).and_return(double) + allow(conversation.conversation_participants).to receive(:find_or_create_by!).and_raise(ActiveRecord::RecordNotUnique) + expect { listener.assignee_changed(event) }.not_to raise_error + expect(Rails.logger).to have_received(:warn).with('Failed to create conversation participant for account ' \ + "#{account.id} : user #{agent.id} : conversation #{conversation.id}") + end end end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 2f79b08f2..0ca7303ef 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -156,7 +156,7 @@ RSpec.describe Conversation do changed_attributes: nil, performed_by: nil) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, - changed_attributes: nil, performed_by: nil) + changed_attributes: changed_attributes, performed_by: nil) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true, changed_attributes: changed_attributes, performed_by: nil) From 07e33fd98a8e59c813b105a769383e7bcd7e3ae1 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 13 May 2024 13:32:11 -0700 Subject: [PATCH 03/21] chore: Switch models to gpt-4o (#9458) - Switch model to gpt-4o from gpt-4-turbo --- enterprise/app/models/enterprise/concerns/article.rb | 2 +- enterprise/lib/chat_gpt.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/enterprise/app/models/enterprise/concerns/article.rb b/enterprise/app/models/enterprise/concerns/article.rb index e230a3a28..799584568 100644 --- a/enterprise/app/models/enterprise/concerns/article.rb +++ b/enterprise/app/models/enterprise/concerns/article.rb @@ -66,7 +66,7 @@ module Enterprise::Concerns::Article { role: 'user', content: "title: #{title} \n description: #{description} \n content: #{content}" } ] headers = { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{ENV.fetch('OPENAI_API_KEY', nil)}" } - body = { model: 'gpt-4-turbo', messages: messages, response_format: { type: 'json_object' } }.to_json + body = { model: 'gpt-4o', messages: messages, response_format: { type: 'json_object' } }.to_json Rails.logger.info "Requesting Chat GPT with body: #{body}" response = HTTParty.post('https://api.openai.com/v1/chat/completions', headers: headers, body: body) Rails.logger.info "Chat GPT response: #{response.body}" diff --git a/enterprise/lib/chat_gpt.rb b/enterprise/lib/chat_gpt.rb index d13ec118f..44afbd641 100644 --- a/enterprise/lib/chat_gpt.rb +++ b/enterprise/lib/chat_gpt.rb @@ -4,7 +4,7 @@ class ChatGpt end def initialize(context_sections = '') - @model = 'gpt-4-0125-preview' + @model = 'gpt-4o' @messages = [system_message(context_sections)] end From e992283993e89e9b4120943395eb91d7b2414104 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 13 May 2024 16:07:56 -0700 Subject: [PATCH 04/21] fix: [Snyk] Security upgrade omniauth-rails_csrf_protection from 1.0.1 to 1.0.2 (#9454) fix: Gemfile & Gemfile.lock to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-RACK-1061917 Co-authored-by: snyk-bot --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 38dee9bc4..9241cc253 100644 --- a/Gemfile +++ b/Gemfile @@ -166,7 +166,7 @@ gem 'audited', '~> 5.4', '>= 5.4.1' # need for google auth gem 'omniauth', '>= 2.1.2' gem 'omniauth-google-oauth2', '>= 1.1.2' -gem 'omniauth-rails_csrf_protection', '~> 1.0' +gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2' ## Gems for reponse bot # adds cosine similarity to postgres using vector extension diff --git a/Gemfile.lock b/Gemfile.lock index 5dc67bef1..f9298ee22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM barnes (0.0.9) multi_json (~> 1) statsd-ruby (~> 1.1) - base64 (0.1.1) + base64 (0.2.0) bcrypt (3.1.20) bigdecimal (3.1.7) bindex (0.8.1) @@ -369,7 +369,7 @@ GEM mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.14.4) + i18n (1.14.5) concurrent-ruby (~> 1.0) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) @@ -524,7 +524,7 @@ GEM omniauth-oauth2 (1.8.0) oauth2 (>= 1.4, < 3) omniauth (~> 2.0) - omniauth-rails_csrf_protection (1.0.1) + omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) openssl (3.1.0) @@ -904,7 +904,7 @@ DEPENDENCIES omniauth (>= 2.1.2) omniauth-google-oauth2 (>= 1.1.2) omniauth-oauth2 - omniauth-rails_csrf_protection (~> 1.0) + omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2) pg pg_search pgvector From 1d4798a3bf4115c80b127a03a3aab0d44383b8ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 18:12:02 -0700 Subject: [PATCH 05/21] chore(deps): bump nokogiri from 1.16.4 to 1.16.5 (#9459) Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.16.4 to 1.16.5. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.16.4...v1.16.5) --- updated-dependencies: - dependency-name: nokogiri dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f9298ee22..2aa13fea9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -490,14 +490,14 @@ GEM newrelic_rpm (9.6.0) base64 nio4r (2.7.1) - nokogiri (1.16.4) + nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.4-arm64-darwin) + nokogiri (1.16.5-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.4-x86_64-darwin) + nokogiri (1.16.5-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.4-x86_64-linux) + nokogiri (1.16.5-x86_64-linux) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) From d54492f7b52c217c09d3e288751523a595800aec Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 14 May 2024 14:19:02 -0700 Subject: [PATCH 06/21] chore: Add debug statement in spec (#9466) - Add a debug statement to check the failed specs --- spec/controllers/api/v1/accounts/contacts_controller_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index 815cecc62..97eddc171 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -97,6 +97,8 @@ RSpec.describe 'Contacts API', type: :request do expect(response).to have_http_status(:success) response_body = response.parsed_body + # TODO: this spec has been flaky for a while, so adding a debug statement to see the response + Rails.logger.info(response_body) expect(response_body['payload'].first['email']).to eq(contact.email) expect(response_body['payload'].first['id']).to eq(contact.id) expect(response_body['payload'].last['email']).to eq(contact_4.email) From e98e27dc1fd5a862d8b79d6aa093e5790dc09a8c Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 14 May 2024 14:32:17 -0700 Subject: [PATCH 07/21] chore: Make IP_LOOKUP_BASE_URL configurable (#9467) Since we download the GeoIP database during worker/server initialization, there is a high chance of spamming the server with too many requests for downloads, especially if the number of web and worker nodes is high. This PR provides the ability to specify a custom URL for the GeoLite database download, configurable via an environment variable. This helps in distributing the load and avoiding server overload during the initialization process --- lib/tasks/ip_lookup.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/ip_lookup.rake b/lib/tasks/ip_lookup.rake index d0db9dbc5..3225caffa 100644 --- a/lib/tasks/ip_lookup.rake +++ b/lib/tasks/ip_lookup.rake @@ -13,7 +13,7 @@ namespace :ip_lookup do Rails.logger.info '[rake ip_lookup:setup] Fetch GeoLite2-City database' begin - base_url = 'https://download.maxmind.com/app/geoip_download' + base_url = ENV.fetch('IP_LOOKUP_BASE_URL', 'https://download.maxmind.com/app/geoip_download') source_file = Down.download( "#{base_url}?edition_id=GeoLite2-City&suffix=tar.gz&license_key=#{ip_lookup_api_key}" ) From 5a289776de5e8f77f134680c8fe54da903419a9b Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 15 May 2024 09:48:55 +0530 Subject: [PATCH 08/21] fix: Widget phone number input country undefined in onSelectCountry (#9457) * fix: Widget phone number input country undefined in onSelectCountry * chore: Minor fix --- .../widget/components/Form/PhoneInput.vue | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/app/javascript/widget/components/Form/PhoneInput.vue b/app/javascript/widget/components/Form/PhoneInput.vue index 8a37d02c9..c7fafa916 100644 --- a/app/javascript/widget/components/Form/PhoneInput.vue +++ b/app/javascript/widget/components/Form/PhoneInput.vue @@ -39,6 +39,9 @@ v-on-clickaway="closeDropdown" :class="dropdownBackgroundClass" class="country-dropdown h-48 overflow-y-auto z-10 absolute top-12 px-0 pt-0 pl-1 pr-1 pb-1 rounded shadow-lg" + @keydown.up="moveSelectionUp" + @keydown.down="moveSelectionDown" + @keydown.enter="onSelect" >
import countries from 'shared/constants/countries.js'; import FluentIcon from 'shared/components/FluentIcon/Index.vue'; -import mentionSelectionKeyboardMixin from 'dashboard/components/widgets/mentions/mentionSelectionKeyboardMixin.js'; import FormulateInputMixin from '@braid/vue-formulate/src/FormulateInputMixin'; import darkModeMixin from 'widget/mixins/darkModeMixin'; @@ -93,7 +95,7 @@ export default { components: { FluentIcon, }, - mixins: [mentionSelectionKeyboardMixin, FormulateInputMixin, darkModeMixin], + mixins: [FormulateInputMixin, darkModeMixin], props: { placeholder: { type: String, @@ -185,6 +187,14 @@ export default { ); }, }, + watch: { + items(newItems) { + if (newItems.length < this.selectedIndex + 1) { + // Reset the selected index to 0 if the new items length is less than the selected index. + this.selectedIndex = 0; + } + }, + }, methods: { setContextValue(code) { // This function is used to set the context value. @@ -235,7 +245,26 @@ export default { this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('focus')); }); }, + adjustSelection(direction) { + if (!this.showDropdown) return; + const maxIndex = this.items.length - 1; + if (direction === 'up') { + this.selectedIndex = + this.selectedIndex <= 0 ? maxIndex : this.selectedIndex - 1; + } else if (direction === 'down') { + this.selectedIndex = + this.selectedIndex >= maxIndex ? 0 : this.selectedIndex + 1; + } + this.adjustScroll(); + }, + moveSelectionUp() { + this.adjustSelection('up'); + }, + moveSelectionDown() { + this.adjustSelection('down'); + }, onSelect() { + if (!this.showDropdown || this.selectedIndex === -1) return; this.onSelectCountry(this.items[this.selectedIndex]); }, scrollToFocusedOrActiveItem(item) { From bc8736c08e8e5f68ad4281d15ab240319b0e2ab5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 15 May 2024 10:45:03 +0530 Subject: [PATCH 09/21] fix: widget does not load when navigating on pages with view transition [CW-3249] (#9443) * feat: add ids to each element * feat: restore elements for apps that use view transitions * fix: remove generator check condition * feat: handle turbolinks * fix: new body handling * chore: undo debug changes --- app/javascript/packs/sdk.js | 25 ++++++++++++++++++++++++- app/javascript/sdk/DOMHelpers.js | 19 +++++++++++++++++++ app/javascript/sdk/IFrameHelper.js | 1 + app/javascript/sdk/bubbleHelpers.js | 1 + 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index 9bb8314ca..84cf5fe2d 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -10,14 +10,37 @@ import { getUserCookieName, hasUserKeys, } from '../sdk/cookieHelpers'; -import { addClasses, removeClasses } from '../sdk/DOMHelpers'; +import { + addClasses, + removeClasses, + restoreWidgetInDOM, +} from '../sdk/DOMHelpers'; import { setCookieWithDomain } from '../sdk/cookieHelpers'; import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents'; + const runSDK = ({ baseUrl, websiteToken }) => { if (window.$chatwoot) { return; } + if (window.Turbo) { + // if this is a Rails Turbo app + document.addEventListener('turbo:before-render', event => + restoreWidgetInDOM(event.detail.newBody) + ); + } + + if (window.Turbolinks) { + document.addEventListener('turbolinks:before-render', event => { + restoreWidgetInDOM(event.data.newBody); + }); + } + + // if this is an astro app + document.addEventListener('astro:before-swap', event => + restoreWidgetInDOM(event.newDocument.body) + ); + const chatwootSettings = window.chatwootSettings || {}; let locale = chatwootSettings.locale; let baseDomain = chatwootSettings.baseDomain; diff --git a/app/javascript/sdk/DOMHelpers.js b/app/javascript/sdk/DOMHelpers.js index 47a45cc78..f8ba55337 100644 --- a/app/javascript/sdk/DOMHelpers.js +++ b/app/javascript/sdk/DOMHelpers.js @@ -4,9 +4,28 @@ import { IFrameHelper } from './IFrameHelper'; export const loadCSS = () => { const css = document.createElement('style'); css.innerHTML = `${SDK_CSS}`; + css.id = 'cw-widget-styles'; document.body.appendChild(css); }; +// This is a method specific to Turbo +// The body replacing strategy removes Chatwoot styles +// as well as the widget, this help us get it back +export const restoreElement = (id, newBody) => { + const element = document.getElementById(id); + const newElement = newBody.querySelector(`#${id}`); + + if (element && !newElement) { + newBody.appendChild(element); + } +}; + +export const restoreWidgetInDOM = newBody => { + restoreElement('cw-bubble-holder', newBody); + restoreElement('cw-widget-holder', newBody); + restoreElement('cw-widget-styles', newBody); +}; + export const addClasses = (elm, classes) => { elm.classList.add(...classes.split(' ')); }; diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 2166b4304..3687fa311 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -78,6 +78,7 @@ export const IFrameHelper = { } addClasses(widgetHolder, holderClassName); + widgetHolder.id = 'cw-widget-holder'; widgetHolder.appendChild(iframe); body.appendChild(widgetHolder); IFrameHelper.initPostMessageCommunication(); diff --git a/app/javascript/sdk/bubbleHelpers.js b/app/javascript/sdk/bubbleHelpers.js index 54401a82c..600c1444a 100644 --- a/app/javascript/sdk/bubbleHelpers.js +++ b/app/javascript/sdk/bubbleHelpers.js @@ -61,6 +61,7 @@ export const createBubbleHolder = hideMessageBubble => { addClasses(bubbleHolder, 'woot-hidden'); } addClasses(bubbleHolder, 'woot--bubble-holder'); + bubbleHolder.id = 'cw-bubble-holder'; body.appendChild(bubbleHolder); }; From fc1c992cdee8b55bcbeaa61a24d3a16a67e5b4b0 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 15 May 2024 11:52:40 -0700 Subject: [PATCH 10/21] fix: [Snyk] Security upgrade devise_token_auth from 1.2.1 to 1.2.3 (#9468) fix: Gemfile & Gemfile.lock to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-ACTIONCABLE-20338 - https://snyk.io/vuln/SNYK-RUBY-RACK-1061917 Co-authored-by: snyk-bot --- Gemfile | 2 +- Gemfile.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 9241cc253..bee9eddc1 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ gem 'barnes' ##--- gems for authentication & authorization ---## gem 'devise', '>= 4.9.4' gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot' -gem 'devise_token_auth' +gem 'devise_token_auth', '>= 1.2.3' # authorization gem 'jwt' gem 'pundit' diff --git a/Gemfile.lock b/Gemfile.lock index 2aa13fea9..2f890deaa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -200,10 +200,10 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise_token_auth (1.2.1) + devise_token_auth (1.2.3) bcrypt (~> 3.0) devise (> 3.5.2, < 5) - rails (>= 4.2.0, < 7.1) + rails (>= 4.2.0, < 7.2) diff-lcs (1.5.0) digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) @@ -474,7 +474,7 @@ GEM uri net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.10) + net-imap (0.4.11) date net-protocol net-pop (0.1.2) @@ -489,7 +489,7 @@ GEM sidekiq newrelic_rpm (9.6.0) base64 - nio4r (2.7.1) + nio4r (2.7.3) nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -819,7 +819,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.13) + zeitwerk (2.6.14) PLATFORMS arm64-darwin-20 @@ -860,7 +860,7 @@ DEPENDENCIES debug (~> 1.8) devise (>= 4.9.4) devise-secure_password! - devise_token_auth + devise_token_auth (>= 1.2.3) dotenv-rails down elastic-apm From 7ed375f6f55b259d8242117ce728365259c24b87 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 15 May 2024 15:53:41 -0700 Subject: [PATCH 11/21] chore: Show valid error messages on Inbox creation (#9474) At the moment, when creating an inbox for Whatsapp, Telegram, etc., we show a generic error message saying that inbox creation failed. This PR will show the error messages directly from the API call, which is more helpful as it says if the error is due to the provided credentials. --- .../dashboard/settings/inbox/channels/360DialogWhatsapp.vue | 4 +++- .../dashboard/settings/inbox/channels/CloudWhatsapp.vue | 4 +++- .../routes/dashboard/settings/inbox/channels/Telegram.vue | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/360DialogWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/360DialogWhatsapp.vue index ded9142db..ebac2a2d1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/360DialogWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/360DialogWhatsapp.vue @@ -111,7 +111,9 @@ export default { }, }); } catch (error) { - this.showAlert(this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE')); + this.showAlert( + error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE') + ); } }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue index a6253e516..dd320dd17 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue @@ -155,7 +155,9 @@ export default { }, }); } catch (error) { - this.showAlert(this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE')); + this.showAlert( + error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE') + ); } }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Telegram.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Telegram.vue index 1f677bd66..c7cbaeb4a 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Telegram.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Telegram.vue @@ -86,7 +86,8 @@ export default { }); } catch (error) { this.showAlert( - this.$t('INBOX_MGMT.ADD.TELEGRAM_CHANNEL.API.ERROR_MESSAGE') + error.message || + this.$t('INBOX_MGMT.ADD.TELEGRAM_CHANNEL.API.ERROR_MESSAGE') ); } }, From 8520846b91aaf3ecca048f31d47754625c1f89b7 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 15 May 2024 16:10:39 -0700 Subject: [PATCH 12/21] chore: Improved indexes for Conversations & Contacts [CW-3300] (#9475) Based on our recent performant optimisation exercises, We have identified a better indexing strategy for conversations and contacts. The previous index on last_activity_at for conversations significantly slowed down conversation filters. Similarly, the new index on Contacts will allow the page rendering to improve for accounts with many contacts. fixes: https://linear.app/chatwoot/issue/CW-3300/db-improvements --- app/models/contact.rb | 1 + app/models/conversation.rb | 1 - ...01632_index_improvements_conversations_contacts.rb | 11 +++++++++++ db/schema.rb | 4 ++-- 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20240515201632_index_improvements_conversations_contacts.rb diff --git a/app/models/contact.rb b/app/models/contact.rb index 95ee69c75..c79dd3e9e 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -25,6 +25,7 @@ # Indexes # # index_contacts_on_account_id (account_id) +# index_contacts_on_account_id_and_last_activity_at (account_id,last_activity_at DESC NULLS LAST) # index_contacts_on_blocked (blocked) # index_contacts_on_lower_email_account_id (lower((email)::text), account_id) # index_contacts_on_name_email_phone_number_identifier (name,email,phone_number,identifier) USING gin diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 64412b289..3a39b82fc 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -41,7 +41,6 @@ # index_conversations_on_first_reply_created_at (first_reply_created_at) # index_conversations_on_id_and_account_id (account_id,id) # index_conversations_on_inbox_id (inbox_id) -# index_conversations_on_last_activity_at (last_activity_at) # index_conversations_on_priority (priority) # index_conversations_on_status_and_account_id (status,account_id) # index_conversations_on_status_and_priority (status,priority) diff --git a/db/migrate/20240515201632_index_improvements_conversations_contacts.rb b/db/migrate/20240515201632_index_improvements_conversations_contacts.rb new file mode 100644 index 000000000..f6ed8596e --- /dev/null +++ b/db/migrate/20240515201632_index_improvements_conversations_contacts.rb @@ -0,0 +1,11 @@ +class IndexImprovementsConversationsContacts < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + remove_index :conversations, :last_activity_at + add_index :contacts, [:account_id, :last_activity_at], + order: { last_activity_at: 'DESC NULLS LAST' }, + algorithm: :concurrently, + name: 'index_contacts_on_account_id_and_last_activity_at' + end +end diff --git a/db/schema.rb b/db/schema.rb index 1282c6e5e..496d2e280 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_04_15_210313) do +ActiveRecord::Schema[7.0].define(version: 2024_05_15_201632) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -426,6 +426,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_15_210313) do t.boolean "blocked", default: false, null: false t.index "lower((email)::text), account_id", name: "index_contacts_on_lower_email_account_id" t.index ["account_id", "email", "phone_number", "identifier"], name: "index_contacts_on_nonempty_fields", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))" + t.index ["account_id", "last_activity_at"], name: "index_contacts_on_account_id_and_last_activity_at", order: { last_activity_at: "DESC NULLS LAST" } t.index ["account_id"], name: "index_contacts_on_account_id" t.index ["account_id"], name: "index_resolved_contact_account_id", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))" t.index ["blocked"], name: "index_contacts_on_blocked" @@ -483,7 +484,6 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_15_210313) do t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id" t.index ["first_reply_created_at"], name: "index_conversations_on_first_reply_created_at" t.index ["inbox_id"], name: "index_conversations_on_inbox_id" - t.index ["last_activity_at"], name: "index_conversations_on_last_activity_at" t.index ["priority"], name: "index_conversations_on_priority" t.index ["status", "account_id"], name: "index_conversations_on_status_and_account_id" t.index ["status", "priority"], name: "index_conversations_on_status_and_priority" From ae5ef73e915a5bf969112ec9bc22c1fad0ae7bb8 Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 15 May 2024 17:53:45 -0700 Subject: [PATCH 13/21] fix: Update the voice note format to MP3 to fix the delivery issues (#9448) Use MP3 as the default format to send voice notes recorded from Chatwoot. This change was made to fix the issue of Telegram voice notes not working with the error `WEBPAGE_CURL_FAILED` . Telegram treats the mp3 recordings as audio attachments. Once we can identify a fix for the original issue, we will revisit the `ogg` implementation. --------- Co-authored-by: Sojan Jose --- .../widgets/WootWriter/AudioRecorder.vue | 37 ++++++++++++------- .../widgets/conversation/ReplyBox.vue | 8 +++- app/javascript/shared/constants/messages.js | 1 + 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue b/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue index 132408dc2..aa566fd4d 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue @@ -33,6 +33,21 @@ import { convertWavToMp3 } from './utils/mp3ConversionUtils'; WaveSurfer.microphone = MicrophonePlugin; +const RECORDER_CONFIG = { + [AUDIO_FORMATS.WAV]: { + audioMimeType: 'audio/wav', + audioWorkerURL: waveWorker, + }, + [AUDIO_FORMATS.MP3]: { + audioMimeType: 'audio/wav', + audioWorkerURL: waveWorker, + }, + [AUDIO_FORMATS.OGG]: { + audioMimeType: 'audio/ogg', + audioWorkerURL: encoderWorker, + }, +}; + export default { name: 'WootAudioRecorder', mixins: [alertMixin], @@ -94,14 +109,7 @@ export default { audioSampleRate: 48000, audioBitRate: 128, audioEngine: 'opus-recorder', - ...(this.audioRecordFormat === AUDIO_FORMATS.WAV && { - audioMimeType: 'audio/wav', - audioWorkerURL: waveWorker, - }), - ...(this.audioRecordFormat === AUDIO_FORMATS.OGG && { - audioMimeType: 'audio/ogg', - audioWorkerURL: encoderWorker, - }), + ...RECORDER_CONFIG[this.audioRecordFormat], }, }, }, @@ -139,7 +147,11 @@ export default { methods: { deviceReady() { if (this.player.record().engine instanceof OpusRecorderEngine) { - if (this.audioRecordFormat === AUDIO_FORMATS.WAV) { + if ( + [AUDIO_FORMATS.WAV, AUDIO_FORMATS.MP3].includes( + this.audioRecordFormat + ) + ) { this.player.record().engine.audioType = 'audio/wav'; } } @@ -154,13 +166,12 @@ export default { async finishRecord() { let recordedContent = this.player.recordedData; let fileName = this.player.recordedData.name; - if (this.isAWhatsAppChannel) { + let type = this.player.recordedData.type; + if (this.audioRecordFormat === AUDIO_FORMATS.MP3) { recordedContent = await convertWavToMp3(this.player.recordedData); fileName = `${getUuid()}.mp3`; + type = AUDIO_FORMATS.MP3; } - const type = !this.isAWhatsAppChannel - ? this.player.recordedData.type - : 'audio/mp3'; const file = new File([recordedContent], fileName, { type }); this.fireRecorderBlob(file); }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 83d31d11f..42fa74ec7 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -53,7 +53,6 @@ v-if="showAudioRecorderEditor" ref="audioRecorderInput" :audio-record-format="audioRecordFormat" - :is-a-whats-app-channel="isAWhatsAppChannel" @state-recorder-progress-changed="onStateProgressRecorderChanged" @state-recorder-changed="onStateRecorderChanged" @finish-record="onFinishRecorder" @@ -502,7 +501,10 @@ export default { return `draft-${this.conversationIdByRoute}-${this.replyType}`; }, audioRecordFormat() { - if (this.isAPIInbox || this.isATelegramChannel) { + if (this.isAWhatsAppChannel || this.isATelegramChannel) { + return AUDIO_FORMATS.MP3; + } + if (this.isAPIInbox) { return AUDIO_FORMATS.OGG; } return AUDIO_FORMATS.WAV; @@ -1250,6 +1252,7 @@ export default { } } } + .send-button { @apply mb-0; } @@ -1274,6 +1277,7 @@ export default { .emoji-dialog--rtl { @apply left-[unset] -right-80; + &::before { transform: rotate(90deg); filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08)); diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js index b3ef11e1b..9451f83d3 100644 --- a/app/javascript/shared/constants/messages.js +++ b/app/javascript/shared/constants/messages.js @@ -98,6 +98,7 @@ export const CSAT_RATINGS = [ export const AUDIO_FORMATS = { WEBM: 'audio/webm', OGG: 'audio/ogg', + MP3: 'audio/mp3', WAV: 'audio/wav', }; From 565747357321bf6b1a9156f4ace458b0071d602d Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 16 May 2024 09:16:02 +0530 Subject: [PATCH 14/21] fix: Dashboard phone number input country `undefined` in `onSelectCountry` (#9473) # Pull Request Template ## Description This PR will fix this sentry [issue](https://chatwoot-p3.sentry.io/issues/5291039795/) **Issue** The root cause of this issue is the usage of `keyboardEventListenerMixins`. The key events are always active when the edit conversation modal is active, even if the country dropdown is not visible. So, if we press the enter key, this error will be thrown into the console. **Solution** Remove the use of `keyboardEventListenerMixins` and handle it directly in the Vue native key events. Also, always check if the dropdown is active. **Other changes** 1. Remove the `mouseup` event lister and use the click away directive. 2. Use inline Tailwind css Fixes https://linear.app/chatwoot/issue/CW-3282/phonenumberinput-country-undefined-in-onselectcountry ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? **Steps** 1. Open a conversation. 3. And click the edit contact button 4. And click the enter key 5. Now you can see the error in the console ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../components/widgets/forms/PhoneInput.vue | 176 ++++++------------ 1 file changed, 62 insertions(+), 114 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/forms/PhoneInput.vue b/app/javascript/dashboard/components/widgets/forms/PhoneInput.vue index 9b77faafe..6b09acbac 100644 --- a/app/javascript/dashboard/components/widgets/forms/PhoneInput.vue +++ b/app/javascript/dashboard/components/widgets/forms/PhoneInput.vue @@ -1,9 +1,16 @@