+
<% if !@is_plain_layout_enabled %>
<%= render "public/api/v1/portals/header", portal: @portal %>
<% end %>
@@ -42,15 +70,53 @@ By default, it renders:
+
diff --git a/app/views/super_admin/application/_nav_item.html.erb b/app/views/super_admin/application/_nav_item.html.erb
new file mode 100644
index 000000000..a6366d8b5
--- /dev/null
+++ b/app/views/super_admin/application/_nav_item.html.erb
@@ -0,0 +1,9 @@
+
-
- <%= link_to image_tag('/brand-assets/logo.svg', alt: 'Chatwoot Admin Dashboard'), super_admin_root_url %>
-
v<%= Chatwoot.config[:version] %>
+
+
diff --git a/app/views/super_admin/devise/sessions/new.html.erb b/app/views/super_admin/devise/sessions/new.html.erb
index b79cbe8b3..24672aee1 100644
--- a/app/views/super_admin/devise/sessions/new.html.erb
+++ b/app/views/super_admin/devise/sessions/new.html.erb
@@ -5,42 +5,45 @@
<%= javascript_pack_tag 'superadmin' %>
<%= stylesheet_pack_tag 'superadmin' %>
-
-
+
+
+ <%= link_to image_tag('/brand-assets/logo_thumbnail.svg', alt: 'Chatwoot Admin Dashboard', class: 'h-10'), super_admin_root_url %>
+
+
+
+
+ Chatwoot <%= Chatwoot.config[:version] %>
+ Super Admin Console
+ -
+ <%= render partial: "nav_item", locals: { icon: 'icon-grid-line', url: super_admin_root_url, label: 'Dashboard' } %>
+ <% Administrate::Namespace.new(namespace).resources.each do |resource| %>
+ <% next if ["account_users", "access_tokens", "installation_configs", "dashboard", "devise/sessions", "app_configs", "instance_statuses", "responses", "response_sources", "response_documents" , "settings"].include? resource.resource %>
+ <%= render partial: "nav_item", locals: {
+ icon: sidebar_icons[resource.resource.to_sym],
+ url: resource_index_route(resource),
+ label: display_resource_name(resource),
+ }
+ %>
+ <% end %>
+
+ <% if InstallationConfig.find_by(name: 'DEPLOYMENT_ENV')&.value == 'cloud' || Rails.env.development? %> + <%= render partial: "nav_item", locals: { icon: 'icon-folder-3-line', url: super_admin_response_sources_url, label: 'Sources' } %> + <%= render partial: "nav_item", locals: { icon: 'icon-draft-line', url: super_admin_response_documents_url, label: 'Documents' } %> + <%= render partial: "nav_item", locals: { icon: 'icon-reply-line', url: super_admin_responses_url, label: 'Responses' } %> + <% end %> +
+
-
- -
+ <% if ChatwootApp.enterprise? %>
+ <%= render partial: "nav_item", locals: { icon: 'icon-settings-2-line', url: super_admin_settings_url, label: 'Settings' } %>
+ <% end %>
+ <%= render partial: "nav_item", locals: { icon: 'icon-mist-fill', url: sidekiq_web_url, label: 'Sidekiq Dashboard' } %>
+ <%= render partial: "nav_item", locals: { icon: 'icon-health-book-line', url: super_admin_instance_status_url, label: 'Instance Health' } %>
+ <%= render partial: "nav_item", locals: { icon: 'icon-dashboard-line', url: '/', label: 'Agent Dashboard' } %>
+ <%= render partial: "nav_item", locals: { icon: 'icon-logout-circle-r-line', url: super_admin_logout_url, label: 'Logout' } %>
+
-
-
- - - <%= link_to "Dashboard", super_admin_root_url %> - - -
- - - <%= link_to "App Config", super_admin_app_config_url %> - - - <% Administrate::Namespace.new(namespace).resources.each do |resource| %> - <% next if ["account_users", "dashboard", "devise/sessions", "app_configs", "instance_statuses", "responses", "response_sources", "response_documents" ].include? resource.resource %> -
- - - <%= link_to( - display_resource_name(resource), - resource_index_route(resource) - ) if existing_action? resource, :index %> - - <% end %> - - <% if InstallationConfig.find_by(name: 'DEPLOYMENT_ENV')&.value == 'cloud' || Rails.env.development? %> -
- - - <%= link_to "Sources", super_admin_response_sources_url %> - - -
- - - <%= link_to "Documents", super_admin_response_documents_url %> - - -
- - - <%= link_to "Responses", super_admin_responses_url %> - -
- - - <%= link_to "Instance Health", super_admin_instance_status_url %> - - -
- - - <%= link_to "Sidekiq", sidekiq_web_url, { target: "_blank" } %> - -
-
- <% end %> - -
-
-
-
- - - <%= link_to "Logout", super_admin_logout_url %> - -
- - - <%= link_to "Agent Dashboard", '/' %> - -
-
-
-
+
+
+
+
+
+
+
+
+
Howdy, admin 👋
-
-
-
- <%= form_for(resource, as: resource_name, url: '/super_admin/sign_in', html: { class: 'login-box column align-self-top'}) do |f| %>
-
- <% if flash[:error].present? %>
- <%= flash[:error] %>
- <% end %>
-
+
+
diff --git a/app/views/super_admin/instance_statuses/show.html.erb b/app/views/super_admin/instance_statuses/show.html.erb
index d0d23f2a1..5474bff6a 100644
--- a/app/views/super_admin/instance_statuses/show.html.erb
+++ b/app/views/super_admin/instance_statuses/show.html.erb
@@ -1,5 +1,5 @@
<% content_for(:title) do %>
- Instance Health
+ Instance Status
<% end %>
@@ -15,7 +15,7 @@
<% @metrics.each do |key,value| %>
<%= key %>
- <%= value %>
+ <%= value %>
<% end %>
diff --git a/app/views/super_admin/settings/show.html.erb b/app/views/super_admin/settings/show.html.erb
new file mode 100644
index 000000000..45dd46c47
--- /dev/null
+++ b/app/views/super_admin/settings/show.html.erb
@@ -0,0 +1,109 @@
+<% content_for(:title) do %>
+ Settings
+<% end %>
+
+
+
+
+
+
+ <%= content_for(:title) %>
+
+ Update your instance settings, access billing portal
+
+
+
+
+
+
+ Current plan
+
+
+ Refresh
+
+
+ <%= SuperAdmin::FeaturesHelper.plan_details.html_safe %>
+
+ Installation Identifier
+ <%= ChatwootHub.installation_identifier %>
+
+
+
+
+ Current plan
+ <%= SuperAdmin::FeaturesHelper.plan_details.html_safe %>
+
+
+
+
+
+
+ <% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %>
+
+
+ You have <%= User.count %> agents. Please add more licenses.
+
+
+ <% end %>
+
+
+
+
+ Need help?
+ Do you face any issues? We are here to help.
+
+
+
+
+ <% if ChatwootHub.pricing_plan !='community' %>
+
+ <% end %>
+
+
+ Features
+
+
+ <% SuperAdmin::FeaturesHelper.available_features.each do |feature, attrs| %>
+
+
+
+
+
+
+
+ <% if !attrs[:enabled] %>
+
+ <% end %>
+
+
+ <%= attrs[:name] %>
+ <% if attrs[:enterprise] %>
+ EE
+ <% end %>
+ <% if attrs[:config_key].present? && attrs[:enabled] %>
+
+
+
+ <% end %>
+
+
+ <%= attrs[:description] %>
+
+ <% end %>
+
+
+
diff --git a/config/app.yml b/config/app.yml
index 93e0bcbed..1e8d30423 100644
--- a/config/app.yml
+++ b/config/app.yml
@@ -1,5 +1,5 @@
shared: &shared
- version: '3.3.1'
+ version: '3.4.0'
development:
<<: *shared
diff --git a/config/initializers/facebook_messenger.rb b/config/initializers/facebook_messenger.rb
index f9f4fae88..354687cbd 100644
--- a/config/initializers/facebook_messenger.rb
+++ b/config/initializers/facebook_messenger.rb
@@ -29,14 +29,13 @@ Rails.application.reloader.to_prepare do
end
Facebook::Messenger::Bot.on :delivery do |delivery|
- # delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
- # delivery.sender # => { 'id' => '1008372609250235' }
- # delivery.recipient # => { 'id' => '2015573629214912' }
- # delivery.at # => 2016-04-22 21:30:36 +0200
- # delivery.seq # => 37
- updater = Integrations::Facebook::DeliveryStatus.new(delivery)
- updater.perform
- Rails.logger.info "Human was online at #{delivery.at}"
+ Rails.logger.info "Recieved delivery status #{delivery.to_json}"
+ Webhooks::FacebookDeliveryJob.perform_later(delivery.to_json)
+ end
+
+ Facebook::Messenger::Bot.on :read do |read|
+ Rails.logger.info "Recieved read status #{read.to_json}"
+ Webhooks::FacebookDeliveryJob.perform_later(read.to_json)
end
Facebook::Messenger::Bot.on :message_echo do |message|
diff --git a/config/installation_config.yml b/config/installation_config.yml
index c1bbb299b..a53bb1ed5 100644
--- a/config/installation_config.yml
+++ b/config/installation_config.yml
@@ -74,3 +74,13 @@
value:
- name: LOGO_DARK
value: '/brand-assets/logo_dark.svg'
+- name: INSTALLATION_PRICING_PLAN
+ value: 'community'
+- name: INSTALLATION_PRICING_PLAN_QUANTITY
+ value: 0
+- name: CHATWOOT_SUPPORT_WEBSITE_TOKEN
+ value:
+- name: CHATWOOT_SUPPORT_SCRIPT_URL
+ value:
+- name: CHATWOOT_SUPPORT_IDENTIFIER_HASH
+ value:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 14175b263..e6629ae8d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -215,6 +215,11 @@ en:
view_all_articles: View all
article: article
articles: articles
+ author: author
+ authors: authors
+ other: other
+ others: others
+ by: By
no_articles: There are no articles here
footer:
made_with: Made with
diff --git a/config/routes.rb b/config/routes.rb
index f9346eb0f..ec838c107 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -45,7 +45,9 @@ Rails.application.routes.draw do
end
resource :bulk_actions, only: [:create]
resources :agents, only: [:index, :create, :update, :destroy]
- resources :agent_bots, only: [:index, :create, :show, :update, :destroy]
+ resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
+ delete :avatar, on: :member
+ end
resources :contact_inboxes, only: [] do
collection do
post :filter
@@ -84,6 +86,7 @@ Rails.application.routes.draw do
resources :messages, only: [:index, :create, :destroy] do
member do
post :translate
+ post :retry
end
end
resources :assignments, only: [:create]
@@ -166,11 +169,14 @@ Rails.application.routes.draw do
end
end
- resources :notifications, only: [:index, :update] do
+ resources :notifications, only: [:index, :update, :destroy] do
collection do
post :read_all
get :unread_count
end
+ member do
+ post :snooze
+ end
end
resource :notification_settings, only: [:show, :update]
@@ -325,7 +331,9 @@ Rails.application.routes.draw do
get :login
end
end
- resources :agent_bots, only: [:index, :create, :show, :update, :destroy]
+ resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
+ delete :avatar, on: :member
+ end
resources :accounts, only: [:create, :show, :update, :destroy] do
resources :account_users, only: [:index, :create] do
collection do
@@ -441,6 +449,10 @@ Rails.application.routes.draw do
resources :platform_apps, only: [:index, :new, :create, :show, :edit, :update]
resource :instance_status, only: [:show]
+ resource :settings, only: [:show] do
+ get :refresh, on: :collection
+ end
+
# resources that doesn't appear in primary navigation in super admin
resources :account_users, only: [:new, :create, :destroy]
end
diff --git a/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb b/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb
new file mode 100644
index 000000000..14a52a9a7
--- /dev/null
+++ b/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb
@@ -0,0 +1,5 @@
+class AddSnoozedUntilToNotifications < ActiveRecord::Migration[7.0]
+ def change
+ add_column :notifications, :snoozed_until, :datetime
+ end
+end
diff --git a/db/migrate/20231201014644_remove_notifications_with_message_primary_actor.rb b/db/migrate/20231201014644_remove_notifications_with_message_primary_actor.rb
new file mode 100644
index 000000000..d0698a8cb
--- /dev/null
+++ b/db/migrate/20231201014644_remove_notifications_with_message_primary_actor.rb
@@ -0,0 +1,5 @@
+class RemoveNotificationsWithMessagePrimaryActor < ActiveRecord::Migration[7.0]
+ def change
+ Migration::RemoveMessageNotifications.perform_later
+ end
+end
diff --git a/db/migrate/20231211010807_add_cached_labels_list.rb b/db/migrate/20231211010807_add_cached_labels_list.rb
new file mode 100644
index 000000000..026bcf7d1
--- /dev/null
+++ b/db/migrate/20231211010807_add_cached_labels_list.rb
@@ -0,0 +1,7 @@
+class AddCachedLabelsList < ActiveRecord::Migration[7.0]
+ def change
+ add_column :conversations, :cached_label_list, :string
+ Conversation.reset_column_information
+ ActsAsTaggableOn::Taggable::Cache.included(Conversation)
+ end
+end
diff --git a/db/migrate/20231219000743_re_run_cache_label_job.rb b/db/migrate/20231219000743_re_run_cache_label_job.rb
new file mode 100644
index 000000000..ebe553f1c
--- /dev/null
+++ b/db/migrate/20231219000743_re_run_cache_label_job.rb
@@ -0,0 +1,16 @@
+class ReRunCacheLabelJob < ActiveRecord::Migration[7.0]
+ def change
+ update_exisiting_conversations
+ end
+
+ private
+
+ def update_exisiting_conversations
+ # Run label migrations on the accounts that are not suspended
+ ::Account.active.find_in_batches do |account_batch|
+ account_batch.each do |account|
+ Migration::ConversationCacheLabelJob.perform_later(account)
+ end
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 081030b11..373d81918 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: 2023_11_14_111614) do
+ActiveRecord::Schema[7.0].define(version: 2023_12_19_000743) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -453,6 +453,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_14_111614) do
t.integer "priority"
t.bigint "sla_policy_id"
t.datetime "waiting_since"
+ t.string "cached_label_list"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id"
t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx"
@@ -729,6 +730,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_14_111614) do
t.datetime "read_at", precision: nil
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.datetime "snoozed_until"
t.index ["account_id"], name: "index_notifications_on_account_id"
t.index ["primary_actor_type", "primary_actor_id"], name: "uniq_primary_actor_per_account_notifications"
t.index ["secondary_actor_type", "secondary_actor_id"], name: "uniq_secondary_actor_per_account_notifications"
diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh
index 5e2d3c250..c5060f869 100644
--- a/deployment/setup_20.04.sh
+++ b/deployment/setup_20.04.sh
@@ -2,7 +2,7 @@
# Description: Install and manage a Chatwoot installation.
# OS: Ubuntu 20.04 LTS
-# Script Version: 2.6.0
+# Script Version: 2.7.0
# Run this script as root
set -eu -o errexit -o pipefail -o noclobber -o nounset
@@ -19,7 +19,7 @@ fi
# option --output/-o requires 1 argument
LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,webserver,version
OPTIONS=cdhiI:l:rsuwv
-CWCTL_VERSION="2.6.0"
+CWCTL_VERSION="2.7.0"
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
CHATWOOT_HUB_URL="https://hub.2.chatwoot.com/events"
@@ -173,15 +173,19 @@ EOF
function install_dependencies() {
apt update && apt upgrade -y
apt install -y curl
- curl -sL https://deb.nodesource.com/setup_20.x | bash -
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
+ mkdir -p /etc/apt/keyrings
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
+ NODE_MAJOR=20
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
+
apt update
apt install -y \
- git software-properties-common imagemagick libpq-dev \
+ git software-properties-common ca-certificates imagemagick libpq-dev \
libxml2-dev libxslt1-dev file g++ gcc autoconf build-essential \
libssl-dev libyaml-dev libreadline-dev gnupg2 \
postgresql-client redis-tools \
@@ -754,10 +758,39 @@ function upgrade_redis() {
apt install libvips -y
}
+
+##############################################################################
+# Update nodejs to v20+
+# Globals:
+# None
+# Arguments:
+# None
+# Outputs:
+# None
+##############################################################################
function upgrade_node() {
- echo "Upgrading nodejs version to v20.x"
- curl -sL https://deb.nodesource.com/setup_20.x | sudo bash -
- apt install -y nodejs
+ echo "Checking Node.js version..."
+
+ # Get current Node.js version
+ current_version=$(node --version | cut -c 2-)
+
+ # Parse major version number
+ major_version=$(echo "$current_version" | cut -d. -f1)
+
+ if [ "$major_version" -ge 20 ]; then
+ echo "Node.js is already version $current_version (>= 20.x). Skipping Node.js upgrade."
+ return
+ fi
+
+ echo "Upgrading Node.js version to v20.x"
+ mkdir -p /etc/apt/keyrings
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
+ NODE_MAJOR=20
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
+
+ apt update
+ apt install nodejs -y
+
}
##############################################################################
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 676b5bf74..6503530b7 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -104,7 +104,7 @@ RUN apk update && apk add --no-cache \
&& gem install bundler
RUN if [ "$RAILS_ENV" != "production" ]; then \
- apk add --no-cache nodejs yarn; \
+ apk add --no-cache nodejs-current yarn; \
fi
COPY --from=pre-builder /gems/ /gems/
diff --git a/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb b/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb
new file mode 100644
index 000000000..c8a42e391
--- /dev/null
+++ b/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb
@@ -0,0 +1,23 @@
+module Enterprise::SuperAdmin::AppConfigsController
+ private
+
+ def allowed_configs
+ return super if ChatwootHub.pricing_plan == 'community'
+
+ case @config
+ when 'custom_branding'
+ @allowed_configs = %w[
+ LOGO_THUMBNAIL
+ LOGO
+ BRAND_NAME
+ INSTALLATION_NAME
+ BRAND_URL
+ WIDGET_BRAND_URL
+ TERMS_URL
+ PRIVACY_URL
+ ]
+ else
+ super
+ end
+ end
+end
diff --git a/enterprise/app/helpers/super_admin/features.yml b/enterprise/app/helpers/super_admin/features.yml
new file mode 100644
index 000000000..41e5af426
--- /dev/null
+++ b/enterprise/app/helpers/super_admin/features.yml
@@ -0,0 +1,66 @@
+custom_branding:
+ name: 'Custom Branding'
+ description: 'Apply your own branding to this installation.'
+ enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
+ icon: 'icon-paint-brush-line'
+ config_key: 'custom_branding'
+ enterprise: true
+agent_capacity:
+ name: 'Agent Capacity'
+ description: 'Set limits to auto-assigning conversations to your agents.'
+ enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
+ icon: 'icon-hourglass-line'
+ enterprise: true
+audit_logs:
+ name: 'Audit Logs'
+ description: 'Track and trace account activities with ease with detailed audit logs.'
+ enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
+ icon: 'icon-menu-search-line'
+ enterprise: true
+disable_branding:
+ name: 'Disable Branding'
+ description: 'Disable branding on live-chat widget and external emails.'
+ enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
+ icon: 'icon-sailbot-fill'
+ enterprise: true
+live_chat:
+ name: 'Live Chat'
+ description: 'Improve your customer experience using a live chat on your website.'
+ enabled: true
+ icon: 'icon-chat-smile-3-line'
+email:
+ name: 'Email'
+ description: 'Manage your email customer interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-mail-send-fill'
+messenger:
+ name: 'Messenger'
+ description: 'Stay connected with your customers on Facebook & Instagram.'
+ enabled: true
+ icon: 'icon-messenger-line'
+ config_key: 'facebook'
+whatsapp:
+ name: 'WhatsApp'
+ description: 'Manage your WhatsApp business interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-whatsapp-line'
+telegram:
+ name: 'Telegram'
+ description: 'Manage your Telegram customer interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-telegram-line'
+line:
+ name: 'Line'
+ description: 'Manage your Line customer interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-line-line'
+sms:
+ name: 'SMS'
+ description: 'Manage your SMS customer interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-message-line'
+help_center:
+ name: 'Help Center'
+ description: 'Allow agents to create help center articles and publish them in a portal.'
+ enabled: true
+ icon: 'icon-book-2-line'
diff --git a/enterprise/app/helpers/super_admin/features_helper.rb b/enterprise/app/helpers/super_admin/features_helper.rb
new file mode 100644
index 000000000..2fbcd1715
--- /dev/null
+++ b/enterprise/app/helpers/super_admin/features_helper.rb
@@ -0,0 +1,16 @@
+module SuperAdmin::FeaturesHelper
+ def self.available_features
+ YAML.load(ERB.new(Rails.root.join('enterprise/app/helpers/super_admin/features.yml').read).result).with_indifferent_access
+ end
+
+ def self.plan_details
+ plan = ChatwootHub.pricing_plan
+ quantity = ChatwootHub.pricing_plan_quantity
+
+ if plan == 'premium'
+ "You are currently on the #{plan} plan with #{quantity} agents."
+ else
+ "You are currently on the #{plan} edition plan."
+ end
+ end
+end
diff --git a/enterprise/app/models/enterprise/message.rb b/enterprise/app/models/enterprise/message.rb
index 4f72d8c19..11c7044e5 100644
--- a/enterprise/app/models/enterprise/message.rb
+++ b/enterprise/app/models/enterprise/message.rb
@@ -1,5 +1,5 @@
module Enterprise::Message
def update_message_sentiments
- ::Enterprise::SentimentAnalysisJob.perform_later(self) if ENV.fetch('SENTIMENT_FILE_PATH', nil)
+ ::Enterprise::SentimentAnalysisJob.perform_later(self) if ENV.fetch('SENTIMENT_FILE_PATH', nil).present?
end
end
diff --git a/lib/chatwoot_hub.rb b/lib/chatwoot_hub.rb
index 68bae8267..6d28b10fa 100644
--- a/lib/chatwoot_hub.rb
+++ b/lib/chatwoot_hub.rb
@@ -4,6 +4,7 @@ class ChatwootHub
REGISTRATION_URL = "#{BASE_URL}/instances".freeze
PUSH_NOTIFICATION_URL = "#{BASE_URL}/send_push".freeze
EVENTS_URL = "#{BASE_URL}/events".freeze
+ BILLING_URL = "#{BASE_URL}/billing".freeze
def self.installation_identifier
identifier = InstallationConfig.find_by(name: 'INSTALLATION_IDENTIFIER')&.value
@@ -11,6 +12,26 @@ class ChatwootHub
identifier
end
+ def self.billing_url
+ "#{BILLING_URL}?installation_identifier=#{installation_identifier}"
+ end
+
+ def self.pricing_plan
+ InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN')&.value || 'community'
+ end
+
+ def self.pricing_plan_quantity
+ InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN_QUANTITY')&.value || 0
+ end
+
+ def self.support_config
+ {
+ support_website_token: InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN')&.value,
+ support_script_url: InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_SCRIPT_URL')&.value,
+ support_identifier_hash: InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH')&.value
+ }
+ end
+
def self.instance_config
{
installation_identifier: installation_identifier,
@@ -33,18 +54,18 @@ class ChatwootHub
}
end
- def self.latest_version
+ def self.sync_with_hub
begin
info = instance_config
info = info.merge(instance_metrics) unless ENV['DISABLE_TELEMETRY']
response = RestClient.post(PING_URL, info.to_json, { content_type: :json, accept: :json })
- version = JSON.parse(response)['version']
+ parsed_response = JSON.parse(response)
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
- version
+ parsed_response
end
def self.register_instance(company_name, owner_name, owner_email)
diff --git a/lib/events/types.rb b/lib/events/types.rb
index 2693f5216..6e34fc358 100644
--- a/lib/events/types.rb
+++ b/lib/events/types.rb
@@ -48,6 +48,7 @@ module Events::Types
# notification events
NOTIFICATION_CREATED = 'notification.created'
+ NOTIFICATION_DELETED = 'notification.deleted'
# agent events
AGENT_ADDED = 'agent.added'
diff --git a/lib/integrations/facebook/delivery_status.rb b/lib/integrations/facebook/delivery_status.rb
index d38ece092..1d6257bba 100644
--- a/lib/integrations/facebook/delivery_status.rb
+++ b/lib/integrations/facebook/delivery_status.rb
@@ -1,32 +1,37 @@
# frozen_string_literal: true
class Integrations::Facebook::DeliveryStatus
- def initialize(params)
- @params = params
- end
+ pattr_initialize [:params!]
def perform
- update_message_status
+ return if facebook_channel.blank?
+ return unless conversation
+
+ process_delivery_status if params.delivery_watermark
+ process_read_status if params.read_watermark
end
private
- def sender_id
- @params.sender['id']
+ def process_delivery_status
+ timestamp = Time.zone.at(params.delivery_watermark.to_i).to_datetime.utc
+ ::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, timestamp, :delivered)
+ end
+
+ def process_read_status
+ timestamp = Time.zone.at(params.read_watermark.to_i).to_datetime.utc
+ ::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, timestamp, :read)
end
def contact
- ::ContactInbox.find_by(source_id: sender_id)&.contact
+ ::ContactInbox.find_by(source_id: params.sender_id)&.contact
end
def conversation
@conversation ||= ::Conversation.find_by(contact_id: contact.id) if contact.present?
end
- def update_message_status
- return unless conversation
-
- conversation.contact_last_seen_at = @params.at
- conversation.save!
+ def facebook_channel
+ @facebook_channel ||= Channel::FacebookPage.find_by(page_id: params.recipient_id)
end
end
diff --git a/lib/integrations/facebook/message_parser.rb b/lib/integrations/facebook/message_parser.rb
index 123b521cb..275a46c39 100644
--- a/lib/integrations/facebook/message_parser.rb
+++ b/lib/integrations/facebook/message_parser.rb
@@ -34,6 +34,22 @@ class Integrations::Facebook::MessageParser
@messaging.dig('message', 'mid')
end
+ def delivery
+ @messaging['delivery']
+ end
+
+ def read
+ @messaging['read']
+ end
+
+ def read_watermark
+ read&.dig('watermark')
+ end
+
+ def delivery_watermark
+ delivery&.dig('watermark')
+ end
+
def echo?
@messaging.dig('message', 'is_echo')
end
diff --git a/package.json b/package.json
index f2efcdc3c..602d1e383 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
- "version": "3.3.1",
+ "version": "3.4.0",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
@@ -22,7 +22,7 @@
"size-limit": [
{
"path": "public/packs/js/widget-*.js",
- "limit": "275 KB"
+ "limit": "280 KB"
},
{
"path": "public/packs/js/sdk.js",
@@ -46,10 +46,11 @@
"@tailwindcss/typography": "^0.5.9",
"activestorage": "^5.2.6",
"autoprefixer": "^10.4.14",
- "axios": "^0.21.2",
+ "axios": "^1.6.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-vue-jsx": "^3.7.0",
"chart.js": "~2.9.4",
+ "color2k": "^2.0.2",
"company-email-validator": "^1.0.8",
"core-js": "3.11.0",
"date-fns": "2.21.1",
@@ -90,6 +91,7 @@
"vue-router": "~3.5.2",
"vue-template-compiler": "^2.7.0",
"vue-upload-component": "2.8.22",
+ "vue-virtual-scroll-list": "^2.3.5",
"vue2-datepicker": "^3.9.1",
"vuedraggable": "^2.24.3",
"vuelidate": "0.7.7",
@@ -167,4 +169,4 @@
"scss-lint"
]
}
-}
+}
\ No newline at end of file
diff --git a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb
index 70fcf9f42..918552406 100644
--- a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb
@@ -141,6 +141,28 @@ RSpec.describe 'Agent Bot API', type: :request do
expect(agent_bot.reload.name).not_to eq('test_updated')
expect(response.body).not_to include(global_bot.access_token.token)
end
+
+ it 'updates avatar' do
+ # no avatar before upload
+ expect(agent_bot.avatar.attached?).to be(false)
+ file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
+ patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
+ headers: admin.create_new_auth_token,
+ params: valid_params.merge(avatar: file)
+
+ expect(response).to have_http_status(:success)
+ agent_bot.reload
+ expect(agent_bot.avatar.attached?).to be(true)
+ end
+
+ it 'updated avatar with avatar_url' do
+ patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
+ headers: admin.create_new_auth_token,
+ params: valid_params.merge(avatar_url: 'http://example.com/avatar.png'),
+ as: :json
+ expect(response).to have_http_status(:success)
+ expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(agent_bot, 'http://example.com/avatar.png')
+ end
end
end
@@ -183,4 +205,29 @@ RSpec.describe 'Agent Bot API', type: :request do
end
end
end
+
+ describe 'DELETE /api/v1/accounts/{account.id}/agent_bots/:id/avatar' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ before do
+ agent_bot.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
+ end
+
+ it 'delete agent_bot avatar' do
+ delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/avatar",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect { agent_bot.avatar.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(response).to have_http_status(:success)
+ end
+ end
+ end
end
diff --git a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
index dc9de8c35..8f7a5fea0 100644
--- a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
@@ -234,4 +234,49 @@ RSpec.describe 'Conversation Messages API', type: :request do
end
end
end
+
+ describe 'POST /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id/retry' do
+ let(:message) { create(:message, account: account, status: :failed, content_attributes: { external_error: 'error' }) }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/#{message.id}/retry"
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user with access to conversation' do
+ let(:agent) { create(:user, account: account, role: :agent) }
+
+ before do
+ create(:inbox_member, inbox: message.conversation.inbox, user: agent)
+ end
+
+ it 'retries the message' do
+ post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/#{message.id}/retry",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(message.reload.status).to eq('sent')
+ expect(message.reload.content_attributes['external_error']).to be_nil
+ end
+ end
+
+ context 'when the message id is invalid' do
+ let(:agent) { create(:user, account: account, role: :agent) }
+
+ before do
+ create(:inbox_member, inbox: message.conversation.inbox, user: agent)
+ end
+
+ it 'returns not found error' do
+ post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/99999/retry",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+ end
end
diff --git a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
index 99baf1b25..cadc212d4 100644
--- a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
@@ -127,4 +127,58 @@ RSpec.describe 'Notifications API', type: :request do
end
end
end
+
+ describe 'DELETE /api/v1/accounts/{account.id}/notifications/:id' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let!(:notification) { create(:notification, account: account, user: admin) }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ delete "/api/v1/accounts/#{account.id}/notifications/#{notification.id}"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+
+ it 'deletes the notification' do
+ delete "/api/v1/accounts/#{account.id}/notifications/#{notification.id}",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(Notification.count).to eq(0)
+ end
+ end
+ end
+
+ describe 'PATCH /api/v1/accounts/{account.id}/notifications/:id/snooze' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let!(:notification) { create(:notification, account: account, user: admin) }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/snooze",
+ params: { snoozed_until: DateTime.now.utc + 1.day }
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+
+ it 'updates the notification snoozed until' do
+ post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/snooze",
+ headers: admin.create_new_auth_token,
+ params: { snoozed_until: DateTime.now.utc + 1.day },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(notification.reload.snoozed_until).not_to eq('')
+ end
+ end
+ end
end
diff --git a/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb b/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb
index 030d694ff..ae3ef61e5 100644
--- a/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb
+++ b/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb
@@ -140,6 +140,26 @@ RSpec.describe 'Platform Agent Bot API', type: :request do
data = response.parsed_body
expect(data['name']).to eq('test123')
end
+
+ it 'updates avatar' do
+ # no avatar before upload
+ create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
+ expect(agent_bot.avatar.attached?).to be(false)
+ file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
+ patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test123' }.merge(avatar: file),
+ headers: { api_access_token: platform_app.access_token.token }
+ expect(response).to have_http_status(:success)
+ agent_bot.reload
+ expect(agent_bot.avatar.attached?).to be(true)
+ end
+
+ it 'updated avatar with avatar_url' do
+ create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
+ patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test123' }.merge(avatar_url: 'http://example.com/avatar.png'),
+ headers: { api_access_token: platform_app.access_token.token }
+ expect(response).to have_http_status(:success)
+ expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(agent_bot, 'http://example.com/avatar.png')
+ end
end
end
@@ -169,4 +189,32 @@ RSpec.describe 'Platform Agent Bot API', type: :request do
end
end
end
+
+ describe 'DELETE /platform/api/v1/agent_bots/{agent_bot_id}/avatar' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ delete "/platform/api/v1/agent_bots/#{agent_bot.id}/avatar"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:platform_app) { create(:platform_app) }
+
+ before do
+ agent_bot.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
+ create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
+ end
+
+ it 'delete agent_bot avatar' do
+ delete "/platform/api/v1/agent_bots/#{agent_bot.id}/avatar",
+ headers: { api_access_token: platform_app.access_token.token },
+ as: :json
+
+ expect { agent_bot.avatar.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(response).to have_http_status(:success)
+ end
+ end
+ end
end
diff --git a/spec/controllers/super_admin/app_config_controller_spec.rb b/spec/controllers/super_admin/app_config_controller_spec.rb
index 3d6df46a8..338c3e611 100644
--- a/spec/controllers/super_admin/app_config_controller_spec.rb
+++ b/spec/controllers/super_admin/app_config_controller_spec.rb
@@ -34,13 +34,13 @@ RSpec.describe 'Super Admin Application Config API', type: :request do
context 'when it is an aunthenticated super admin' do
it 'shows the app_config page' do
sign_in(super_admin, scope: :super_admin)
- post '/super_admin/app_config', params: { app_config: { TESTKEY: 'TESTVALUE' } }
+ post '/super_admin/app_config', params: { app_config: { FB_APP_ID: 'FB_APP_ID' } }
expect(response).to have_http_status(:found)
- expect(response).to redirect_to(super_admin_app_config_path)
+ expect(response).to redirect_to(super_admin_settings_path)
- config = GlobalConfig.get('TESTKEY')
- expect(config['TESTKEY']).to eq('TESTVALUE')
+ config = GlobalConfig.get('FB_APP_ID')
+ expect(config['FB_APP_ID']).to eq('FB_APP_ID')
end
end
end
diff --git a/spec/controllers/super_admin/instance_statuses_controller_spec.rb b/spec/controllers/super_admin/instance_statuses_controller_spec.rb
index b2b1237f9..c00ea5984 100644
--- a/spec/controllers/super_admin/instance_statuses_controller_spec.rb
+++ b/spec/controllers/super_admin/instance_statuses_controller_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe 'Super Admin Instance health', type: :request do
+RSpec.describe 'Super Admin Instance status', type: :request do
let(:super_admin) { create(:super_admin) }
describe 'GET /super_admin/instance_status' do
diff --git a/spec/drops/contact_drop_spec.rb b/spec/drops/contact_drop_spec.rb
index e83ebb377..d00a0924d 100644
--- a/spec/drops/contact_drop_spec.rb
+++ b/spec/drops/contact_drop_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe ContactDrop do
subject(:contact_drop) { described_class.new(contact) }
- let!(:contact) { create(:contact) }
+ let!(:contact) { create(:contact, custom_attributes: { car_model: 'Tesla Model S', car_year: '2022' }) }
context 'when first name' do
it 'returns first name' do
@@ -38,4 +38,19 @@ describe ContactDrop do
expect(subject.last_name).to eq 'Doe'
end
end
+
+ context 'when accessing custom attributes' do
+ it 'returns the correct car model from custom attributes' do
+ expect(contact_drop.custom_attribute['car_model']).to eq 'Tesla Model S'
+ end
+
+ it 'returns the correct car year from custom attributes' do
+ expect(contact_drop.custom_attribute['car_year']).to eq '2022'
+ end
+
+ it 'returns empty hash when there are no custom attributes' do
+ contact.update!(custom_attributes: nil)
+ expect(contact_drop.custom_attribute).to eq({})
+ end
+ end
end
diff --git a/spec/factories/facebook_message/incoming_fb_text_message.rb b/spec/factories/facebook_message/incoming_fb_text_message.rb
index 83d516f09..dc75a912c 100644
--- a/spec/factories/facebook_message/incoming_fb_text_message.rb
+++ b/spec/factories/facebook_message/incoming_fb_text_message.rb
@@ -10,4 +10,24 @@ FactoryBot.define do
initialize_with { attributes }
end
+
+ factory :message_deliveries, class: Hash do
+ messaging do
+ { sender: { id: '3383290475046708' },
+ recipient: { id: '117172741761305' },
+ delivery: { watermark: '1648581633369' } }
+ end
+
+ initialize_with { attributes }
+ end
+
+ factory :message_reads, class: Hash do
+ messaging do
+ { sender: { id: '3383290475046708' },
+ recipient: { id: '117172741761305' },
+ read: { watermark: '1648581633369' } }
+ end
+
+ initialize_with { attributes }
+ end
end
diff --git a/spec/helpers/portal_helper_spec.rb b/spec/helpers/portal_helper_spec.rb
index 4611e4a6f..84ed4ba72 100644
--- a/spec/helpers/portal_helper_spec.rb
+++ b/spec/helpers/portal_helper_spec.rb
@@ -33,71 +33,175 @@ describe PortalHelper do
describe '#generate_portal_bg' do
context 'when theme is dark' do
it 'returns the correct background with dark grid image and color mix with black' do
- expected_bg = 'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #ff0000 20%, black)'
+ expected_bg = 'url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #ff0000 20%, black)'
expect(helper.generate_portal_bg('#ff0000', 'dark')).to eq(expected_bg)
end
end
context 'when theme is not dark' do
it 'returns the correct background with light grid image and color mix with white' do
- expected_bg = 'background: url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #ff0000 20%, white)'
+ expected_bg = 'url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #ff0000 20%, white)'
expect(helper.generate_portal_bg('#ff0000', 'light')).to eq(expected_bg)
end
end
context 'when provided with various colors' do
it 'adjusts the background appropriately for dark theme' do
- expected_bg = 'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #00ff00 20%, black)'
+ expected_bg = 'url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #00ff00 20%, black)'
expect(helper.generate_portal_bg('#00ff00', 'dark')).to eq(expected_bg)
end
it 'adjusts the background appropriately for light theme' do
- expected_bg = 'background: url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #0000ff 20%, white)'
+ expected_bg = 'url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #0000ff 20%, white)'
expect(helper.generate_portal_bg('#0000ff', 'light')).to eq(expected_bg)
end
end
end
- describe '#get_theme_names' do
- it 'returns the light theme name' do
- expect(helper.get_theme_names('light')).to eq(I18n.t('public_portal.header.appearance.light'))
- end
-
- it 'returns the dark theme name' do
- expect(helper.get_theme_names('dark')).to eq(I18n.t('public_portal.header.appearance.dark'))
- end
-
- it 'returns the system theme name for any other value' do
- expect(helper.get_theme_names('any_other_value')).to eq(I18n.t('public_portal.header.appearance.system'))
- end
- end
-
- describe '#get_theme_icon' do
- it 'returns the light theme icon' do
- expect(helper.get_theme_icon('light')).to eq('icons/sun')
- end
-
- it 'returns the dark theme icon' do
- expect(helper.get_theme_icon('dark')).to eq('icons/moon')
- end
-
- it 'returns the system theme icon for any other value' do
- expect(helper.get_theme_icon('any_other_value')).to eq('icons/monitor')
- end
- end
-
describe '#generate_gradient_to_bottom' do
context 'when theme is dark' do
- it 'returns the correct background gradient' do
- expected_gradient = 'background-image: linear-gradient(to bottom, transparent, #151718)'
- expect(helper.generate_gradient_to_bottom('dark')).to eq(expected_gradient)
+ it 'returns the correct gradient' do
+ expect(helper.generate_gradient_to_bottom('dark')).to eq(
+ 'linear-gradient(to bottom, transparent, #151718)'
+ )
end
end
context 'when theme is not dark' do
- it 'returns the correct background gradient' do
- expected_gradient = 'background-image: linear-gradient(to bottom, transparent, white)'
- expect(helper.generate_gradient_to_bottom('light')).to eq(expected_gradient)
+ it 'returns the correct gradient' do
+ expect(helper.generate_gradient_to_bottom('light')).to eq(
+ 'linear-gradient(to bottom, transparent, white)'
+ )
+ end
+ end
+
+ context 'when provided with various colors' do
+ it 'adjusts the gradient appropriately' do
+ expect(helper.generate_gradient_to_bottom('dark')).to eq(
+ 'linear-gradient(to bottom, transparent, #151718)'
+ )
+ expect(helper.generate_gradient_to_bottom('light')).to eq(
+ 'linear-gradient(to bottom, transparent, white)'
+ )
+ end
+ end
+ end
+
+ describe '#generate_portal_hover_color' do
+ context 'when theme is dark' do
+ it 'returns the correct color mix with #1B1B1B' do
+ expect(helper.generate_portal_hover_color('#ff0000', 'dark')).to eq(
+ 'color-mix(in srgb, #ff0000 5%, #1B1B1B)'
+ )
+ end
+ end
+
+ context 'when theme is not dark' do
+ it 'returns the correct color mix with #F9F9F9' do
+ expect(helper.generate_portal_hover_color('#ff0000', 'light')).to eq(
+ 'color-mix(in srgb, #ff0000 5%, #F9F9F9)'
+ )
+ end
+ end
+
+ context 'when provided with various colors' do
+ it 'adjusts the color mix appropriately' do
+ expect(helper.generate_portal_hover_color('#00ff00', 'dark')).to eq(
+ 'color-mix(in srgb, #00ff00 5%, #1B1B1B)'
+ )
+ expect(helper.generate_portal_hover_color('#0000ff', 'light')).to eq(
+ 'color-mix(in srgb, #0000ff 5%, #F9F9F9)'
+ )
+ end
+ end
+ end
+
+ describe '#theme_query_string' do
+ context 'when theme is present and not system' do
+ it 'returns the correct query string' do
+ expect(helper.theme_query_string('dark')).to eq('?theme=dark')
+ end
+ end
+
+ context 'when theme is not present' do
+ it 'returns the correct query string' do
+ expect(helper.theme_query_string(nil)).to eq('')
+ end
+ end
+
+ context 'when theme is system' do
+ it 'returns the correct query string' do
+ expect(helper.theme_query_string('system')).to eq('')
+ end
+ end
+ end
+
+ describe '#generate_home_link' do
+ context 'when theme is not present' do
+ it 'returns the correct link' do
+ expect(helper.generate_home_link('portal_slug', 'en', nil, true)).to eq(
+ '/hc/portal_slug/en'
+ )
+ end
+ end
+
+ context 'when theme is present and plain layout is enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_home_link('portal_slug', 'en', 'dark', true)).to eq(
+ '/hc/portal_slug/en?theme=dark'
+ )
+ end
+ end
+
+ context 'when plain layout is not enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_home_link('portal_slug', 'en', 'dark', false)).to eq(
+ '/hc/portal_slug/en'
+ )
+ end
+ end
+ end
+
+ describe '#generate_category_link' do
+ context 'when theme is not present' do
+ it 'returns the correct link' do
+ expect(helper.generate_category_link(
+ portal_slug: 'portal_slug',
+ category_locale: 'en',
+ category_slug: 'category_slug',
+ theme: nil,
+ is_plain_layout_enabled: true
+ )).to eq(
+ '/hc/portal_slug/en/categories/category_slug'
+ )
+ end
+ end
+
+ context 'when theme is present and plain layout is enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_category_link(
+ portal_slug: 'portal_slug',
+ category_locale: 'en',
+ category_slug: 'category_slug',
+ theme: 'dark',
+ is_plain_layout_enabled: true
+ )).to eq(
+ '/hc/portal_slug/en/categories/category_slug?theme=dark'
+ )
+ end
+ end
+
+ context 'when plain layout is not enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_category_link(
+ portal_slug: 'portal_slug',
+ category_locale: 'en',
+ category_slug: 'category_slug',
+ theme: 'dark',
+ is_plain_layout_enabled: false
+ )).to eq(
+ '/hc/portal_slug/en/categories/category_slug'
+ )
end
end
end
@@ -105,19 +209,27 @@ describe PortalHelper do
describe '#generate_article_link' do
context 'when theme is not present' do
it 'returns the correct link' do
- expect(helper.generate_article_link('portal_slug', 'article_slug', nil)).to eq(
+ expect(helper.generate_article_link('portal_slug', 'article_slug', nil, true)).to eq(
'/hc/portal_slug/articles/article_slug'
)
end
end
- context 'when theme is present' do
+ context 'when theme is present and plain layout is enabled' do
it 'returns the correct link' do
- expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark')).to eq(
+ expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark', true)).to eq(
'/hc/portal_slug/articles/article_slug?theme=dark'
)
end
end
+
+ context 'when plain layout is not enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark', false)).to eq(
+ '/hc/portal_slug/articles/article_slug'
+ )
+ end
+ end
end
describe '#render_category_content' do
@@ -134,4 +246,16 @@ describe PortalHelper do
expect(helper.render_category_content(markdown_content)).to eq(plain_text_content)
end
end
+
+ describe '#thumbnail_bg_color' do
+ it 'returns the correct color based on username length' do
+ expect(helper.thumbnail_bg_color('')).to be_in(['#6D95BA', '#A4C3C3', '#E19191'])
+ expect(helper.thumbnail_bg_color('Joe')).to eq('#6D95BA') # Length 3, so index is 0
+ expect(helper.thumbnail_bg_color('John')).to eq('#A4C3C3') # Length 4, so index is 1
+ expect(helper.thumbnail_bg_color('Jane james')).to eq('#A4C3C3') # Length 10, so index is 1
+ expect(helper.thumbnail_bg_color('Jane_123')).to eq('#E19191') # Length 8, so index is 2
+ expect(helper.thumbnail_bg_color('AlexanderTheGreat')).to eq('#E19191') # Length 17, so index is 2
+ expect(helper.thumbnail_bg_color('Reginald John Sans')).to eq('#6D95BA') # Length 18, so index is 0
+ end
+ end
end
diff --git a/spec/jobs/internal/check_new_versions_job_spec.rb b/spec/jobs/internal/check_new_versions_job_spec.rb
index 3253ce319..3d36c4ba7 100644
--- a/spec/jobs/internal/check_new_versions_job_spec.rb
+++ b/spec/jobs/internal/check_new_versions_job_spec.rb
@@ -4,11 +4,11 @@ RSpec.describe Internal::CheckNewVersionsJob do
subject(:job) { described_class.perform_now }
it 'updates the latest chatwoot version in redis' do
- version = '1.1.1'
+ data = { 'version' => '1.2.3' }.to_json
allow(Rails.env).to receive(:production?).and_return(true)
- allow(ChatwootHub).to receive(:latest_version).and_return(version)
+ allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
job
- expect(ChatwootHub).to have_received(:latest_version)
- expect(Redis::Alfred.get(Redis::Alfred::LATEST_CHATWOOT_VERSION)).to eq version
+ expect(ChatwootHub).to have_received(:sync_with_hub)
+ expect(Redis::Alfred.get(Redis::Alfred::LATEST_CHATWOOT_VERSION)).to eq data['version']
end
end
diff --git a/spec/jobs/migration/remove_message_notifications_spec.rb b/spec/jobs/migration/remove_message_notifications_spec.rb
new file mode 100644
index 000000000..9c90e1160
--- /dev/null
+++ b/spec/jobs/migration/remove_message_notifications_spec.rb
@@ -0,0 +1,10 @@
+require 'rails_helper'
+
+RSpec.describe Migration::RemoveMessageNotifications do
+ subject(:job) { described_class.perform_later }
+
+ it 'enqueues the job' do
+ expect { job }.to have_enqueued_job(described_class)
+ .on_queue('scheduled_jobs')
+ end
+end
diff --git a/spec/jobs/notification/remove_duplicate_notification_job_spec.rb b/spec/jobs/notification/remove_duplicate_notification_job_spec.rb
new file mode 100644
index 000000000..c48524e1a
--- /dev/null
+++ b/spec/jobs/notification/remove_duplicate_notification_job_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+RSpec.describe Notification::RemoveDuplicateNotificationJob do
+ let(:user) { create(:user) }
+ let(:conversation) { create(:conversation) }
+
+ it 'enqueues the job' do
+ duplicate_notification = create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
+ expect do
+ described_class.perform_later(duplicate_notification)
+ end.to have_enqueued_job(described_class)
+ .on_queue('default')
+ end
+
+ it 'removes duplicate notifications' do
+ create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
+ duplicate_notification = create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
+
+ described_class.perform_now(duplicate_notification)
+ expect(Notification.count).to eq(1)
+ end
+end
diff --git a/spec/jobs/webhooks/facebook_delivery_job_spec.rb b/spec/jobs/webhooks/facebook_delivery_job_spec.rb
new file mode 100644
index 000000000..8dc91b9ab
--- /dev/null
+++ b/spec/jobs/webhooks/facebook_delivery_job_spec.rb
@@ -0,0 +1,44 @@
+require 'rails_helper'
+
+RSpec.describe Webhooks::FacebookDeliveryJob do
+ include ActiveJob::TestHelper
+
+ let(:message) { 'test_message' }
+ let(:parsed_message) { instance_double(Integrations::Facebook::MessageParser) }
+ let(:delivery_status) { instance_double(Integrations::Facebook::DeliveryStatus) }
+
+ before do
+ allow(Integrations::Facebook::MessageParser).to receive(:new).with(message).and_return(parsed_message)
+ allow(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message).and_return(delivery_status)
+ allow(delivery_status).to receive(:perform)
+ end
+
+ after do
+ clear_enqueued_jobs
+ end
+
+ describe '#perform_later' do
+ it 'enqueues the job' do
+ expect do
+ described_class.perform_later(message)
+ end.to have_enqueued_job(described_class).with(message).on_queue('low')
+ end
+ end
+
+ describe '#perform' do
+ it 'calls the MessageParser with the correct argument' do
+ expect(Integrations::Facebook::MessageParser).to receive(:new).with(message)
+ described_class.perform_now(message)
+ end
+
+ it 'calls the DeliveryStatus with the correct argument' do
+ expect(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message)
+ described_class.perform_now(message)
+ end
+
+ it 'executes perform on the DeliveryStatus instance' do
+ expect(delivery_status).to receive(:perform)
+ described_class.perform_now(message)
+ end
+ end
+end
diff --git a/spec/lib/chatwoot_hub_spec.rb b/spec/lib/chatwoot_hub_spec.rb
index 99eaf19da..b5e0da4dd 100644
--- a/spec/lib/chatwoot_hub_spec.rb
+++ b/spec/lib/chatwoot_hub_spec.rb
@@ -7,11 +7,11 @@ describe ChatwootHub do
expect(described_class.installation_identifier).to eq installation_identifier
end
- context 'when fetching latest_version' do
+ context 'when fetching sync_with_hub' do
it 'get latest version from chatwoot hub' do
version = '1.1.1'
allow(RestClient).to receive(:post).and_return({ version: version }.to_json)
- expect(described_class.latest_version).to eq version
+ expect(described_class.sync_with_hub['version']).to eq version
expect(RestClient).to have_received(:post).with(described_class::PING_URL, described_class.instance_config
.merge(described_class.instance_metrics).to_json, { content_type: :json, accept: :json })
end
@@ -20,7 +20,7 @@ describe ChatwootHub do
version = '1.1.1'
with_modified_env DISABLE_TELEMETRY: 'true' do
allow(RestClient).to receive(:post).and_return({ version: version }.to_json)
- expect(described_class.latest_version).to eq version
+ expect(described_class.sync_with_hub['version']).to eq version
expect(RestClient).to have_received(:post).with(described_class::PING_URL,
described_class.instance_config.to_json, { content_type: :json, accept: :json })
end
@@ -28,7 +28,7 @@ describe ChatwootHub do
it 'returns nil when chatwoot hub is down' do
allow(RestClient).to receive(:post).and_raise(ExceptionList::REST_CLIENT_EXCEPTIONS.sample)
- expect(described_class.latest_version).to be_nil
+ expect(described_class.sync_with_hub).to be_nil
end
end
diff --git a/spec/lib/integrations/facebook/delivery_status_spec.rb b/spec/lib/integrations/facebook/delivery_status_spec.rb
new file mode 100644
index 000000000..28f1fe982
--- /dev/null
+++ b/spec/lib/integrations/facebook/delivery_status_spec.rb
@@ -0,0 +1,83 @@
+require 'rails_helper'
+
+describe Integrations::Facebook::DeliveryStatus do
+ subject(:message_builder) { described_class.new(message_deliveries, facebook_channel.inbox).perform }
+
+ before do
+ stub_request(:post, /graph\.facebook\.com/)
+ end
+
+ let!(:account) { create(:account) }
+ let!(:facebook_channel) { create(:channel_facebook_page, page_id: '117172741761305') }
+ let!(:message_delivery_object) { build(:message_deliveries).to_json }
+ let!(:message_deliveries) { Integrations::Facebook::MessageParser.new(message_delivery_object) }
+
+ let!(:contact) { create(:contact, account: account) }
+ let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: facebook_channel.inbox, source_id: '3383290475046708') }
+ let!(:conversation) { create(:conversation, inbox: facebook_channel.inbox, contact: contact, contact_inbox: contact_inbox) }
+
+ let!(:message_read_object) { build(:message_reads).to_json }
+ let!(:message_reads) { Integrations::Facebook::MessageParser.new(message_read_object) }
+ let!(:message1) do
+ create(:message, content: 'facebook message', message_type: 'outgoing', inbox: facebook_channel.inbox, conversation: conversation)
+ end
+ let!(:message2) do
+ create(:message, content: 'facebook message', message_type: 'incoming', inbox: facebook_channel.inbox, conversation: conversation)
+ end
+
+ describe '#perform' do
+ context 'when message_deliveries callback fires' do
+ before do
+ allow(Conversations::UpdateMessageStatusJob).to receive(:perform_later)
+ end
+
+ it 'updates all messages if the status is delivered' do
+ described_class.new(params: message_deliveries).perform
+ expect(Conversations::UpdateMessageStatusJob).to have_received(:perform_later).with(
+ message1.conversation.id,
+ Time.zone.at(message_deliveries.delivery['watermark'].to_i).to_datetime,
+ :delivered
+ )
+ end
+
+ it 'does not update the message status if the message is incoming' do
+ described_class.new(params: message_deliveries).perform
+ expect(message2.reload.status).to eq('sent')
+ end
+
+ it 'does not update the message status if the message was created after the watermark' do
+ message1.update(created_at: 1.day.from_now)
+ message_deliveries.delivery['watermark'] = 1.day.ago.to_i
+ described_class.new(params: message_deliveries).perform
+ expect(message1.reload.status).to eq('sent')
+ end
+ end
+
+ context 'when message_reads callback fires' do
+ before do
+ allow(Conversations::UpdateMessageStatusJob).to receive(:perform_later)
+ end
+
+ it 'updates all messages if the status is read' do
+ described_class.new(params: message_reads).perform
+ expect(Conversations::UpdateMessageStatusJob).to have_received(:perform_later).with(
+ message1.conversation.id,
+ Time.zone.at(message_reads.read['watermark'].to_i).to_datetime,
+ :read
+ )
+ end
+
+ it 'does not update the message status if the message is incoming' do
+ described_class.new(params: message_reads).perform
+ expect(message2.reload.status).to eq('sent')
+ end
+
+ it 'does not update the message status if the message was created after the watermark' do
+ message1.update(created_at: 1.day.from_now)
+ message_reads.read['watermark'] = 1.day.ago.to_i
+ described_class.new(params: message_reads).perform
+ expect(message1.reload.status).to eq('sent')
+ end
+ end
+ end
+end
diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb
index 276fadbf9..6275c8dc3 100644
--- a/spec/listeners/action_cable_listener_spec.rb
+++ b/spec/listeners/action_cable_listener_spec.rb
@@ -131,6 +131,27 @@ describe ActionCableListener do
end
end
+ describe '#notification_deleted' do
+ let(:event_name) { :'notification.deleted' }
+ let!(:notification) { create(:notification, account: account, user: agent) }
+ let!(:event) { Events::Base.new(event_name, Time.zone.now, notification: notification) }
+
+ it 'sends message to account admins, inbox agents' do
+ expect(ActionCableBroadcastJob).to receive(:perform_later).with(
+ [agent.pubsub_token],
+ 'notification.deleted',
+ {
+ account_id: notification.account_id,
+ notification: notification.push_event_data,
+ unread_count: 1,
+ count: 1
+ }
+ )
+
+ listener.notification_deleted(event)
+ end
+ end
+
describe '#conversation_updated' do
let(:event_name) { :'conversation.updated' }
let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: agent, is_private: false) }
diff --git a/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb b/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
index 9faabc3af..5661af714 100644
--- a/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
+++ b/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
end
describe 'conversation_creation' do
- let(:mail) { described_class.with(account: account).conversation_creation(conversation, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).conversation_creation(conversation, agent, nil).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation
@@ -27,7 +27,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
end
describe 'conversation_assignment' do
- let(:mail) { described_class.with(account: account).conversation_assignment(conversation, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).conversation_assignment(conversation, agent, nil).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation.display_id}] has been assigned to you.")
@@ -42,7 +42,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
let(:contact) { create(:contact, name: nil, account: account) }
let(:another_agent) { create(:user, email: 'agent2@example.com', account: account) }
let(:message) { create(:message, conversation: conversation, account: account, sender: another_agent) }
- let(:mail) { described_class.with(account: account).conversation_mention(message, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).conversation_mention(conversation, agent, message).deliver_now }
let(:contact_inbox) { create(:contact_inbox, account: account, inbox: conversation.inbox) }
before do
@@ -72,7 +72,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
describe 'assigned_conversation_new_message' do
let(:message) { create(:message, conversation: conversation, account: account) }
- let(:mail) { described_class.with(account: account).assigned_conversation_new_message(message, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).assigned_conversation_new_message(conversation, agent, message).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, New message in your assigned conversation [ID - #{message.conversation.display_id}].")
@@ -90,7 +90,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
describe 'participating_conversation_new_message' do
let(:message) { create(:message, conversation: conversation, account: account) }
- let(:mail) { described_class.with(account: account).participating_conversation_new_message(message, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).participating_conversation_new_message(conversation, agent, message).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, New message in your participating conversation [ID - #{message.conversation.display_id}].")
diff --git a/spec/models/concerns/assignment_handler_shared.rb b/spec/models/concerns/assignment_handler_shared.rb
index 55951cd99..8447b8146 100644
--- a/spec/models/concerns/assignment_handler_shared.rb
+++ b/spec/models/concerns/assignment_handler_shared.rb
@@ -63,47 +63,4 @@ shared_examples_for 'assignment_handler' do
end
end
end
-
- describe '#update_assignee' do
- subject(:update_assignee) { conversation.update_assignee(agent) }
-
- let(:conversation) { create(:conversation, assignee: nil) }
- let(:agent) do
- create(:user, email: 'agent@example.com', account: conversation.account, role: :agent)
- end
- let(:assignment_mailer) { instance_double(AgentNotifications::ConversationNotificationsMailer, deliver: true) }
-
- before do
- create(:inbox_member, user: agent, inbox: conversation.inbox)
- end
-
- it 'assigns the agent to conversation' do
- expect(update_assignee).to be(true)
- expect(conversation.reload.assignee).to eq(agent)
- end
-
- it 'dispaches assignee changed event' do
- # TODO: FIX me
- # expect(EventDispatcherJob).to(have_been_enqueued.at_least(:once).with('assignee.changed', anything, anything, anything, anything))
- expect(EventDispatcherJob).to(have_been_enqueued.at_least(:once))
- expect(update_assignee).to be(true)
- end
-
- it 'adds assignee to conversation participants' do
- expect { update_assignee }.to change { conversation.conversation_participants.count }.by(1)
- end
-
- context 'when agent is current user' do
- before do
- Current.user = agent
- end
-
- it 'creates self-assigned message activity' do
- expect(update_assignee).to be(true)
- expect(Conversations::ActivityMessageJob).to(have_been_enqueued.at_least(:once)
- .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id,
- message_type: :activity, content: "#{agent.name} self-assigned this conversation" }))
- end
- end
- end
end
diff --git a/spec/models/concerns/liquidable_shared.rb b/spec/models/concerns/liquidable_shared.rb
index 7648bd15c..8df526a2f 100644
--- a/spec/models/concerns/liquidable_shared.rb
+++ b/spec/models/concerns/liquidable_shared.rb
@@ -2,8 +2,8 @@ require 'rails_helper'
shared_examples_for 'liqudable' do
context 'when liquid is present in content' do
- let(:contact) { create(:contact, name: 'john', phone_number: '+912883') }
- let(:conversation) { create(:conversation, id: 1, contact: contact) }
+ let(:contact) { create(:contact, name: 'john', phone_number: '+912883', custom_attributes: { customer_type: 'platinum' }) }
+ let(:conversation) { create(:conversation, id: 1, contact: contact, custom_attributes: { priority: 'high' }) }
context 'when message is incoming' do
let(:message) { build(:message, conversation: conversation, message_type: 'incoming') }
@@ -24,6 +24,14 @@ shared_examples_for 'liqudable' do
expect(message.content).to eq 'hey John how are you?'
end
+ it 'set replaces liquid custom attributes in message' do
+ message.content = 'Are you a {{contact.custom_attribute.customer_type}} customer,
+ If yes then the priority is {{conversation.custom_attribute.priority}}'
+ message.save!
+ expect(message.content).to eq 'Are you a platinum customer,
+ If yes then the priority is high'
+ end
+
it 'process liquid operators like default value' do
message.content = 'Can we send you an email at {{ contact.email | default: "default" }} ?'
message.save!
diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb
index 0a04cd73b..82dac1979 100644
--- a/spec/models/conversation_spec.rb
+++ b/spec/models/conversation_spec.rb
@@ -715,61 +715,52 @@ RSpec.describe Conversation do
end
end
- describe 'Custom Sort' do
+ describe 'custom sort option' do
include ActiveJob::TestHelper
- let!(:conversation_7) { create(:conversation, created_at: DateTime.now - 10.days, last_activity_at: DateTime.now - 13.days) }
- let!(:conversation_6) { create(:conversation, created_at: DateTime.now - 10.days, last_activity_at: DateTime.now - 10.days) }
- let!(:conversation_5) { create(:conversation, created_at: DateTime.now - 10.days, last_activity_at: DateTime.now - 12.days, priority: :urgent) }
- let!(:conversation_4) { create(:conversation, created_at: DateTime.now - 10.days, last_activity_at: DateTime.now - 10.days, priority: :urgent) }
- let!(:conversation_3) { create(:conversation, created_at: DateTime.now - 9.days, last_activity_at: DateTime.now - 9.days, priority: :low) }
- let!(:conversation_2) { create(:conversation, created_at: DateTime.now - 6.days, last_activity_at: DateTime.now - 6.days, priority: :high) }
- let!(:conversation_1) { create(:conversation, created_at: DateTime.now - 8.days, last_activity_at: DateTime.now - 8.days, priority: :medium) }
+ let!(:conversation_7) { create(:conversation, created_at: DateTime.now - 6.days, last_activity_at: DateTime.now - 13.days) }
+ let!(:conversation_6) { create(:conversation, created_at: DateTime.now - 7.days, last_activity_at: DateTime.now - 10.days) }
+ let!(:conversation_5) { create(:conversation, created_at: DateTime.now - 8.days, last_activity_at: DateTime.now - 12.days, priority: :urgent) }
+ let!(:conversation_4) { create(:conversation, created_at: DateTime.now - 9.days, last_activity_at: DateTime.now - 11.days, priority: :urgent) }
+ let!(:conversation_3) { create(:conversation, created_at: DateTime.now - 5.days, last_activity_at: DateTime.now - 9.days, priority: :low) }
+ let!(:conversation_2) { create(:conversation, created_at: DateTime.now - 3.days, last_activity_at: DateTime.now - 6.days, priority: :high) }
+ let!(:conversation_1) { create(:conversation, created_at: DateTime.now - 4.days, last_activity_at: DateTime.now - 8.days, priority: :medium) }
- it 'Sort conversations based on created_at' do
- records = described_class.sort_on_created_at
- expect(records.first.id).to eq(conversation_7.id)
- expect(records.last.id).to eq(conversation_2.id)
+ describe 'sort_on_created_at' do
+ let(:created_desc_order) do
+ [
+ conversation_2.id, conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id,
+ conversation_5.id, conversation_4.id
+ ]
+ end
+
+ it 'returns the list in ascending order by default' do
+ records = described_class.sort_on_created_at
+ expect(records.map(&:id)).to eq created_desc_order.reverse
+ end
+
+ it 'returns the list in descending order if desc is passed as sort direction' do
+ records = described_class.sort_on_created_at(:desc)
+ expect(records.map(&:id)).to eq created_desc_order
+ end
end
- context 'when sort on last_user_message_at' do
- before do
- create(:message, conversation_id: conversation_3.id, message_type: :outgoing, created_at: DateTime.now - 9.days)
- create(:message, conversation_id: conversation_1.id, message_type: :incoming, created_at: DateTime.now - 8.days)
- create(:message, conversation_id: conversation_1.id, message_type: :incoming, created_at: DateTime.now - 8.days)
- create(:message, conversation_id: conversation_1.id, message_type: :outgoing, created_at: DateTime.now - 7.days)
- create(:message, conversation_id: conversation_2.id, message_type: :incoming, created_at: DateTime.now - 6.days)
- create(:message, conversation_id: conversation_2.id, message_type: :incoming, created_at: DateTime.now - 6.days)
- create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now - 6.days)
- create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now - 6.days)
- create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now - 2.days)
+ describe 'sort_on_last_activity_at' do
+ let(:last_activity_asc_order) do
+ [
+ conversation_7.id, conversation_5.id, conversation_4.id, conversation_6.id, conversation_3.id,
+ conversation_1.id, conversation_2.id
+ ]
end
- # conversation_2 has last unanswered incoming message 6 days ago
- # conversation_3 has last unanswered incoming message 2 days ago
- # conversation_1 has incoming message 8 days ago but outgoing message on 7 days ago
- # so we won't consider it to show it on top of the sort as it is answered/replied conversation
- it 'Sort conversations with oldest unanswered incoming message first' do
- conversation_with_message_count = described_class.joins(:messages).uniq.count
- records = described_class.last_user_message_at
-
- expect(records.length).to eq(conversation_with_message_count)
- expect(records[0]['id']).to eq(conversation_2.id)
- expect(records[1]['id']).to eq(conversation_3.id)
- expect(records[2]['id']).to eq(conversation_1.id)
- expect(records.pluck(:id)).not_to include(conversation_4.id)
+ it 'returns the list in descending order by default' do
+ records = described_class.sort_on_last_activity_at
+ expect(records.map(&:id)).to eq last_activity_asc_order.reverse
end
- # Now we have no incoming message the sprt will happen on the created at
- it 'Sort based on oldest message first when there are no incoming message' do
- Message.where(message_type: :incoming).update(message_type: :template)
- conversation_with_message_count = described_class.joins(:messages).uniq.count
- records = described_class.last_user_message_at
-
- expect(records.length).to eq(conversation_with_message_count)
- expect(records[0]['id']).to eq(conversation_1.id)
- expect(records[1]['id']).to eq(conversation_2.id)
- expect(records[2]['id']).to eq(conversation_3.id)
+ it 'returns the list in asc order if asc is passed as sort direction' do
+ records = described_class.sort_on_last_activity_at(:asc)
+ expect(records.map(&:id)).to eq last_activity_asc_order
end
end
@@ -781,7 +772,7 @@ RSpec.describe Conversation do
end
it 'sort conversations with latest resolved conversation at first' do
- records = described_class.latest
+ records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_3.id)
@@ -795,27 +786,48 @@ RSpec.describe Conversation do
content: 'Conversation was marked resolved by system due to days of inactivity'
)
end
- records = described_class.latest
+ records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_1.id)
end
it 'Sort conversations with latest message' do
create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now)
- records = described_class.latest
+ records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_3.id)
end
end
- context 'when sort on priority' do
- it 'Sort conversations with the following order high > medium > low > nil' do
+ describe 'sort_on_priority' do
+ it 'return list with the following order urgent > high > medium > low > nil by default' do
# ensure they are not pre-sorted
records = described_class.sort_on_created_at
expect(records.pluck(:priority)).not_to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
records = described_class.sort_on_priority
expect(records.pluck(:priority)).to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
+ expect(records.pluck(:id)).to eq(
+ [
+ conversation_4.id, conversation_5.id, conversation_2.id, conversation_1.id, conversation_3.id,
+ conversation_6.id, conversation_7.id
+ ]
+ )
+ end
+
+ it 'return list with the following order low > medium > high > urgent > nil by default' do
+ # ensure they are not pre-sorted
+ records = described_class.sort_on_created_at
+ expect(records.pluck(:priority)).not_to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
+
+ records = described_class.sort_on_priority(:asc)
+ expect(records.pluck(:priority)).to eq(['low', 'medium', 'high', 'urgent', 'urgent', nil, nil])
+ expect(records.pluck(:id)).to eq(
+ [
+ conversation_3.id, conversation_1.id, conversation_2.id, conversation_4.id, conversation_5.id,
+ conversation_6.id, conversation_7.id
+ ]
+ )
end
it 'sorts conversation with last_activity for the same priority' do
@@ -830,5 +842,33 @@ RSpec.describe Conversation do
expect(records.pluck(:priority, :id)).to eq([[nil, conversation_6.id], [nil, conversation_7.id]])
end
end
+
+ describe 'sort_on_waiting_since' do
+ it 'returns the list in ascending order by default' do
+ records = described_class.sort_on_waiting_since
+ expect(records.map(&:id)).to eq [
+ conversation_4.id, conversation_5.id, conversation_6.id, conversation_7.id, conversation_3.id, conversation_1.id,
+ conversation_2.id
+ ]
+ end
+
+ it 'returns the list in desc order if asc is passed as sort direction' do
+ records = described_class.sort_on_waiting_since(:desc)
+ expect(records.map(&:id)).to eq [
+ conversation_2.id, conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id, conversation_5.id,
+ conversation_4.id
+ ]
+ end
+ end
+ end
+
+ describe 'cached_label_list_array' do
+ let(:conversation) { create(:conversation) }
+
+ it 'returns the correct list of labels' do
+ conversation.update(label_list: %w[customer-support enterprise paid-customer])
+
+ expect(conversation.cached_label_list_array).to eq %w[customer-support enterprise paid-customer]
+ end
end
end
diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb
index 71a6209ad..31d127844 100644
--- a/spec/models/notification_spec.rb
+++ b/spec/models/notification_spec.rb
@@ -24,7 +24,6 @@ RSpec.describe Notification do
context 'when push_title is called' do
it 'returns appropriate title suited for the notification type conversation_creation' do
notification = create(:notification, notification_type: 'conversation_creation')
-
expect(notification.push_message_title).to eq "[New conversation] - ##{notification.primary_actor.display_id} has\
been created in #{notification.primary_actor.inbox.name}"
end
@@ -37,7 +36,8 @@ RSpec.describe Notification do
it 'returns appropriate title suited for the notification type assigned_conversation_new_message' do
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
- notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
+ notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} \
#{message.content.truncate_words(10)}"
@@ -46,14 +46,16 @@ RSpec.describe Notification do
it 'returns appropriate title suited for the notification type assigned_conversation_new_message when attachment message' do
# checking content nil should be suffice for attachments
message = create(:message, sender: create(:user), content: nil)
- notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
+ notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} "
end
it 'returns appropriate title suited for the notification type participating_conversation_new_message' do
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
- notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message)
+ notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} \
#{message.content.truncate_words(10)}"
@@ -61,17 +63,16 @@ RSpec.describe Notification do
it 'returns appropriate title suited for the notification type participating_conversation_new_message having mention' do
message = create(:message, sender: create(:user), content: 'Hey [@John](mention://user/1/john), can you check this ticket?')
- notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message,
- secondary_actor: message.sender)
-
+ notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} Hey @John, can you check this ticket?"
end
it 'returns appropriate title suited for the notification type participating_conversation_new_message having multple mention' do
message = create(:message, sender: create(:user),
content: 'Hey [@John](mention://user/1/john), [@Alisha Peter](mention://user/2/alisha) can you check this ticket?')
- notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message,
- secondary_actor: message.sender)
+ notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} \
Hey @John, @Alisha Peter can you check this ticket?"
@@ -79,15 +80,15 @@ Hey @John, @Alisha Peter can you check this ticket?"
it 'returns appropriate title suited for the notification type participating_conversation_new_message if username contains white space' do
message = create(:message, sender: create(:user), content: 'Hey [@John Peter](mention://user/1/john%20K) please check this?')
- notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message,
- secondary_actor: message.sender)
+ notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} Hey @John Peter please check this?"
end
it 'returns appropriate title suited for the notification type conversation_mention' do
message = create(:message, sender: create(:user), content: 'Hey [@John](mention://user/1/john), can you check this ticket?')
- notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message, secondary_actor: message.sender)
+ notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message.conversation, secondary_actor: message)
expect(notification.push_message_title).to eq "[##{message.conversation.display_id}] Hey @John, can you check this ticket?"
end
@@ -98,7 +99,7 @@ Hey @John, @Alisha Peter can you check this ticket?"
create(:user),
content: 'Hey [@John](mention://user/1/john), [@Alisha Peter](mention://user/2/alisha) can you check this ticket?'
)
- notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message, secondary_actor: message.sender)
+ notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message.conversation, secondary_actor: message)
expect(notification.push_message_title).to eq "[##{message.conversation.display_id}] Hey @John, @Alisha Peter can you check this ticket?"
end
@@ -109,10 +110,15 @@ Hey @John, @Alisha Peter can you check this ticket?"
create(:user),
content: 'Hey [@John Peter](mention://user/1/john%20K) please check this?'
)
- notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message, secondary_actor: message.sender)
-
+ notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message.conversation, secondary_actor: message)
expect(notification.push_message_title).to eq "[##{message.conversation.display_id}] Hey @John Peter please check this?"
end
+
+ it 'calls remove duplicate notification job' do
+ allow(Notification::RemoveDuplicateNotificationJob).to receive(:perform_later)
+ notification = create(:notification, notification_type: 'conversation_mention')
+ expect(Notification::RemoveDuplicateNotificationJob).to have_received(:perform_later).with(notification)
+ end
end
context 'when fcm push data' do
@@ -125,16 +131,15 @@ Hey @John, @Alisha Peter can you check this ticket?"
it 'returns correct data for primary actor message' do
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
- notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
-
+ notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.fcm_push_data[:primary_actor]).to eq({
- 'id' => notification.primary_actor.id,
- 'conversation_id' => notification.primary_actor.conversation.display_id
+ 'id' => notification.primary_actor.display_id
})
end
end
- context 'when primary actory is deleted' do
+ context 'when primary actor is deleted' do
let!(:conversation) { create(:conversation) }
it 'clears notifications' do
diff --git a/spec/services/line/incoming_message_service_spec.rb b/spec/services/line/incoming_message_service_spec.rb
index ee53421b5..0547cfbd0 100644
--- a/spec/services/line/incoming_message_service_spec.rb
+++ b/spec/services/line/incoming_message_service_spec.rb
@@ -105,6 +105,41 @@ describe Line::IncomingMessageService do
}.with_indifferent_access
end
+ let(:sticker_params) do
+ {
+ 'destination': '2342234234',
+ 'events': [
+ {
+ 'replyToken': '0f3779fba3b349968c5d07db31eab56f',
+ 'type': 'message',
+ 'mode': 'active',
+ 'timestamp': 1_462_629_479_859,
+ 'source': {
+ 'type': 'user',
+ 'userId': 'U4af4980629'
+ },
+ 'message': {
+ 'type': 'sticker',
+ 'id': '1501597916',
+ 'quoteToken': 'q3Plxr4AgKd...',
+ 'stickerId': '52002738',
+ 'packageId': '11537'
+ }
+ },
+ {
+ 'replyToken': '8cf9239d56244f4197887e939187e19e',
+ 'type': 'follow',
+ 'mode': 'active',
+ 'timestamp': 1_462_629_479_859,
+ 'source': {
+ 'type': 'user',
+ 'userId': 'U4af4980629'
+ }
+ }
+ ]
+ }.with_indifferent_access
+ end
+
describe '#perform' do
context 'when valid text message params' do
it 'creates appropriate conversations, message and contacts' do
@@ -126,6 +161,26 @@ describe Line::IncomingMessageService do
end
end
+ context 'when valid sticker message params' do
+ it 'creates appropriate conversations, message and contacts' do
+ line_bot = double
+ line_user_profile = double
+ allow(Line::Bot::Client).to receive(:new).and_return(line_bot)
+ allow(line_bot).to receive(:get_profile).and_return(line_user_profile)
+ allow(line_user_profile).to receive(:body).and_return(
+ {
+ 'displayName': 'LINE Test',
+ 'userId': 'U4af4980629',
+ 'pictureUrl': 'https://test.com'
+ }.to_json
+ )
+ described_class.new(inbox: line_channel.inbox, params: sticker_params).perform
+ expect(line_channel.inbox.conversations).not_to eq(0)
+ expect(Contact.all.first.name).to eq('LINE Test')
+ expect(line_channel.inbox.messages.first.content).to eq('')
+ end
+ end
+
context 'when valid image message params' do
it 'creates appropriate conversations, message and contacts' do
line_bot = double
diff --git a/spec/services/line/send_on_line_service_spec.rb b/spec/services/line/send_on_line_service_spec.rb
index 2320f8679..bed8917b8 100644
--- a/spec/services/line/send_on_line_service_spec.rb
+++ b/spec/services/line/send_on_line_service_spec.rb
@@ -92,5 +92,57 @@ describe Line::SendOnLineService do
expect(message.status).to eq('delivered')
end
end
+
+ context 'with message attachments' do
+ it 'sends the message with text and attachments' do
+ attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
+ attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
+ expected_url_regex = %r{rails/active_storage/disk/[a-zA-Z0-9=_\-+]+/avatar\.png}
+
+ expect(line_client).to receive(:push_message).with(
+ message.conversation.contact_inbox.source_id,
+ [
+ { type: 'text', text: message.content },
+ {
+ type: 'image',
+ originalContentUrl: match(expected_url_regex),
+ previewImageUrl: match(expected_url_regex)
+ }
+ ]
+ )
+
+ described_class.new(message: message).perform
+ end
+
+ it 'sends the message with attachments only' do
+ attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
+ attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
+ message.update!(content: nil)
+ expected_url_regex = %r{rails/active_storage/disk/[a-zA-Z0-9=_\-+]+/avatar\.png}
+
+ expect(line_client).to receive(:push_message).with(
+ message.conversation.contact_inbox.source_id,
+ [
+ {
+ type: 'image',
+ originalContentUrl: match(expected_url_regex),
+ previewImageUrl: match(expected_url_regex)
+ }
+ ]
+ )
+
+ described_class.new(message: message).perform
+ end
+
+ it 'sends the message with text only' do
+ message.attachments.destroy_all
+ expect(line_client).to receive(:push_message).with(
+ message.conversation.contact_inbox.source_id,
+ { type: 'text', text: message.content }
+ )
+
+ described_class.new(message: message).perform
+ end
+ end
end
end
diff --git a/spec/services/messages/mention_service_spec.rb b/spec/services/messages/mention_service_spec.rb
index 6090a013c..94277570b 100644
--- a/spec/services/messages/mention_service_spec.rb
+++ b/spec/services/messages/mention_service_spec.rb
@@ -32,7 +32,8 @@ describe Messages::MentionService do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'conversation_mention',
user: first_agent,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
@@ -55,11 +56,13 @@ describe Messages::MentionService do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'conversation_mention',
user: second_agent,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'conversation_mention',
user: first_agent,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
it 'add the users to the participants list' do
diff --git a/spec/services/messages/new_message_notification_service_spec.rb b/spec/services/messages/new_message_notification_service_spec.rb
index bb35ec48a..c76453174 100644
--- a/spec/services/messages/new_message_notification_service_spec.rb
+++ b/spec/services/messages/new_message_notification_service_spec.rb
@@ -40,21 +40,24 @@ describe Messages::NewMessageNotificationService do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_2,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
it 'creates notifications for assignee' do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
it 'will not create notifications for the user who created the message' do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_1,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
@@ -69,18 +72,21 @@ describe Messages::NewMessageNotificationService do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
it 'creates notifications for all participating users' do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_1,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_2,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
@@ -97,7 +103,8 @@ describe Messages::NewMessageNotificationService do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
@@ -112,11 +119,13 @@ describe Messages::NewMessageNotificationService do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 59ad6c1d3..f7a9462e3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7746,12 +7746,14 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae"
integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==
-axios@^0.21.2:
- version "0.21.2"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
- integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==
+axios@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
+ integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
dependencies:
- follow-redirects "^1.14.0"
+ follow-redirects "^1.15.0"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
axobject-query@^3.1.1:
version "3.2.1"
@@ -8926,6 +8928,11 @@ color-support@^1.1.2:
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
+color2k@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/color2k/-/color2k-2.0.2.tgz#ac2b4aea11c822a6bcb70c768b5a289f4fffcebb"
+ integrity sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==
+
color@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e"
@@ -11392,10 +11399,10 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
-follow-redirects@^1.0.0, follow-redirects@^1.14.0:
- version "1.14.8"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
- integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
+follow-redirects@^1.0.0, follow-redirects@^1.15.0:
+ version "1.15.3"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
+ integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
for-each@^0.3.3:
version "0.3.3"
@@ -17300,6 +17307,11 @@ proxy-from-env@1.0.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -20637,6 +20649,11 @@ vue-upload-component@2.8.22:
resolved "https://registry.yarnpkg.com/vue-upload-component/-/vue-upload-component-2.8.22.tgz#7a1573149a4afa5ca6e8c7e0bc70533925fe26b7"
integrity sha512-AJpETqiZrgqs8bwJQpWTFrRg3i6s7cUodRRZVnb1f94Jvpd0YYfzGY4zluBqPmssNSkUaYu7EteXaK8aW17Osw==
+vue-virtual-scroll-list@^2.3.5:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.5.tgz#b589ac6245faf857c35090f854e59d653e90626c"
+ integrity sha512-YFK6u5yltqtAOfTBcij/KGAS2SoZvzbNIAf9qTULauPObEp53xj22tDuohrrM2vNkgoD5kejXICIUBt2Q4ZDqQ==
+
vue2-datepicker@^3.9.1:
version "3.9.1"
resolved "https://registry.yarnpkg.com/vue2-datepicker/-/vue2-datepicker-3.9.1.tgz#00d11cf30942e850f8b1a397af3c15c7465f248e"
Howdy, admin 👋
-
- <%= form_for(resource, as: resource_name, url: '/super_admin/sign_in', html: { class: 'login-box column align-self-top'}) do |f| %>
-
diff --git a/app/views/super_admin/instance_statuses/show.html.erb b/app/views/super_admin/instance_statuses/show.html.erb
index d0d23f2a1..5474bff6a 100644
--- a/app/views/super_admin/instance_statuses/show.html.erb
+++ b/app/views/super_admin/instance_statuses/show.html.erb
@@ -1,5 +1,5 @@
<% content_for(:title) do %>
- Instance Health
+ Instance Status
<% end %>
- <% if flash[:error].present? %>
-
+
+
<%= flash[:error] %>
- <% end %>
-
@@ -15,7 +15,7 @@
<% @metrics.each do |key,value| %>
<%= key %>
- <%= value %>
+ <%= value %>
<% end %>
diff --git a/app/views/super_admin/settings/show.html.erb b/app/views/super_admin/settings/show.html.erb
new file mode 100644
index 000000000..45dd46c47
--- /dev/null
+++ b/app/views/super_admin/settings/show.html.erb
@@ -0,0 +1,109 @@
+<% content_for(:title) do %>
+ Settings
+<% end %>
+
+
+
+
+
+
+ <%= content_for(:title) %>
+
+ Update your instance settings, access billing portal
+
+
+
+
+
+
+ Current plan
+
+
+ Refresh
+
+
+ <%= SuperAdmin::FeaturesHelper.plan_details.html_safe %>
+
+ Installation Identifier
+ <%= ChatwootHub.installation_identifier %>
+
+
+
+
+ Current plan
+ <%= SuperAdmin::FeaturesHelper.plan_details.html_safe %>
+
+
+
+
+
+
+ <% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %>
+
+
+ You have <%= User.count %> agents. Please add more licenses.
+
+
+ <% end %>
+
+
+
+
+ Need help?
+ Do you face any issues? We are here to help.
+
+
+
+
+ <% if ChatwootHub.pricing_plan !='community' %>
+
+ <% end %>
+
+
+ Features
+
+
+ <% SuperAdmin::FeaturesHelper.available_features.each do |feature, attrs| %>
+
+
+
+
+
+
+
+ <% if !attrs[:enabled] %>
+
+ <% end %>
+
+
+ <%= attrs[:name] %>
+ <% if attrs[:enterprise] %>
+ EE
+ <% end %>
+ <% if attrs[:config_key].present? && attrs[:enabled] %>
+
+
+
+ <% end %>
+
+
+ <%= attrs[:description] %>
+
+ <% end %>
+
+
+
diff --git a/config/app.yml b/config/app.yml
index 93e0bcbed..1e8d30423 100644
--- a/config/app.yml
+++ b/config/app.yml
@@ -1,5 +1,5 @@
shared: &shared
- version: '3.3.1'
+ version: '3.4.0'
development:
<<: *shared
diff --git a/config/initializers/facebook_messenger.rb b/config/initializers/facebook_messenger.rb
index f9f4fae88..354687cbd 100644
--- a/config/initializers/facebook_messenger.rb
+++ b/config/initializers/facebook_messenger.rb
@@ -29,14 +29,13 @@ Rails.application.reloader.to_prepare do
end
Facebook::Messenger::Bot.on :delivery do |delivery|
- # delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
- # delivery.sender # => { 'id' => '1008372609250235' }
- # delivery.recipient # => { 'id' => '2015573629214912' }
- # delivery.at # => 2016-04-22 21:30:36 +0200
- # delivery.seq # => 37
- updater = Integrations::Facebook::DeliveryStatus.new(delivery)
- updater.perform
- Rails.logger.info "Human was online at #{delivery.at}"
+ Rails.logger.info "Recieved delivery status #{delivery.to_json}"
+ Webhooks::FacebookDeliveryJob.perform_later(delivery.to_json)
+ end
+
+ Facebook::Messenger::Bot.on :read do |read|
+ Rails.logger.info "Recieved read status #{read.to_json}"
+ Webhooks::FacebookDeliveryJob.perform_later(read.to_json)
end
Facebook::Messenger::Bot.on :message_echo do |message|
diff --git a/config/installation_config.yml b/config/installation_config.yml
index c1bbb299b..a53bb1ed5 100644
--- a/config/installation_config.yml
+++ b/config/installation_config.yml
@@ -74,3 +74,13 @@
value:
- name: LOGO_DARK
value: '/brand-assets/logo_dark.svg'
+- name: INSTALLATION_PRICING_PLAN
+ value: 'community'
+- name: INSTALLATION_PRICING_PLAN_QUANTITY
+ value: 0
+- name: CHATWOOT_SUPPORT_WEBSITE_TOKEN
+ value:
+- name: CHATWOOT_SUPPORT_SCRIPT_URL
+ value:
+- name: CHATWOOT_SUPPORT_IDENTIFIER_HASH
+ value:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 14175b263..e6629ae8d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -215,6 +215,11 @@ en:
view_all_articles: View all
article: article
articles: articles
+ author: author
+ authors: authors
+ other: other
+ others: others
+ by: By
no_articles: There are no articles here
footer:
made_with: Made with
diff --git a/config/routes.rb b/config/routes.rb
index f9346eb0f..ec838c107 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -45,7 +45,9 @@ Rails.application.routes.draw do
end
resource :bulk_actions, only: [:create]
resources :agents, only: [:index, :create, :update, :destroy]
- resources :agent_bots, only: [:index, :create, :show, :update, :destroy]
+ resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
+ delete :avatar, on: :member
+ end
resources :contact_inboxes, only: [] do
collection do
post :filter
@@ -84,6 +86,7 @@ Rails.application.routes.draw do
resources :messages, only: [:index, :create, :destroy] do
member do
post :translate
+ post :retry
end
end
resources :assignments, only: [:create]
@@ -166,11 +169,14 @@ Rails.application.routes.draw do
end
end
- resources :notifications, only: [:index, :update] do
+ resources :notifications, only: [:index, :update, :destroy] do
collection do
post :read_all
get :unread_count
end
+ member do
+ post :snooze
+ end
end
resource :notification_settings, only: [:show, :update]
@@ -325,7 +331,9 @@ Rails.application.routes.draw do
get :login
end
end
- resources :agent_bots, only: [:index, :create, :show, :update, :destroy]
+ resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
+ delete :avatar, on: :member
+ end
resources :accounts, only: [:create, :show, :update, :destroy] do
resources :account_users, only: [:index, :create] do
collection do
@@ -441,6 +449,10 @@ Rails.application.routes.draw do
resources :platform_apps, only: [:index, :new, :create, :show, :edit, :update]
resource :instance_status, only: [:show]
+ resource :settings, only: [:show] do
+ get :refresh, on: :collection
+ end
+
# resources that doesn't appear in primary navigation in super admin
resources :account_users, only: [:new, :create, :destroy]
end
diff --git a/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb b/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb
new file mode 100644
index 000000000..14a52a9a7
--- /dev/null
+++ b/db/migrate/20231129091149_add_snoozed_until_to_notifications.rb
@@ -0,0 +1,5 @@
+class AddSnoozedUntilToNotifications < ActiveRecord::Migration[7.0]
+ def change
+ add_column :notifications, :snoozed_until, :datetime
+ end
+end
diff --git a/db/migrate/20231201014644_remove_notifications_with_message_primary_actor.rb b/db/migrate/20231201014644_remove_notifications_with_message_primary_actor.rb
new file mode 100644
index 000000000..d0698a8cb
--- /dev/null
+++ b/db/migrate/20231201014644_remove_notifications_with_message_primary_actor.rb
@@ -0,0 +1,5 @@
+class RemoveNotificationsWithMessagePrimaryActor < ActiveRecord::Migration[7.0]
+ def change
+ Migration::RemoveMessageNotifications.perform_later
+ end
+end
diff --git a/db/migrate/20231211010807_add_cached_labels_list.rb b/db/migrate/20231211010807_add_cached_labels_list.rb
new file mode 100644
index 000000000..026bcf7d1
--- /dev/null
+++ b/db/migrate/20231211010807_add_cached_labels_list.rb
@@ -0,0 +1,7 @@
+class AddCachedLabelsList < ActiveRecord::Migration[7.0]
+ def change
+ add_column :conversations, :cached_label_list, :string
+ Conversation.reset_column_information
+ ActsAsTaggableOn::Taggable::Cache.included(Conversation)
+ end
+end
diff --git a/db/migrate/20231219000743_re_run_cache_label_job.rb b/db/migrate/20231219000743_re_run_cache_label_job.rb
new file mode 100644
index 000000000..ebe553f1c
--- /dev/null
+++ b/db/migrate/20231219000743_re_run_cache_label_job.rb
@@ -0,0 +1,16 @@
+class ReRunCacheLabelJob < ActiveRecord::Migration[7.0]
+ def change
+ update_exisiting_conversations
+ end
+
+ private
+
+ def update_exisiting_conversations
+ # Run label migrations on the accounts that are not suspended
+ ::Account.active.find_in_batches do |account_batch|
+ account_batch.each do |account|
+ Migration::ConversationCacheLabelJob.perform_later(account)
+ end
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 081030b11..373d81918 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: 2023_11_14_111614) do
+ActiveRecord::Schema[7.0].define(version: 2023_12_19_000743) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -453,6 +453,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_14_111614) do
t.integer "priority"
t.bigint "sla_policy_id"
t.datetime "waiting_since"
+ t.string "cached_label_list"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id"
t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx"
@@ -729,6 +730,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_14_111614) do
t.datetime "read_at", precision: nil
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.datetime "snoozed_until"
t.index ["account_id"], name: "index_notifications_on_account_id"
t.index ["primary_actor_type", "primary_actor_id"], name: "uniq_primary_actor_per_account_notifications"
t.index ["secondary_actor_type", "secondary_actor_id"], name: "uniq_secondary_actor_per_account_notifications"
diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh
index 5e2d3c250..c5060f869 100644
--- a/deployment/setup_20.04.sh
+++ b/deployment/setup_20.04.sh
@@ -2,7 +2,7 @@
# Description: Install and manage a Chatwoot installation.
# OS: Ubuntu 20.04 LTS
-# Script Version: 2.6.0
+# Script Version: 2.7.0
# Run this script as root
set -eu -o errexit -o pipefail -o noclobber -o nounset
@@ -19,7 +19,7 @@ fi
# option --output/-o requires 1 argument
LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,webserver,version
OPTIONS=cdhiI:l:rsuwv
-CWCTL_VERSION="2.6.0"
+CWCTL_VERSION="2.7.0"
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
CHATWOOT_HUB_URL="https://hub.2.chatwoot.com/events"
@@ -173,15 +173,19 @@ EOF
function install_dependencies() {
apt update && apt upgrade -y
apt install -y curl
- curl -sL https://deb.nodesource.com/setup_20.x | bash -
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
+ mkdir -p /etc/apt/keyrings
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
+ NODE_MAJOR=20
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
+
apt update
apt install -y \
- git software-properties-common imagemagick libpq-dev \
+ git software-properties-common ca-certificates imagemagick libpq-dev \
libxml2-dev libxslt1-dev file g++ gcc autoconf build-essential \
libssl-dev libyaml-dev libreadline-dev gnupg2 \
postgresql-client redis-tools \
@@ -754,10 +758,39 @@ function upgrade_redis() {
apt install libvips -y
}
+
+##############################################################################
+# Update nodejs to v20+
+# Globals:
+# None
+# Arguments:
+# None
+# Outputs:
+# None
+##############################################################################
function upgrade_node() {
- echo "Upgrading nodejs version to v20.x"
- curl -sL https://deb.nodesource.com/setup_20.x | sudo bash -
- apt install -y nodejs
+ echo "Checking Node.js version..."
+
+ # Get current Node.js version
+ current_version=$(node --version | cut -c 2-)
+
+ # Parse major version number
+ major_version=$(echo "$current_version" | cut -d. -f1)
+
+ if [ "$major_version" -ge 20 ]; then
+ echo "Node.js is already version $current_version (>= 20.x). Skipping Node.js upgrade."
+ return
+ fi
+
+ echo "Upgrading Node.js version to v20.x"
+ mkdir -p /etc/apt/keyrings
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
+ NODE_MAJOR=20
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
+
+ apt update
+ apt install nodejs -y
+
}
##############################################################################
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 676b5bf74..6503530b7 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -104,7 +104,7 @@ RUN apk update && apk add --no-cache \
&& gem install bundler
RUN if [ "$RAILS_ENV" != "production" ]; then \
- apk add --no-cache nodejs yarn; \
+ apk add --no-cache nodejs-current yarn; \
fi
COPY --from=pre-builder /gems/ /gems/
diff --git a/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb b/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb
new file mode 100644
index 000000000..c8a42e391
--- /dev/null
+++ b/enterprise/app/controllers/enterprise/super_admin/app_configs_controller.rb
@@ -0,0 +1,23 @@
+module Enterprise::SuperAdmin::AppConfigsController
+ private
+
+ def allowed_configs
+ return super if ChatwootHub.pricing_plan == 'community'
+
+ case @config
+ when 'custom_branding'
+ @allowed_configs = %w[
+ LOGO_THUMBNAIL
+ LOGO
+ BRAND_NAME
+ INSTALLATION_NAME
+ BRAND_URL
+ WIDGET_BRAND_URL
+ TERMS_URL
+ PRIVACY_URL
+ ]
+ else
+ super
+ end
+ end
+end
diff --git a/enterprise/app/helpers/super_admin/features.yml b/enterprise/app/helpers/super_admin/features.yml
new file mode 100644
index 000000000..41e5af426
--- /dev/null
+++ b/enterprise/app/helpers/super_admin/features.yml
@@ -0,0 +1,66 @@
+custom_branding:
+ name: 'Custom Branding'
+ description: 'Apply your own branding to this installation.'
+ enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
+ icon: 'icon-paint-brush-line'
+ config_key: 'custom_branding'
+ enterprise: true
+agent_capacity:
+ name: 'Agent Capacity'
+ description: 'Set limits to auto-assigning conversations to your agents.'
+ enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
+ icon: 'icon-hourglass-line'
+ enterprise: true
+audit_logs:
+ name: 'Audit Logs'
+ description: 'Track and trace account activities with ease with detailed audit logs.'
+ enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
+ icon: 'icon-menu-search-line'
+ enterprise: true
+disable_branding:
+ name: 'Disable Branding'
+ description: 'Disable branding on live-chat widget and external emails.'
+ enabled: <%= (ChatwootHub.pricing_plan != 'community') %>
+ icon: 'icon-sailbot-fill'
+ enterprise: true
+live_chat:
+ name: 'Live Chat'
+ description: 'Improve your customer experience using a live chat on your website.'
+ enabled: true
+ icon: 'icon-chat-smile-3-line'
+email:
+ name: 'Email'
+ description: 'Manage your email customer interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-mail-send-fill'
+messenger:
+ name: 'Messenger'
+ description: 'Stay connected with your customers on Facebook & Instagram.'
+ enabled: true
+ icon: 'icon-messenger-line'
+ config_key: 'facebook'
+whatsapp:
+ name: 'WhatsApp'
+ description: 'Manage your WhatsApp business interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-whatsapp-line'
+telegram:
+ name: 'Telegram'
+ description: 'Manage your Telegram customer interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-telegram-line'
+line:
+ name: 'Line'
+ description: 'Manage your Line customer interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-line-line'
+sms:
+ name: 'SMS'
+ description: 'Manage your SMS customer interactions from Chatwoot.'
+ enabled: true
+ icon: 'icon-message-line'
+help_center:
+ name: 'Help Center'
+ description: 'Allow agents to create help center articles and publish them in a portal.'
+ enabled: true
+ icon: 'icon-book-2-line'
diff --git a/enterprise/app/helpers/super_admin/features_helper.rb b/enterprise/app/helpers/super_admin/features_helper.rb
new file mode 100644
index 000000000..2fbcd1715
--- /dev/null
+++ b/enterprise/app/helpers/super_admin/features_helper.rb
@@ -0,0 +1,16 @@
+module SuperAdmin::FeaturesHelper
+ def self.available_features
+ YAML.load(ERB.new(Rails.root.join('enterprise/app/helpers/super_admin/features.yml').read).result).with_indifferent_access
+ end
+
+ def self.plan_details
+ plan = ChatwootHub.pricing_plan
+ quantity = ChatwootHub.pricing_plan_quantity
+
+ if plan == 'premium'
+ "You are currently on the #{plan} plan with #{quantity} agents."
+ else
+ "You are currently on the #{plan} edition plan."
+ end
+ end
+end
diff --git a/enterprise/app/models/enterprise/message.rb b/enterprise/app/models/enterprise/message.rb
index 4f72d8c19..11c7044e5 100644
--- a/enterprise/app/models/enterprise/message.rb
+++ b/enterprise/app/models/enterprise/message.rb
@@ -1,5 +1,5 @@
module Enterprise::Message
def update_message_sentiments
- ::Enterprise::SentimentAnalysisJob.perform_later(self) if ENV.fetch('SENTIMENT_FILE_PATH', nil)
+ ::Enterprise::SentimentAnalysisJob.perform_later(self) if ENV.fetch('SENTIMENT_FILE_PATH', nil).present?
end
end
diff --git a/lib/chatwoot_hub.rb b/lib/chatwoot_hub.rb
index 68bae8267..6d28b10fa 100644
--- a/lib/chatwoot_hub.rb
+++ b/lib/chatwoot_hub.rb
@@ -4,6 +4,7 @@ class ChatwootHub
REGISTRATION_URL = "#{BASE_URL}/instances".freeze
PUSH_NOTIFICATION_URL = "#{BASE_URL}/send_push".freeze
EVENTS_URL = "#{BASE_URL}/events".freeze
+ BILLING_URL = "#{BASE_URL}/billing".freeze
def self.installation_identifier
identifier = InstallationConfig.find_by(name: 'INSTALLATION_IDENTIFIER')&.value
@@ -11,6 +12,26 @@ class ChatwootHub
identifier
end
+ def self.billing_url
+ "#{BILLING_URL}?installation_identifier=#{installation_identifier}"
+ end
+
+ def self.pricing_plan
+ InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN')&.value || 'community'
+ end
+
+ def self.pricing_plan_quantity
+ InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN_QUANTITY')&.value || 0
+ end
+
+ def self.support_config
+ {
+ support_website_token: InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN')&.value,
+ support_script_url: InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_SCRIPT_URL')&.value,
+ support_identifier_hash: InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH')&.value
+ }
+ end
+
def self.instance_config
{
installation_identifier: installation_identifier,
@@ -33,18 +54,18 @@ class ChatwootHub
}
end
- def self.latest_version
+ def self.sync_with_hub
begin
info = instance_config
info = info.merge(instance_metrics) unless ENV['DISABLE_TELEMETRY']
response = RestClient.post(PING_URL, info.to_json, { content_type: :json, accept: :json })
- version = JSON.parse(response)['version']
+ parsed_response = JSON.parse(response)
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
- version
+ parsed_response
end
def self.register_instance(company_name, owner_name, owner_email)
diff --git a/lib/events/types.rb b/lib/events/types.rb
index 2693f5216..6e34fc358 100644
--- a/lib/events/types.rb
+++ b/lib/events/types.rb
@@ -48,6 +48,7 @@ module Events::Types
# notification events
NOTIFICATION_CREATED = 'notification.created'
+ NOTIFICATION_DELETED = 'notification.deleted'
# agent events
AGENT_ADDED = 'agent.added'
diff --git a/lib/integrations/facebook/delivery_status.rb b/lib/integrations/facebook/delivery_status.rb
index d38ece092..1d6257bba 100644
--- a/lib/integrations/facebook/delivery_status.rb
+++ b/lib/integrations/facebook/delivery_status.rb
@@ -1,32 +1,37 @@
# frozen_string_literal: true
class Integrations::Facebook::DeliveryStatus
- def initialize(params)
- @params = params
- end
+ pattr_initialize [:params!]
def perform
- update_message_status
+ return if facebook_channel.blank?
+ return unless conversation
+
+ process_delivery_status if params.delivery_watermark
+ process_read_status if params.read_watermark
end
private
- def sender_id
- @params.sender['id']
+ def process_delivery_status
+ timestamp = Time.zone.at(params.delivery_watermark.to_i).to_datetime.utc
+ ::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, timestamp, :delivered)
+ end
+
+ def process_read_status
+ timestamp = Time.zone.at(params.read_watermark.to_i).to_datetime.utc
+ ::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, timestamp, :read)
end
def contact
- ::ContactInbox.find_by(source_id: sender_id)&.contact
+ ::ContactInbox.find_by(source_id: params.sender_id)&.contact
end
def conversation
@conversation ||= ::Conversation.find_by(contact_id: contact.id) if contact.present?
end
- def update_message_status
- return unless conversation
-
- conversation.contact_last_seen_at = @params.at
- conversation.save!
+ def facebook_channel
+ @facebook_channel ||= Channel::FacebookPage.find_by(page_id: params.recipient_id)
end
end
diff --git a/lib/integrations/facebook/message_parser.rb b/lib/integrations/facebook/message_parser.rb
index 123b521cb..275a46c39 100644
--- a/lib/integrations/facebook/message_parser.rb
+++ b/lib/integrations/facebook/message_parser.rb
@@ -34,6 +34,22 @@ class Integrations::Facebook::MessageParser
@messaging.dig('message', 'mid')
end
+ def delivery
+ @messaging['delivery']
+ end
+
+ def read
+ @messaging['read']
+ end
+
+ def read_watermark
+ read&.dig('watermark')
+ end
+
+ def delivery_watermark
+ delivery&.dig('watermark')
+ end
+
def echo?
@messaging.dig('message', 'is_echo')
end
diff --git a/package.json b/package.json
index f2efcdc3c..602d1e383 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
- "version": "3.3.1",
+ "version": "3.4.0",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
@@ -22,7 +22,7 @@
"size-limit": [
{
"path": "public/packs/js/widget-*.js",
- "limit": "275 KB"
+ "limit": "280 KB"
},
{
"path": "public/packs/js/sdk.js",
@@ -46,10 +46,11 @@
"@tailwindcss/typography": "^0.5.9",
"activestorage": "^5.2.6",
"autoprefixer": "^10.4.14",
- "axios": "^0.21.2",
+ "axios": "^1.6.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-vue-jsx": "^3.7.0",
"chart.js": "~2.9.4",
+ "color2k": "^2.0.2",
"company-email-validator": "^1.0.8",
"core-js": "3.11.0",
"date-fns": "2.21.1",
@@ -90,6 +91,7 @@
"vue-router": "~3.5.2",
"vue-template-compiler": "^2.7.0",
"vue-upload-component": "2.8.22",
+ "vue-virtual-scroll-list": "^2.3.5",
"vue2-datepicker": "^3.9.1",
"vuedraggable": "^2.24.3",
"vuelidate": "0.7.7",
@@ -167,4 +169,4 @@
"scss-lint"
]
}
-}
+}
\ No newline at end of file
diff --git a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb
index 70fcf9f42..918552406 100644
--- a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb
@@ -141,6 +141,28 @@ RSpec.describe 'Agent Bot API', type: :request do
expect(agent_bot.reload.name).not_to eq('test_updated')
expect(response.body).not_to include(global_bot.access_token.token)
end
+
+ it 'updates avatar' do
+ # no avatar before upload
+ expect(agent_bot.avatar.attached?).to be(false)
+ file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
+ patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
+ headers: admin.create_new_auth_token,
+ params: valid_params.merge(avatar: file)
+
+ expect(response).to have_http_status(:success)
+ agent_bot.reload
+ expect(agent_bot.avatar.attached?).to be(true)
+ end
+
+ it 'updated avatar with avatar_url' do
+ patch "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
+ headers: admin.create_new_auth_token,
+ params: valid_params.merge(avatar_url: 'http://example.com/avatar.png'),
+ as: :json
+ expect(response).to have_http_status(:success)
+ expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(agent_bot, 'http://example.com/avatar.png')
+ end
end
end
@@ -183,4 +205,29 @@ RSpec.describe 'Agent Bot API', type: :request do
end
end
end
+
+ describe 'DELETE /api/v1/accounts/{account.id}/agent_bots/:id/avatar' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ before do
+ agent_bot.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
+ end
+
+ it 'delete agent_bot avatar' do
+ delete "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/avatar",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect { agent_bot.avatar.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(response).to have_http_status(:success)
+ end
+ end
+ end
end
diff --git a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
index dc9de8c35..8f7a5fea0 100644
--- a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
@@ -234,4 +234,49 @@ RSpec.describe 'Conversation Messages API', type: :request do
end
end
end
+
+ describe 'POST /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id/retry' do
+ let(:message) { create(:message, account: account, status: :failed, content_attributes: { external_error: 'error' }) }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/#{message.id}/retry"
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user with access to conversation' do
+ let(:agent) { create(:user, account: account, role: :agent) }
+
+ before do
+ create(:inbox_member, inbox: message.conversation.inbox, user: agent)
+ end
+
+ it 'retries the message' do
+ post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/#{message.id}/retry",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(message.reload.status).to eq('sent')
+ expect(message.reload.content_attributes['external_error']).to be_nil
+ end
+ end
+
+ context 'when the message id is invalid' do
+ let(:agent) { create(:user, account: account, role: :agent) }
+
+ before do
+ create(:inbox_member, inbox: message.conversation.inbox, user: agent)
+ end
+
+ it 'returns not found error' do
+ post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/99999/retry",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+ end
end
diff --git a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
index 99baf1b25..cadc212d4 100644
--- a/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/notifications_controller_spec.rb
@@ -127,4 +127,58 @@ RSpec.describe 'Notifications API', type: :request do
end
end
end
+
+ describe 'DELETE /api/v1/accounts/{account.id}/notifications/:id' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let!(:notification) { create(:notification, account: account, user: admin) }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ delete "/api/v1/accounts/#{account.id}/notifications/#{notification.id}"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+
+ it 'deletes the notification' do
+ delete "/api/v1/accounts/#{account.id}/notifications/#{notification.id}",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(Notification.count).to eq(0)
+ end
+ end
+ end
+
+ describe 'PATCH /api/v1/accounts/{account.id}/notifications/:id/snooze' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let!(:notification) { create(:notification, account: account, user: admin) }
+
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/snooze",
+ params: { snoozed_until: DateTime.now.utc + 1.day }
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+
+ it 'updates the notification snoozed until' do
+ post "/api/v1/accounts/#{account.id}/notifications/#{notification.id}/snooze",
+ headers: admin.create_new_auth_token,
+ params: { snoozed_until: DateTime.now.utc + 1.day },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(notification.reload.snoozed_until).not_to eq('')
+ end
+ end
+ end
end
diff --git a/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb b/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb
index 030d694ff..ae3ef61e5 100644
--- a/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb
+++ b/spec/controllers/platform/api/v1/agent_bots_controller_spec.rb
@@ -140,6 +140,26 @@ RSpec.describe 'Platform Agent Bot API', type: :request do
data = response.parsed_body
expect(data['name']).to eq('test123')
end
+
+ it 'updates avatar' do
+ # no avatar before upload
+ create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
+ expect(agent_bot.avatar.attached?).to be(false)
+ file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
+ patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test123' }.merge(avatar: file),
+ headers: { api_access_token: platform_app.access_token.token }
+ expect(response).to have_http_status(:success)
+ agent_bot.reload
+ expect(agent_bot.avatar.attached?).to be(true)
+ end
+
+ it 'updated avatar with avatar_url' do
+ create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
+ patch "/platform/api/v1/agent_bots/#{agent_bot.id}", params: { name: 'test123' }.merge(avatar_url: 'http://example.com/avatar.png'),
+ headers: { api_access_token: platform_app.access_token.token }
+ expect(response).to have_http_status(:success)
+ expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(agent_bot, 'http://example.com/avatar.png')
+ end
end
end
@@ -169,4 +189,32 @@ RSpec.describe 'Platform Agent Bot API', type: :request do
end
end
end
+
+ describe 'DELETE /platform/api/v1/agent_bots/{agent_bot_id}/avatar' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ delete "/platform/api/v1/agent_bots/#{agent_bot.id}/avatar"
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:platform_app) { create(:platform_app) }
+
+ before do
+ agent_bot.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
+ create(:platform_app_permissible, platform_app: platform_app, permissible: agent_bot)
+ end
+
+ it 'delete agent_bot avatar' do
+ delete "/platform/api/v1/agent_bots/#{agent_bot.id}/avatar",
+ headers: { api_access_token: platform_app.access_token.token },
+ as: :json
+
+ expect { agent_bot.avatar.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(response).to have_http_status(:success)
+ end
+ end
+ end
end
diff --git a/spec/controllers/super_admin/app_config_controller_spec.rb b/spec/controllers/super_admin/app_config_controller_spec.rb
index 3d6df46a8..338c3e611 100644
--- a/spec/controllers/super_admin/app_config_controller_spec.rb
+++ b/spec/controllers/super_admin/app_config_controller_spec.rb
@@ -34,13 +34,13 @@ RSpec.describe 'Super Admin Application Config API', type: :request do
context 'when it is an aunthenticated super admin' do
it 'shows the app_config page' do
sign_in(super_admin, scope: :super_admin)
- post '/super_admin/app_config', params: { app_config: { TESTKEY: 'TESTVALUE' } }
+ post '/super_admin/app_config', params: { app_config: { FB_APP_ID: 'FB_APP_ID' } }
expect(response).to have_http_status(:found)
- expect(response).to redirect_to(super_admin_app_config_path)
+ expect(response).to redirect_to(super_admin_settings_path)
- config = GlobalConfig.get('TESTKEY')
- expect(config['TESTKEY']).to eq('TESTVALUE')
+ config = GlobalConfig.get('FB_APP_ID')
+ expect(config['FB_APP_ID']).to eq('FB_APP_ID')
end
end
end
diff --git a/spec/controllers/super_admin/instance_statuses_controller_spec.rb b/spec/controllers/super_admin/instance_statuses_controller_spec.rb
index b2b1237f9..c00ea5984 100644
--- a/spec/controllers/super_admin/instance_statuses_controller_spec.rb
+++ b/spec/controllers/super_admin/instance_statuses_controller_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-RSpec.describe 'Super Admin Instance health', type: :request do
+RSpec.describe 'Super Admin Instance status', type: :request do
let(:super_admin) { create(:super_admin) }
describe 'GET /super_admin/instance_status' do
diff --git a/spec/drops/contact_drop_spec.rb b/spec/drops/contact_drop_spec.rb
index e83ebb377..d00a0924d 100644
--- a/spec/drops/contact_drop_spec.rb
+++ b/spec/drops/contact_drop_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe ContactDrop do
subject(:contact_drop) { described_class.new(contact) }
- let!(:contact) { create(:contact) }
+ let!(:contact) { create(:contact, custom_attributes: { car_model: 'Tesla Model S', car_year: '2022' }) }
context 'when first name' do
it 'returns first name' do
@@ -38,4 +38,19 @@ describe ContactDrop do
expect(subject.last_name).to eq 'Doe'
end
end
+
+ context 'when accessing custom attributes' do
+ it 'returns the correct car model from custom attributes' do
+ expect(contact_drop.custom_attribute['car_model']).to eq 'Tesla Model S'
+ end
+
+ it 'returns the correct car year from custom attributes' do
+ expect(contact_drop.custom_attribute['car_year']).to eq '2022'
+ end
+
+ it 'returns empty hash when there are no custom attributes' do
+ contact.update!(custom_attributes: nil)
+ expect(contact_drop.custom_attribute).to eq({})
+ end
+ end
end
diff --git a/spec/factories/facebook_message/incoming_fb_text_message.rb b/spec/factories/facebook_message/incoming_fb_text_message.rb
index 83d516f09..dc75a912c 100644
--- a/spec/factories/facebook_message/incoming_fb_text_message.rb
+++ b/spec/factories/facebook_message/incoming_fb_text_message.rb
@@ -10,4 +10,24 @@ FactoryBot.define do
initialize_with { attributes }
end
+
+ factory :message_deliveries, class: Hash do
+ messaging do
+ { sender: { id: '3383290475046708' },
+ recipient: { id: '117172741761305' },
+ delivery: { watermark: '1648581633369' } }
+ end
+
+ initialize_with { attributes }
+ end
+
+ factory :message_reads, class: Hash do
+ messaging do
+ { sender: { id: '3383290475046708' },
+ recipient: { id: '117172741761305' },
+ read: { watermark: '1648581633369' } }
+ end
+
+ initialize_with { attributes }
+ end
end
diff --git a/spec/helpers/portal_helper_spec.rb b/spec/helpers/portal_helper_spec.rb
index 4611e4a6f..84ed4ba72 100644
--- a/spec/helpers/portal_helper_spec.rb
+++ b/spec/helpers/portal_helper_spec.rb
@@ -33,71 +33,175 @@ describe PortalHelper do
describe '#generate_portal_bg' do
context 'when theme is dark' do
it 'returns the correct background with dark grid image and color mix with black' do
- expected_bg = 'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #ff0000 20%, black)'
+ expected_bg = 'url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #ff0000 20%, black)'
expect(helper.generate_portal_bg('#ff0000', 'dark')).to eq(expected_bg)
end
end
context 'when theme is not dark' do
it 'returns the correct background with light grid image and color mix with white' do
- expected_bg = 'background: url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #ff0000 20%, white)'
+ expected_bg = 'url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #ff0000 20%, white)'
expect(helper.generate_portal_bg('#ff0000', 'light')).to eq(expected_bg)
end
end
context 'when provided with various colors' do
it 'adjusts the background appropriately for dark theme' do
- expected_bg = 'background: url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #00ff00 20%, black)'
+ expected_bg = 'url(/assets/images/hc/hexagon-dark.svg) color-mix(in srgb, #00ff00 20%, black)'
expect(helper.generate_portal_bg('#00ff00', 'dark')).to eq(expected_bg)
end
it 'adjusts the background appropriately for light theme' do
- expected_bg = 'background: url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #0000ff 20%, white)'
+ expected_bg = 'url(/assets/images/hc/hexagon-light.svg) color-mix(in srgb, #0000ff 20%, white)'
expect(helper.generate_portal_bg('#0000ff', 'light')).to eq(expected_bg)
end
end
end
- describe '#get_theme_names' do
- it 'returns the light theme name' do
- expect(helper.get_theme_names('light')).to eq(I18n.t('public_portal.header.appearance.light'))
- end
-
- it 'returns the dark theme name' do
- expect(helper.get_theme_names('dark')).to eq(I18n.t('public_portal.header.appearance.dark'))
- end
-
- it 'returns the system theme name for any other value' do
- expect(helper.get_theme_names('any_other_value')).to eq(I18n.t('public_portal.header.appearance.system'))
- end
- end
-
- describe '#get_theme_icon' do
- it 'returns the light theme icon' do
- expect(helper.get_theme_icon('light')).to eq('icons/sun')
- end
-
- it 'returns the dark theme icon' do
- expect(helper.get_theme_icon('dark')).to eq('icons/moon')
- end
-
- it 'returns the system theme icon for any other value' do
- expect(helper.get_theme_icon('any_other_value')).to eq('icons/monitor')
- end
- end
-
describe '#generate_gradient_to_bottom' do
context 'when theme is dark' do
- it 'returns the correct background gradient' do
- expected_gradient = 'background-image: linear-gradient(to bottom, transparent, #151718)'
- expect(helper.generate_gradient_to_bottom('dark')).to eq(expected_gradient)
+ it 'returns the correct gradient' do
+ expect(helper.generate_gradient_to_bottom('dark')).to eq(
+ 'linear-gradient(to bottom, transparent, #151718)'
+ )
end
end
context 'when theme is not dark' do
- it 'returns the correct background gradient' do
- expected_gradient = 'background-image: linear-gradient(to bottom, transparent, white)'
- expect(helper.generate_gradient_to_bottom('light')).to eq(expected_gradient)
+ it 'returns the correct gradient' do
+ expect(helper.generate_gradient_to_bottom('light')).to eq(
+ 'linear-gradient(to bottom, transparent, white)'
+ )
+ end
+ end
+
+ context 'when provided with various colors' do
+ it 'adjusts the gradient appropriately' do
+ expect(helper.generate_gradient_to_bottom('dark')).to eq(
+ 'linear-gradient(to bottom, transparent, #151718)'
+ )
+ expect(helper.generate_gradient_to_bottom('light')).to eq(
+ 'linear-gradient(to bottom, transparent, white)'
+ )
+ end
+ end
+ end
+
+ describe '#generate_portal_hover_color' do
+ context 'when theme is dark' do
+ it 'returns the correct color mix with #1B1B1B' do
+ expect(helper.generate_portal_hover_color('#ff0000', 'dark')).to eq(
+ 'color-mix(in srgb, #ff0000 5%, #1B1B1B)'
+ )
+ end
+ end
+
+ context 'when theme is not dark' do
+ it 'returns the correct color mix with #F9F9F9' do
+ expect(helper.generate_portal_hover_color('#ff0000', 'light')).to eq(
+ 'color-mix(in srgb, #ff0000 5%, #F9F9F9)'
+ )
+ end
+ end
+
+ context 'when provided with various colors' do
+ it 'adjusts the color mix appropriately' do
+ expect(helper.generate_portal_hover_color('#00ff00', 'dark')).to eq(
+ 'color-mix(in srgb, #00ff00 5%, #1B1B1B)'
+ )
+ expect(helper.generate_portal_hover_color('#0000ff', 'light')).to eq(
+ 'color-mix(in srgb, #0000ff 5%, #F9F9F9)'
+ )
+ end
+ end
+ end
+
+ describe '#theme_query_string' do
+ context 'when theme is present and not system' do
+ it 'returns the correct query string' do
+ expect(helper.theme_query_string('dark')).to eq('?theme=dark')
+ end
+ end
+
+ context 'when theme is not present' do
+ it 'returns the correct query string' do
+ expect(helper.theme_query_string(nil)).to eq('')
+ end
+ end
+
+ context 'when theme is system' do
+ it 'returns the correct query string' do
+ expect(helper.theme_query_string('system')).to eq('')
+ end
+ end
+ end
+
+ describe '#generate_home_link' do
+ context 'when theme is not present' do
+ it 'returns the correct link' do
+ expect(helper.generate_home_link('portal_slug', 'en', nil, true)).to eq(
+ '/hc/portal_slug/en'
+ )
+ end
+ end
+
+ context 'when theme is present and plain layout is enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_home_link('portal_slug', 'en', 'dark', true)).to eq(
+ '/hc/portal_slug/en?theme=dark'
+ )
+ end
+ end
+
+ context 'when plain layout is not enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_home_link('portal_slug', 'en', 'dark', false)).to eq(
+ '/hc/portal_slug/en'
+ )
+ end
+ end
+ end
+
+ describe '#generate_category_link' do
+ context 'when theme is not present' do
+ it 'returns the correct link' do
+ expect(helper.generate_category_link(
+ portal_slug: 'portal_slug',
+ category_locale: 'en',
+ category_slug: 'category_slug',
+ theme: nil,
+ is_plain_layout_enabled: true
+ )).to eq(
+ '/hc/portal_slug/en/categories/category_slug'
+ )
+ end
+ end
+
+ context 'when theme is present and plain layout is enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_category_link(
+ portal_slug: 'portal_slug',
+ category_locale: 'en',
+ category_slug: 'category_slug',
+ theme: 'dark',
+ is_plain_layout_enabled: true
+ )).to eq(
+ '/hc/portal_slug/en/categories/category_slug?theme=dark'
+ )
+ end
+ end
+
+ context 'when plain layout is not enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_category_link(
+ portal_slug: 'portal_slug',
+ category_locale: 'en',
+ category_slug: 'category_slug',
+ theme: 'dark',
+ is_plain_layout_enabled: false
+ )).to eq(
+ '/hc/portal_slug/en/categories/category_slug'
+ )
end
end
end
@@ -105,19 +209,27 @@ describe PortalHelper do
describe '#generate_article_link' do
context 'when theme is not present' do
it 'returns the correct link' do
- expect(helper.generate_article_link('portal_slug', 'article_slug', nil)).to eq(
+ expect(helper.generate_article_link('portal_slug', 'article_slug', nil, true)).to eq(
'/hc/portal_slug/articles/article_slug'
)
end
end
- context 'when theme is present' do
+ context 'when theme is present and plain layout is enabled' do
it 'returns the correct link' do
- expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark')).to eq(
+ expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark', true)).to eq(
'/hc/portal_slug/articles/article_slug?theme=dark'
)
end
end
+
+ context 'when plain layout is not enabled' do
+ it 'returns the correct link' do
+ expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark', false)).to eq(
+ '/hc/portal_slug/articles/article_slug'
+ )
+ end
+ end
end
describe '#render_category_content' do
@@ -134,4 +246,16 @@ describe PortalHelper do
expect(helper.render_category_content(markdown_content)).to eq(plain_text_content)
end
end
+
+ describe '#thumbnail_bg_color' do
+ it 'returns the correct color based on username length' do
+ expect(helper.thumbnail_bg_color('')).to be_in(['#6D95BA', '#A4C3C3', '#E19191'])
+ expect(helper.thumbnail_bg_color('Joe')).to eq('#6D95BA') # Length 3, so index is 0
+ expect(helper.thumbnail_bg_color('John')).to eq('#A4C3C3') # Length 4, so index is 1
+ expect(helper.thumbnail_bg_color('Jane james')).to eq('#A4C3C3') # Length 10, so index is 1
+ expect(helper.thumbnail_bg_color('Jane_123')).to eq('#E19191') # Length 8, so index is 2
+ expect(helper.thumbnail_bg_color('AlexanderTheGreat')).to eq('#E19191') # Length 17, so index is 2
+ expect(helper.thumbnail_bg_color('Reginald John Sans')).to eq('#6D95BA') # Length 18, so index is 0
+ end
+ end
end
diff --git a/spec/jobs/internal/check_new_versions_job_spec.rb b/spec/jobs/internal/check_new_versions_job_spec.rb
index 3253ce319..3d36c4ba7 100644
--- a/spec/jobs/internal/check_new_versions_job_spec.rb
+++ b/spec/jobs/internal/check_new_versions_job_spec.rb
@@ -4,11 +4,11 @@ RSpec.describe Internal::CheckNewVersionsJob do
subject(:job) { described_class.perform_now }
it 'updates the latest chatwoot version in redis' do
- version = '1.1.1'
+ data = { 'version' => '1.2.3' }.to_json
allow(Rails.env).to receive(:production?).and_return(true)
- allow(ChatwootHub).to receive(:latest_version).and_return(version)
+ allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
job
- expect(ChatwootHub).to have_received(:latest_version)
- expect(Redis::Alfred.get(Redis::Alfred::LATEST_CHATWOOT_VERSION)).to eq version
+ expect(ChatwootHub).to have_received(:sync_with_hub)
+ expect(Redis::Alfred.get(Redis::Alfred::LATEST_CHATWOOT_VERSION)).to eq data['version']
end
end
diff --git a/spec/jobs/migration/remove_message_notifications_spec.rb b/spec/jobs/migration/remove_message_notifications_spec.rb
new file mode 100644
index 000000000..9c90e1160
--- /dev/null
+++ b/spec/jobs/migration/remove_message_notifications_spec.rb
@@ -0,0 +1,10 @@
+require 'rails_helper'
+
+RSpec.describe Migration::RemoveMessageNotifications do
+ subject(:job) { described_class.perform_later }
+
+ it 'enqueues the job' do
+ expect { job }.to have_enqueued_job(described_class)
+ .on_queue('scheduled_jobs')
+ end
+end
diff --git a/spec/jobs/notification/remove_duplicate_notification_job_spec.rb b/spec/jobs/notification/remove_duplicate_notification_job_spec.rb
new file mode 100644
index 000000000..c48524e1a
--- /dev/null
+++ b/spec/jobs/notification/remove_duplicate_notification_job_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+RSpec.describe Notification::RemoveDuplicateNotificationJob do
+ let(:user) { create(:user) }
+ let(:conversation) { create(:conversation) }
+
+ it 'enqueues the job' do
+ duplicate_notification = create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
+ expect do
+ described_class.perform_later(duplicate_notification)
+ end.to have_enqueued_job(described_class)
+ .on_queue('default')
+ end
+
+ it 'removes duplicate notifications' do
+ create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
+ duplicate_notification = create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
+
+ described_class.perform_now(duplicate_notification)
+ expect(Notification.count).to eq(1)
+ end
+end
diff --git a/spec/jobs/webhooks/facebook_delivery_job_spec.rb b/spec/jobs/webhooks/facebook_delivery_job_spec.rb
new file mode 100644
index 000000000..8dc91b9ab
--- /dev/null
+++ b/spec/jobs/webhooks/facebook_delivery_job_spec.rb
@@ -0,0 +1,44 @@
+require 'rails_helper'
+
+RSpec.describe Webhooks::FacebookDeliveryJob do
+ include ActiveJob::TestHelper
+
+ let(:message) { 'test_message' }
+ let(:parsed_message) { instance_double(Integrations::Facebook::MessageParser) }
+ let(:delivery_status) { instance_double(Integrations::Facebook::DeliveryStatus) }
+
+ before do
+ allow(Integrations::Facebook::MessageParser).to receive(:new).with(message).and_return(parsed_message)
+ allow(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message).and_return(delivery_status)
+ allow(delivery_status).to receive(:perform)
+ end
+
+ after do
+ clear_enqueued_jobs
+ end
+
+ describe '#perform_later' do
+ it 'enqueues the job' do
+ expect do
+ described_class.perform_later(message)
+ end.to have_enqueued_job(described_class).with(message).on_queue('low')
+ end
+ end
+
+ describe '#perform' do
+ it 'calls the MessageParser with the correct argument' do
+ expect(Integrations::Facebook::MessageParser).to receive(:new).with(message)
+ described_class.perform_now(message)
+ end
+
+ it 'calls the DeliveryStatus with the correct argument' do
+ expect(Integrations::Facebook::DeliveryStatus).to receive(:new).with(params: parsed_message)
+ described_class.perform_now(message)
+ end
+
+ it 'executes perform on the DeliveryStatus instance' do
+ expect(delivery_status).to receive(:perform)
+ described_class.perform_now(message)
+ end
+ end
+end
diff --git a/spec/lib/chatwoot_hub_spec.rb b/spec/lib/chatwoot_hub_spec.rb
index 99eaf19da..b5e0da4dd 100644
--- a/spec/lib/chatwoot_hub_spec.rb
+++ b/spec/lib/chatwoot_hub_spec.rb
@@ -7,11 +7,11 @@ describe ChatwootHub do
expect(described_class.installation_identifier).to eq installation_identifier
end
- context 'when fetching latest_version' do
+ context 'when fetching sync_with_hub' do
it 'get latest version from chatwoot hub' do
version = '1.1.1'
allow(RestClient).to receive(:post).and_return({ version: version }.to_json)
- expect(described_class.latest_version).to eq version
+ expect(described_class.sync_with_hub['version']).to eq version
expect(RestClient).to have_received(:post).with(described_class::PING_URL, described_class.instance_config
.merge(described_class.instance_metrics).to_json, { content_type: :json, accept: :json })
end
@@ -20,7 +20,7 @@ describe ChatwootHub do
version = '1.1.1'
with_modified_env DISABLE_TELEMETRY: 'true' do
allow(RestClient).to receive(:post).and_return({ version: version }.to_json)
- expect(described_class.latest_version).to eq version
+ expect(described_class.sync_with_hub['version']).to eq version
expect(RestClient).to have_received(:post).with(described_class::PING_URL,
described_class.instance_config.to_json, { content_type: :json, accept: :json })
end
@@ -28,7 +28,7 @@ describe ChatwootHub do
it 'returns nil when chatwoot hub is down' do
allow(RestClient).to receive(:post).and_raise(ExceptionList::REST_CLIENT_EXCEPTIONS.sample)
- expect(described_class.latest_version).to be_nil
+ expect(described_class.sync_with_hub).to be_nil
end
end
diff --git a/spec/lib/integrations/facebook/delivery_status_spec.rb b/spec/lib/integrations/facebook/delivery_status_spec.rb
new file mode 100644
index 000000000..28f1fe982
--- /dev/null
+++ b/spec/lib/integrations/facebook/delivery_status_spec.rb
@@ -0,0 +1,83 @@
+require 'rails_helper'
+
+describe Integrations::Facebook::DeliveryStatus do
+ subject(:message_builder) { described_class.new(message_deliveries, facebook_channel.inbox).perform }
+
+ before do
+ stub_request(:post, /graph\.facebook\.com/)
+ end
+
+ let!(:account) { create(:account) }
+ let!(:facebook_channel) { create(:channel_facebook_page, page_id: '117172741761305') }
+ let!(:message_delivery_object) { build(:message_deliveries).to_json }
+ let!(:message_deliveries) { Integrations::Facebook::MessageParser.new(message_delivery_object) }
+
+ let!(:contact) { create(:contact, account: account) }
+ let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: facebook_channel.inbox, source_id: '3383290475046708') }
+ let!(:conversation) { create(:conversation, inbox: facebook_channel.inbox, contact: contact, contact_inbox: contact_inbox) }
+
+ let!(:message_read_object) { build(:message_reads).to_json }
+ let!(:message_reads) { Integrations::Facebook::MessageParser.new(message_read_object) }
+ let!(:message1) do
+ create(:message, content: 'facebook message', message_type: 'outgoing', inbox: facebook_channel.inbox, conversation: conversation)
+ end
+ let!(:message2) do
+ create(:message, content: 'facebook message', message_type: 'incoming', inbox: facebook_channel.inbox, conversation: conversation)
+ end
+
+ describe '#perform' do
+ context 'when message_deliveries callback fires' do
+ before do
+ allow(Conversations::UpdateMessageStatusJob).to receive(:perform_later)
+ end
+
+ it 'updates all messages if the status is delivered' do
+ described_class.new(params: message_deliveries).perform
+ expect(Conversations::UpdateMessageStatusJob).to have_received(:perform_later).with(
+ message1.conversation.id,
+ Time.zone.at(message_deliveries.delivery['watermark'].to_i).to_datetime,
+ :delivered
+ )
+ end
+
+ it 'does not update the message status if the message is incoming' do
+ described_class.new(params: message_deliveries).perform
+ expect(message2.reload.status).to eq('sent')
+ end
+
+ it 'does not update the message status if the message was created after the watermark' do
+ message1.update(created_at: 1.day.from_now)
+ message_deliveries.delivery['watermark'] = 1.day.ago.to_i
+ described_class.new(params: message_deliveries).perform
+ expect(message1.reload.status).to eq('sent')
+ end
+ end
+
+ context 'when message_reads callback fires' do
+ before do
+ allow(Conversations::UpdateMessageStatusJob).to receive(:perform_later)
+ end
+
+ it 'updates all messages if the status is read' do
+ described_class.new(params: message_reads).perform
+ expect(Conversations::UpdateMessageStatusJob).to have_received(:perform_later).with(
+ message1.conversation.id,
+ Time.zone.at(message_reads.read['watermark'].to_i).to_datetime,
+ :read
+ )
+ end
+
+ it 'does not update the message status if the message is incoming' do
+ described_class.new(params: message_reads).perform
+ expect(message2.reload.status).to eq('sent')
+ end
+
+ it 'does not update the message status if the message was created after the watermark' do
+ message1.update(created_at: 1.day.from_now)
+ message_reads.read['watermark'] = 1.day.ago.to_i
+ described_class.new(params: message_reads).perform
+ expect(message1.reload.status).to eq('sent')
+ end
+ end
+ end
+end
diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb
index 276fadbf9..6275c8dc3 100644
--- a/spec/listeners/action_cable_listener_spec.rb
+++ b/spec/listeners/action_cable_listener_spec.rb
@@ -131,6 +131,27 @@ describe ActionCableListener do
end
end
+ describe '#notification_deleted' do
+ let(:event_name) { :'notification.deleted' }
+ let!(:notification) { create(:notification, account: account, user: agent) }
+ let!(:event) { Events::Base.new(event_name, Time.zone.now, notification: notification) }
+
+ it 'sends message to account admins, inbox agents' do
+ expect(ActionCableBroadcastJob).to receive(:perform_later).with(
+ [agent.pubsub_token],
+ 'notification.deleted',
+ {
+ account_id: notification.account_id,
+ notification: notification.push_event_data,
+ unread_count: 1,
+ count: 1
+ }
+ )
+
+ listener.notification_deleted(event)
+ end
+ end
+
describe '#conversation_updated' do
let(:event_name) { :'conversation.updated' }
let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: agent, is_private: false) }
diff --git a/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb b/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
index 9faabc3af..5661af714 100644
--- a/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
+++ b/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
end
describe 'conversation_creation' do
- let(:mail) { described_class.with(account: account).conversation_creation(conversation, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).conversation_creation(conversation, agent, nil).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation
@@ -27,7 +27,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
end
describe 'conversation_assignment' do
- let(:mail) { described_class.with(account: account).conversation_assignment(conversation, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).conversation_assignment(conversation, agent, nil).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, A new conversation [ID - #{conversation.display_id}] has been assigned to you.")
@@ -42,7 +42,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
let(:contact) { create(:contact, name: nil, account: account) }
let(:another_agent) { create(:user, email: 'agent2@example.com', account: account) }
let(:message) { create(:message, conversation: conversation, account: account, sender: another_agent) }
- let(:mail) { described_class.with(account: account).conversation_mention(message, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).conversation_mention(conversation, agent, message).deliver_now }
let(:contact_inbox) { create(:contact_inbox, account: account, inbox: conversation.inbox) }
before do
@@ -72,7 +72,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
describe 'assigned_conversation_new_message' do
let(:message) { create(:message, conversation: conversation, account: account) }
- let(:mail) { described_class.with(account: account).assigned_conversation_new_message(message, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).assigned_conversation_new_message(conversation, agent, message).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, New message in your assigned conversation [ID - #{message.conversation.display_id}].")
@@ -90,7 +90,7 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer do
describe 'participating_conversation_new_message' do
let(:message) { create(:message, conversation: conversation, account: account) }
- let(:mail) { described_class.with(account: account).participating_conversation_new_message(message, agent).deliver_now }
+ let(:mail) { described_class.with(account: account).participating_conversation_new_message(conversation, agent, message).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("#{agent.available_name}, New message in your participating conversation [ID - #{message.conversation.display_id}].")
diff --git a/spec/models/concerns/assignment_handler_shared.rb b/spec/models/concerns/assignment_handler_shared.rb
index 55951cd99..8447b8146 100644
--- a/spec/models/concerns/assignment_handler_shared.rb
+++ b/spec/models/concerns/assignment_handler_shared.rb
@@ -63,47 +63,4 @@ shared_examples_for 'assignment_handler' do
end
end
end
-
- describe '#update_assignee' do
- subject(:update_assignee) { conversation.update_assignee(agent) }
-
- let(:conversation) { create(:conversation, assignee: nil) }
- let(:agent) do
- create(:user, email: 'agent@example.com', account: conversation.account, role: :agent)
- end
- let(:assignment_mailer) { instance_double(AgentNotifications::ConversationNotificationsMailer, deliver: true) }
-
- before do
- create(:inbox_member, user: agent, inbox: conversation.inbox)
- end
-
- it 'assigns the agent to conversation' do
- expect(update_assignee).to be(true)
- expect(conversation.reload.assignee).to eq(agent)
- end
-
- it 'dispaches assignee changed event' do
- # TODO: FIX me
- # expect(EventDispatcherJob).to(have_been_enqueued.at_least(:once).with('assignee.changed', anything, anything, anything, anything))
- expect(EventDispatcherJob).to(have_been_enqueued.at_least(:once))
- expect(update_assignee).to be(true)
- end
-
- it 'adds assignee to conversation participants' do
- expect { update_assignee }.to change { conversation.conversation_participants.count }.by(1)
- end
-
- context 'when agent is current user' do
- before do
- Current.user = agent
- end
-
- it 'creates self-assigned message activity' do
- expect(update_assignee).to be(true)
- expect(Conversations::ActivityMessageJob).to(have_been_enqueued.at_least(:once)
- .with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id,
- message_type: :activity, content: "#{agent.name} self-assigned this conversation" }))
- end
- end
- end
end
diff --git a/spec/models/concerns/liquidable_shared.rb b/spec/models/concerns/liquidable_shared.rb
index 7648bd15c..8df526a2f 100644
--- a/spec/models/concerns/liquidable_shared.rb
+++ b/spec/models/concerns/liquidable_shared.rb
@@ -2,8 +2,8 @@ require 'rails_helper'
shared_examples_for 'liqudable' do
context 'when liquid is present in content' do
- let(:contact) { create(:contact, name: 'john', phone_number: '+912883') }
- let(:conversation) { create(:conversation, id: 1, contact: contact) }
+ let(:contact) { create(:contact, name: 'john', phone_number: '+912883', custom_attributes: { customer_type: 'platinum' }) }
+ let(:conversation) { create(:conversation, id: 1, contact: contact, custom_attributes: { priority: 'high' }) }
context 'when message is incoming' do
let(:message) { build(:message, conversation: conversation, message_type: 'incoming') }
@@ -24,6 +24,14 @@ shared_examples_for 'liqudable' do
expect(message.content).to eq 'hey John how are you?'
end
+ it 'set replaces liquid custom attributes in message' do
+ message.content = 'Are you a {{contact.custom_attribute.customer_type}} customer,
+ If yes then the priority is {{conversation.custom_attribute.priority}}'
+ message.save!
+ expect(message.content).to eq 'Are you a platinum customer,
+ If yes then the priority is high'
+ end
+
it 'process liquid operators like default value' do
message.content = 'Can we send you an email at {{ contact.email | default: "default" }} ?'
message.save!
diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb
index 0a04cd73b..82dac1979 100644
--- a/spec/models/conversation_spec.rb
+++ b/spec/models/conversation_spec.rb
@@ -715,61 +715,52 @@ RSpec.describe Conversation do
end
end
- describe 'Custom Sort' do
+ describe 'custom sort option' do
include ActiveJob::TestHelper
- let!(:conversation_7) { create(:conversation, created_at: DateTime.now - 10.days, last_activity_at: DateTime.now - 13.days) }
- let!(:conversation_6) { create(:conversation, created_at: DateTime.now - 10.days, last_activity_at: DateTime.now - 10.days) }
- let!(:conversation_5) { create(:conversation, created_at: DateTime.now - 10.days, last_activity_at: DateTime.now - 12.days, priority: :urgent) }
- let!(:conversation_4) { create(:conversation, created_at: DateTime.now - 10.days, last_activity_at: DateTime.now - 10.days, priority: :urgent) }
- let!(:conversation_3) { create(:conversation, created_at: DateTime.now - 9.days, last_activity_at: DateTime.now - 9.days, priority: :low) }
- let!(:conversation_2) { create(:conversation, created_at: DateTime.now - 6.days, last_activity_at: DateTime.now - 6.days, priority: :high) }
- let!(:conversation_1) { create(:conversation, created_at: DateTime.now - 8.days, last_activity_at: DateTime.now - 8.days, priority: :medium) }
+ let!(:conversation_7) { create(:conversation, created_at: DateTime.now - 6.days, last_activity_at: DateTime.now - 13.days) }
+ let!(:conversation_6) { create(:conversation, created_at: DateTime.now - 7.days, last_activity_at: DateTime.now - 10.days) }
+ let!(:conversation_5) { create(:conversation, created_at: DateTime.now - 8.days, last_activity_at: DateTime.now - 12.days, priority: :urgent) }
+ let!(:conversation_4) { create(:conversation, created_at: DateTime.now - 9.days, last_activity_at: DateTime.now - 11.days, priority: :urgent) }
+ let!(:conversation_3) { create(:conversation, created_at: DateTime.now - 5.days, last_activity_at: DateTime.now - 9.days, priority: :low) }
+ let!(:conversation_2) { create(:conversation, created_at: DateTime.now - 3.days, last_activity_at: DateTime.now - 6.days, priority: :high) }
+ let!(:conversation_1) { create(:conversation, created_at: DateTime.now - 4.days, last_activity_at: DateTime.now - 8.days, priority: :medium) }
- it 'Sort conversations based on created_at' do
- records = described_class.sort_on_created_at
- expect(records.first.id).to eq(conversation_7.id)
- expect(records.last.id).to eq(conversation_2.id)
+ describe 'sort_on_created_at' do
+ let(:created_desc_order) do
+ [
+ conversation_2.id, conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id,
+ conversation_5.id, conversation_4.id
+ ]
+ end
+
+ it 'returns the list in ascending order by default' do
+ records = described_class.sort_on_created_at
+ expect(records.map(&:id)).to eq created_desc_order.reverse
+ end
+
+ it 'returns the list in descending order if desc is passed as sort direction' do
+ records = described_class.sort_on_created_at(:desc)
+ expect(records.map(&:id)).to eq created_desc_order
+ end
end
- context 'when sort on last_user_message_at' do
- before do
- create(:message, conversation_id: conversation_3.id, message_type: :outgoing, created_at: DateTime.now - 9.days)
- create(:message, conversation_id: conversation_1.id, message_type: :incoming, created_at: DateTime.now - 8.days)
- create(:message, conversation_id: conversation_1.id, message_type: :incoming, created_at: DateTime.now - 8.days)
- create(:message, conversation_id: conversation_1.id, message_type: :outgoing, created_at: DateTime.now - 7.days)
- create(:message, conversation_id: conversation_2.id, message_type: :incoming, created_at: DateTime.now - 6.days)
- create(:message, conversation_id: conversation_2.id, message_type: :incoming, created_at: DateTime.now - 6.days)
- create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now - 6.days)
- create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now - 6.days)
- create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now - 2.days)
+ describe 'sort_on_last_activity_at' do
+ let(:last_activity_asc_order) do
+ [
+ conversation_7.id, conversation_5.id, conversation_4.id, conversation_6.id, conversation_3.id,
+ conversation_1.id, conversation_2.id
+ ]
end
- # conversation_2 has last unanswered incoming message 6 days ago
- # conversation_3 has last unanswered incoming message 2 days ago
- # conversation_1 has incoming message 8 days ago but outgoing message on 7 days ago
- # so we won't consider it to show it on top of the sort as it is answered/replied conversation
- it 'Sort conversations with oldest unanswered incoming message first' do
- conversation_with_message_count = described_class.joins(:messages).uniq.count
- records = described_class.last_user_message_at
-
- expect(records.length).to eq(conversation_with_message_count)
- expect(records[0]['id']).to eq(conversation_2.id)
- expect(records[1]['id']).to eq(conversation_3.id)
- expect(records[2]['id']).to eq(conversation_1.id)
- expect(records.pluck(:id)).not_to include(conversation_4.id)
+ it 'returns the list in descending order by default' do
+ records = described_class.sort_on_last_activity_at
+ expect(records.map(&:id)).to eq last_activity_asc_order.reverse
end
- # Now we have no incoming message the sprt will happen on the created at
- it 'Sort based on oldest message first when there are no incoming message' do
- Message.where(message_type: :incoming).update(message_type: :template)
- conversation_with_message_count = described_class.joins(:messages).uniq.count
- records = described_class.last_user_message_at
-
- expect(records.length).to eq(conversation_with_message_count)
- expect(records[0]['id']).to eq(conversation_1.id)
- expect(records[1]['id']).to eq(conversation_2.id)
- expect(records[2]['id']).to eq(conversation_3.id)
+ it 'returns the list in asc order if asc is passed as sort direction' do
+ records = described_class.sort_on_last_activity_at(:asc)
+ expect(records.map(&:id)).to eq last_activity_asc_order
end
end
@@ -781,7 +772,7 @@ RSpec.describe Conversation do
end
it 'sort conversations with latest resolved conversation at first' do
- records = described_class.latest
+ records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_3.id)
@@ -795,27 +786,48 @@ RSpec.describe Conversation do
content: 'Conversation was marked resolved by system due to days of inactivity'
)
end
- records = described_class.latest
+ records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_1.id)
end
it 'Sort conversations with latest message' do
create(:message, conversation_id: conversation_3.id, message_type: :incoming, created_at: DateTime.now)
- records = described_class.latest
+ records = described_class.sort_on_last_activity_at
expect(records.first.id).to eq(conversation_3.id)
end
end
- context 'when sort on priority' do
- it 'Sort conversations with the following order high > medium > low > nil' do
+ describe 'sort_on_priority' do
+ it 'return list with the following order urgent > high > medium > low > nil by default' do
# ensure they are not pre-sorted
records = described_class.sort_on_created_at
expect(records.pluck(:priority)).not_to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
records = described_class.sort_on_priority
expect(records.pluck(:priority)).to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
+ expect(records.pluck(:id)).to eq(
+ [
+ conversation_4.id, conversation_5.id, conversation_2.id, conversation_1.id, conversation_3.id,
+ conversation_6.id, conversation_7.id
+ ]
+ )
+ end
+
+ it 'return list with the following order low > medium > high > urgent > nil by default' do
+ # ensure they are not pre-sorted
+ records = described_class.sort_on_created_at
+ expect(records.pluck(:priority)).not_to eq(['urgent', 'urgent', 'high', 'medium', 'low', nil, nil])
+
+ records = described_class.sort_on_priority(:asc)
+ expect(records.pluck(:priority)).to eq(['low', 'medium', 'high', 'urgent', 'urgent', nil, nil])
+ expect(records.pluck(:id)).to eq(
+ [
+ conversation_3.id, conversation_1.id, conversation_2.id, conversation_4.id, conversation_5.id,
+ conversation_6.id, conversation_7.id
+ ]
+ )
end
it 'sorts conversation with last_activity for the same priority' do
@@ -830,5 +842,33 @@ RSpec.describe Conversation do
expect(records.pluck(:priority, :id)).to eq([[nil, conversation_6.id], [nil, conversation_7.id]])
end
end
+
+ describe 'sort_on_waiting_since' do
+ it 'returns the list in ascending order by default' do
+ records = described_class.sort_on_waiting_since
+ expect(records.map(&:id)).to eq [
+ conversation_4.id, conversation_5.id, conversation_6.id, conversation_7.id, conversation_3.id, conversation_1.id,
+ conversation_2.id
+ ]
+ end
+
+ it 'returns the list in desc order if asc is passed as sort direction' do
+ records = described_class.sort_on_waiting_since(:desc)
+ expect(records.map(&:id)).to eq [
+ conversation_2.id, conversation_1.id, conversation_3.id, conversation_7.id, conversation_6.id, conversation_5.id,
+ conversation_4.id
+ ]
+ end
+ end
+ end
+
+ describe 'cached_label_list_array' do
+ let(:conversation) { create(:conversation) }
+
+ it 'returns the correct list of labels' do
+ conversation.update(label_list: %w[customer-support enterprise paid-customer])
+
+ expect(conversation.cached_label_list_array).to eq %w[customer-support enterprise paid-customer]
+ end
end
end
diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb
index 71a6209ad..31d127844 100644
--- a/spec/models/notification_spec.rb
+++ b/spec/models/notification_spec.rb
@@ -24,7 +24,6 @@ RSpec.describe Notification do
context 'when push_title is called' do
it 'returns appropriate title suited for the notification type conversation_creation' do
notification = create(:notification, notification_type: 'conversation_creation')
-
expect(notification.push_message_title).to eq "[New conversation] - ##{notification.primary_actor.display_id} has\
been created in #{notification.primary_actor.inbox.name}"
end
@@ -37,7 +36,8 @@ RSpec.describe Notification do
it 'returns appropriate title suited for the notification type assigned_conversation_new_message' do
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
- notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
+ notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} \
#{message.content.truncate_words(10)}"
@@ -46,14 +46,16 @@ RSpec.describe Notification do
it 'returns appropriate title suited for the notification type assigned_conversation_new_message when attachment message' do
# checking content nil should be suffice for attachments
message = create(:message, sender: create(:user), content: nil)
- notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
+ notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} "
end
it 'returns appropriate title suited for the notification type participating_conversation_new_message' do
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
- notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message)
+ notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} \
#{message.content.truncate_words(10)}"
@@ -61,17 +63,16 @@ RSpec.describe Notification do
it 'returns appropriate title suited for the notification type participating_conversation_new_message having mention' do
message = create(:message, sender: create(:user), content: 'Hey [@John](mention://user/1/john), can you check this ticket?')
- notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message,
- secondary_actor: message.sender)
-
+ notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} Hey @John, can you check this ticket?"
end
it 'returns appropriate title suited for the notification type participating_conversation_new_message having multple mention' do
message = create(:message, sender: create(:user),
content: 'Hey [@John](mention://user/1/john), [@Alisha Peter](mention://user/2/alisha) can you check this ticket?')
- notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message,
- secondary_actor: message.sender)
+ notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} \
Hey @John, @Alisha Peter can you check this ticket?"
@@ -79,15 +80,15 @@ Hey @John, @Alisha Peter can you check this ticket?"
it 'returns appropriate title suited for the notification type participating_conversation_new_message if username contains white space' do
message = create(:message, sender: create(:user), content: 'Hey [@John Peter](mention://user/1/john%20K) please check this?')
- notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message,
- secondary_actor: message.sender)
+ notification = create(:notification, notification_type: 'participating_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} Hey @John Peter please check this?"
end
it 'returns appropriate title suited for the notification type conversation_mention' do
message = create(:message, sender: create(:user), content: 'Hey [@John](mention://user/1/john), can you check this ticket?')
- notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message, secondary_actor: message.sender)
+ notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message.conversation, secondary_actor: message)
expect(notification.push_message_title).to eq "[##{message.conversation.display_id}] Hey @John, can you check this ticket?"
end
@@ -98,7 +99,7 @@ Hey @John, @Alisha Peter can you check this ticket?"
create(:user),
content: 'Hey [@John](mention://user/1/john), [@Alisha Peter](mention://user/2/alisha) can you check this ticket?'
)
- notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message, secondary_actor: message.sender)
+ notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message.conversation, secondary_actor: message)
expect(notification.push_message_title).to eq "[##{message.conversation.display_id}] Hey @John, @Alisha Peter can you check this ticket?"
end
@@ -109,10 +110,15 @@ Hey @John, @Alisha Peter can you check this ticket?"
create(:user),
content: 'Hey [@John Peter](mention://user/1/john%20K) please check this?'
)
- notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message, secondary_actor: message.sender)
-
+ notification = create(:notification, notification_type: 'conversation_mention', primary_actor: message.conversation, secondary_actor: message)
expect(notification.push_message_title).to eq "[##{message.conversation.display_id}] Hey @John Peter please check this?"
end
+
+ it 'calls remove duplicate notification job' do
+ allow(Notification::RemoveDuplicateNotificationJob).to receive(:perform_later)
+ notification = create(:notification, notification_type: 'conversation_mention')
+ expect(Notification::RemoveDuplicateNotificationJob).to have_received(:perform_later).with(notification)
+ end
end
context 'when fcm push data' do
@@ -125,16 +131,15 @@ Hey @John, @Alisha Peter can you check this ticket?"
it 'returns correct data for primary actor message' do
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
- notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
-
+ notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message.conversation,
+ secondary_actor: message)
expect(notification.fcm_push_data[:primary_actor]).to eq({
- 'id' => notification.primary_actor.id,
- 'conversation_id' => notification.primary_actor.conversation.display_id
+ 'id' => notification.primary_actor.display_id
})
end
end
- context 'when primary actory is deleted' do
+ context 'when primary actor is deleted' do
let!(:conversation) { create(:conversation) }
it 'clears notifications' do
diff --git a/spec/services/line/incoming_message_service_spec.rb b/spec/services/line/incoming_message_service_spec.rb
index ee53421b5..0547cfbd0 100644
--- a/spec/services/line/incoming_message_service_spec.rb
+++ b/spec/services/line/incoming_message_service_spec.rb
@@ -105,6 +105,41 @@ describe Line::IncomingMessageService do
}.with_indifferent_access
end
+ let(:sticker_params) do
+ {
+ 'destination': '2342234234',
+ 'events': [
+ {
+ 'replyToken': '0f3779fba3b349968c5d07db31eab56f',
+ 'type': 'message',
+ 'mode': 'active',
+ 'timestamp': 1_462_629_479_859,
+ 'source': {
+ 'type': 'user',
+ 'userId': 'U4af4980629'
+ },
+ 'message': {
+ 'type': 'sticker',
+ 'id': '1501597916',
+ 'quoteToken': 'q3Plxr4AgKd...',
+ 'stickerId': '52002738',
+ 'packageId': '11537'
+ }
+ },
+ {
+ 'replyToken': '8cf9239d56244f4197887e939187e19e',
+ 'type': 'follow',
+ 'mode': 'active',
+ 'timestamp': 1_462_629_479_859,
+ 'source': {
+ 'type': 'user',
+ 'userId': 'U4af4980629'
+ }
+ }
+ ]
+ }.with_indifferent_access
+ end
+
describe '#perform' do
context 'when valid text message params' do
it 'creates appropriate conversations, message and contacts' do
@@ -126,6 +161,26 @@ describe Line::IncomingMessageService do
end
end
+ context 'when valid sticker message params' do
+ it 'creates appropriate conversations, message and contacts' do
+ line_bot = double
+ line_user_profile = double
+ allow(Line::Bot::Client).to receive(:new).and_return(line_bot)
+ allow(line_bot).to receive(:get_profile).and_return(line_user_profile)
+ allow(line_user_profile).to receive(:body).and_return(
+ {
+ 'displayName': 'LINE Test',
+ 'userId': 'U4af4980629',
+ 'pictureUrl': 'https://test.com'
+ }.to_json
+ )
+ described_class.new(inbox: line_channel.inbox, params: sticker_params).perform
+ expect(line_channel.inbox.conversations).not_to eq(0)
+ expect(Contact.all.first.name).to eq('LINE Test')
+ expect(line_channel.inbox.messages.first.content).to eq('')
+ end
+ end
+
context 'when valid image message params' do
it 'creates appropriate conversations, message and contacts' do
line_bot = double
diff --git a/spec/services/line/send_on_line_service_spec.rb b/spec/services/line/send_on_line_service_spec.rb
index 2320f8679..bed8917b8 100644
--- a/spec/services/line/send_on_line_service_spec.rb
+++ b/spec/services/line/send_on_line_service_spec.rb
@@ -92,5 +92,57 @@ describe Line::SendOnLineService do
expect(message.status).to eq('delivered')
end
end
+
+ context 'with message attachments' do
+ it 'sends the message with text and attachments' do
+ attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
+ attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
+ expected_url_regex = %r{rails/active_storage/disk/[a-zA-Z0-9=_\-+]+/avatar\.png}
+
+ expect(line_client).to receive(:push_message).with(
+ message.conversation.contact_inbox.source_id,
+ [
+ { type: 'text', text: message.content },
+ {
+ type: 'image',
+ originalContentUrl: match(expected_url_regex),
+ previewImageUrl: match(expected_url_regex)
+ }
+ ]
+ )
+
+ described_class.new(message: message).perform
+ end
+
+ it 'sends the message with attachments only' do
+ attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
+ attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
+ message.update!(content: nil)
+ expected_url_regex = %r{rails/active_storage/disk/[a-zA-Z0-9=_\-+]+/avatar\.png}
+
+ expect(line_client).to receive(:push_message).with(
+ message.conversation.contact_inbox.source_id,
+ [
+ {
+ type: 'image',
+ originalContentUrl: match(expected_url_regex),
+ previewImageUrl: match(expected_url_regex)
+ }
+ ]
+ )
+
+ described_class.new(message: message).perform
+ end
+
+ it 'sends the message with text only' do
+ message.attachments.destroy_all
+ expect(line_client).to receive(:push_message).with(
+ message.conversation.contact_inbox.source_id,
+ { type: 'text', text: message.content }
+ )
+
+ described_class.new(message: message).perform
+ end
+ end
end
end
diff --git a/spec/services/messages/mention_service_spec.rb b/spec/services/messages/mention_service_spec.rb
index 6090a013c..94277570b 100644
--- a/spec/services/messages/mention_service_spec.rb
+++ b/spec/services/messages/mention_service_spec.rb
@@ -32,7 +32,8 @@ describe Messages::MentionService do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'conversation_mention',
user: first_agent,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
@@ -55,11 +56,13 @@ describe Messages::MentionService do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'conversation_mention',
user: second_agent,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'conversation_mention',
user: first_agent,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
it 'add the users to the participants list' do
diff --git a/spec/services/messages/new_message_notification_service_spec.rb b/spec/services/messages/new_message_notification_service_spec.rb
index bb35ec48a..c76453174 100644
--- a/spec/services/messages/new_message_notification_service_spec.rb
+++ b/spec/services/messages/new_message_notification_service_spec.rb
@@ -40,21 +40,24 @@ describe Messages::NewMessageNotificationService do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_2,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
it 'creates notifications for assignee' do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
it 'will not create notifications for the user who created the message' do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_1,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
@@ -69,18 +72,21 @@ describe Messages::NewMessageNotificationService do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
it 'creates notifications for all participating users' do
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_1,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
expect(NotificationBuilder).to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: participating_agent_2,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
@@ -97,7 +103,8 @@ describe Messages::NewMessageNotificationService do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
@@ -112,11 +119,13 @@ describe Messages::NewMessageNotificationService do
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'participating_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
expect(NotificationBuilder).not_to have_received(:new).with(notification_type: 'assigned_conversation_new_message',
user: assignee,
account: account,
- primary_actor: message)
+ primary_actor: message.conversation,
+ secondary_actor: message)
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 59ad6c1d3..f7a9462e3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7746,12 +7746,14 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae"
integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==
-axios@^0.21.2:
- version "0.21.2"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
- integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==
+axios@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
+ integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
dependencies:
- follow-redirects "^1.14.0"
+ follow-redirects "^1.15.0"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
axobject-query@^3.1.1:
version "3.2.1"
@@ -8926,6 +8928,11 @@ color-support@^1.1.2:
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
+color2k@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/color2k/-/color2k-2.0.2.tgz#ac2b4aea11c822a6bcb70c768b5a289f4fffcebb"
+ integrity sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==
+
color@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e"
@@ -11392,10 +11399,10 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
-follow-redirects@^1.0.0, follow-redirects@^1.14.0:
- version "1.14.8"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
- integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
+follow-redirects@^1.0.0, follow-redirects@^1.15.0:
+ version "1.15.3"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
+ integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
for-each@^0.3.3:
version "0.3.3"
@@ -17300,6 +17307,11 @@ proxy-from-env@1.0.0:
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -20637,6 +20649,11 @@ vue-upload-component@2.8.22:
resolved "https://registry.yarnpkg.com/vue-upload-component/-/vue-upload-component-2.8.22.tgz#7a1573149a4afa5ca6e8c7e0bc70533925fe26b7"
integrity sha512-AJpETqiZrgqs8bwJQpWTFrRg3i6s7cUodRRZVnb1f94Jvpd0YYfzGY4zluBqPmssNSkUaYu7EteXaK8aW17Osw==
+vue-virtual-scroll-list@^2.3.5:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.5.tgz#b589ac6245faf857c35090f854e59d653e90626c"
+ integrity sha512-YFK6u5yltqtAOfTBcij/KGAS2SoZvzbNIAf9qTULauPObEp53xj22tDuohrrM2vNkgoD5kejXICIUBt2Q4ZDqQ==
+
vue2-datepicker@^3.9.1:
version "3.9.1"
resolved "https://registry.yarnpkg.com/vue2-datepicker/-/vue2-datepicker-3.9.1.tgz#00d11cf30942e850f8b1a397af3c15c7465f248e"
+
+
+
+
++ <%= content_for(:title) %> +
+Update your instance settings, access billing portal
+
+
+
+
+
+
+ Current plan
+ + + Refresh + +<%= SuperAdmin::FeaturesHelper.plan_details.html_safe %>
+
+
+ Installation Identifier
+ <%= ChatwootHub.installation_identifier %> +
+
+
+ <% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %>
+
+
+
+
+
+ Current plan
+<%= SuperAdmin::FeaturesHelper.plan_details.html_safe %>
+
+
+ <% end %>
+
+
+
+
+ You have <%= User.count %> agents. Please add more licenses.
+
+
+
+
+
+
+
+ <% if ChatwootHub.pricing_plan !='community' %>
+
+ <% end %>
+ Need help?
+Do you face any issues? We are here to help.
+
+
+ Features
+
+ <% SuperAdmin::FeaturesHelper.available_features.each do |feature, attrs| %>
+
+
+
+
+
+
+ <% end %>
+
+
+
+ <% if !attrs[:enabled] %>
+
+ <% end %>
+
+
+
+
+ <%= attrs[:name] %>
+ <% if attrs[:enterprise] %> + EE + <% end %> + <% if attrs[:config_key].present? && attrs[:enabled] %> + + + + <% end %> +<%= attrs[:description] %>
+