diff --git a/.circleci/config.yml b/.circleci/config.yml
index fbb4fea90..e6394ccde 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build
docker:
# specify the version you desire here
- - image: cimg/ruby:3.0.4-browsers
+ - image: cimg/ruby:3.1.3-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
@@ -38,6 +38,18 @@ jobs:
name: Which bundler?
command: bundle -v
+ - run:
+ name: Swap node versions
+ command: |
+ set +e
+ wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
+ export NVM_DIR="$HOME/.nvm"
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+ [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
+ nvm install v16
+ echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
+ echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
+
# Run bundler
# Load installed gems from cache if possible, bundle install then save cache
# Multiple caches are used to increase the chance of a cache hit
@@ -193,4 +205,3 @@ workflows:
- upload-coverage:
requires:
- build
-
diff --git a/.env.example b/.env.example
index 824a96285..149d1f6e6 100644
--- a/.env.example
+++ b/.env.example
@@ -131,6 +131,11 @@ TWITTER_ENVIRONMENT=
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
+# Google OAuth
+GOOGLE_OAUTH_CLIENT_ID=
+GOOGLE_OAUTH_CLIENT_SECRET=
+GOOGLE_OAUTH_CALLBACK_URL=
+
### Change this env variable only if you are using a custom build mobile app
## Mobile app env variables
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index 790a322fd..c42909856 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -19,6 +19,7 @@ concurrency:
jobs:
action:
runs-on: ubuntu-latest
+ if: ${{ github.repository == 'chatwoot/chatwoot' }}
steps:
- uses: dessant/lock-threads@v3
with:
diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml
index 39df7aa3d..98fae5c8e 100644
--- a/.github/workflows/run_foss_spec.yml
+++ b/.github/workflows/run_foss_spec.yml
@@ -47,7 +47,6 @@ jobs:
- uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.0.4 # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: yarn
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 000000000..7a7564ecb
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,28 @@
+# This workflow warns and then closes PRs that have had no activity for a specified amount of time.
+#
+# You can adjust the behavior by modifying this file.
+# For more information, see:
+# https://github.com/actions/stale
+name: Mark stale issues and pull requests
+
+on:
+ schedule:
+ - cron: '28 3 * * *'
+
+jobs:
+ stale:
+
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+
+ steps:
+ - uses: actions/stale@v5
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ days-before-issue-close: -1,
+ days-before-issue-stale: -1
+ days-before-pr-close: -1,
+ days-before-pr-stale: 30,
+ stale-pr-message: '🐢 Turtley slow progress alert! This pull request has been idle for over 30 days. Can we please speed things up and either merge it or release it back into the wild?'
+ stale-pr-label: 'stale'
diff --git a/.gitignore b/.gitignore
index fc77a7b55..4182a0ac7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,3 +62,7 @@ test/cypress/videos/*
/config/*.enc
.vscode/settings.json
+
+# yalc for local testing
+.yalc
+yalc.lock
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 7774f34f5..9984ac506 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,5 +1,11 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
-npm run eslint
-bundle exec rubocop -a
-git add
+
+# lint js and vue files
+npx --no-install lint-staged
+
+# lint only staged ruby files
+git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop -a
+
+# stage rubocop changes to files
+git diff --name-only --cached | xargs git add
diff --git a/.rubocop.yml b/.rubocop.yml
index dafd9a620..8b41dd5fd 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -17,7 +17,6 @@ Metrics/ClassLength:
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/listeners/action_cable_listener.rb'
- - 'app/models/conversation.rb'
RSpec/ExampleLength:
Max: 25
Style/Documentation:
@@ -86,6 +85,10 @@ Style/ClassAndModuleChildren:
- 'config/application.rb'
Style/MapToHash:
Enabled: false
+Style/HashSyntax:
+ Enabled: true
+ EnforcedStyle: no_mixed_keys
+ EnforcedShorthandSyntax: never
RSpec/NestedGroups:
Enabled: true
Max: 4
@@ -159,7 +162,7 @@ RSpec/NamedSubject:
Enabled: false
# we should bring this down
RSpec/MultipleMemoizedHelpers:
- Max: 12
+ Max: 14
AllCops:
NewCops: enable
@@ -184,4 +187,3 @@ AllCops:
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
- db/migrate/20220809104508_revert_cascading_indexes.rb
-
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 4d66828c3..48e714dd6 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -68,7 +68,6 @@ Naming/AccessorMethodName:
- 'app/controllers/api/v1/accounts_controller.rb'
- 'app/controllers/api/v1/callbacks_controller.rb'
- 'app/controllers/api/v1/conversations_controller.rb'
- - 'app/controllers/passwords_controller.rb'
# Offense count: 9
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
diff --git a/.ruby-version b/.ruby-version
index b0f2dcb32..ff365e06b 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.0.4
+3.1.3
diff --git a/Gemfile b/Gemfile
index 1d9c50cb6..011acf584 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,10 +1,10 @@
source 'https://rubygems.org'
-ruby '3.0.4'
+ruby '3.1.3'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
-gem 'rails', '~> 6.1', '>= 6.1.6.1'
+gem 'rails', '~> 6.1', '>= 6.1.7.1'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@@ -39,6 +39,10 @@ gem 'rack-attack'
gem 'down', '~> 5.0'
# authentication type to fetch and send mail over oauth2.0
gem 'gmail_xoauth'
+# Prevent CSV injection
+gem 'csv-safe'
+# Support message translation
+gem 'google-cloud-translate'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
@@ -98,16 +102,16 @@ gem 'newrelic_rpm'
gem 'scout_apm'
gem 'sentry-rails', '~> 5.3', '>= 5.3.1'
gem 'sentry-ruby', '~> 5.3'
-gem 'sentry-sidekiq', '~> 5.3'
+gem 'sentry-sidekiq', '~> 5.3', '>= 5.3.1'
##-- background job processing --##
-gem 'sidekiq', '~> 6.4.0'
+gem 'sidekiq', '~> 6.4.2'
# We want cron jobs
-gem 'sidekiq-cron', '~> 1.3'
+gem 'sidekiq-cron', '~> 1.6', '>= 1.6.0'
##-- Push notification service --##
gem 'fcm'
-gem 'webpush'
+gem 'web-push'
##-- geocoding / parse location from ip --##
# http://www.rubygeocoder.com/
@@ -137,6 +141,11 @@ gem 'stripe'
## to populate db with sample data
gem 'faker'
+# Can remove this in rails 7
+gem 'net-imap', require: false
+gem 'net-pop', require: false
+gem 'net-smtp', require: false
+
group :production, :staging do
# we dont want request timing out in development while using byebug
gem 'rack-timeout'
@@ -157,7 +166,7 @@ end
group :test do
# Cypress in rails.
- gem 'cypress-on-rails', '~> 1.0'
+ gem 'cypress-on-rails', '~> 1.13', '>= 1.13.1'
# fast cleaning of database
gem 'database_cleaner'
# mock http calls
@@ -190,5 +199,11 @@ group :development, :test do
gem 'spring'
gem 'spring-watcher-listen'
end
+
# worked with microsoft refresh token
gem 'omniauth-oauth2'
+
+# need for google auth
+gem 'omniauth'
+gem 'omniauth-google-oauth2'
+gem 'omniauth-rails_csrf_protection', '~> 1.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4b805b109..112e09e7f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -9,63 +9,63 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (6.1.6.1)
- actionpack (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ actioncable (6.1.7.1)
+ actionpack (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.1.6.1)
- actionpack (= 6.1.6.1)
- activejob (= 6.1.6.1)
- activerecord (= 6.1.6.1)
- activestorage (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ actionmailbox (6.1.7.1)
+ actionpack (= 6.1.7.1)
+ activejob (= 6.1.7.1)
+ activerecord (= 6.1.7.1)
+ activestorage (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
mail (>= 2.7.1)
- actionmailer (6.1.6.1)
- actionpack (= 6.1.6.1)
- actionview (= 6.1.6.1)
- activejob (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ actionmailer (6.1.7.1)
+ actionpack (= 6.1.7.1)
+ actionview (= 6.1.7.1)
+ activejob (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.1.6.1)
- actionview (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ actionpack (6.1.7.1)
+ actionview (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.1.6.1)
- actionpack (= 6.1.6.1)
- activerecord (= 6.1.6.1)
- activestorage (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ actiontext (6.1.7.1)
+ actionpack (= 6.1.7.1)
+ activerecord (= 6.1.7.1)
+ activestorage (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
nokogiri (>= 1.8.5)
- actionview (6.1.6.1)
- activesupport (= 6.1.6.1)
+ actionview (6.1.7.1)
+ activesupport (= 6.1.7.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
- activejob (6.1.6.1)
- activesupport (= 6.1.6.1)
+ activejob (6.1.7.1)
+ activesupport (= 6.1.7.1)
globalid (>= 0.3.6)
- activemodel (6.1.6.1)
- activesupport (= 6.1.6.1)
- activerecord (6.1.6.1)
- activemodel (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ activemodel (6.1.7.1)
+ activesupport (= 6.1.7.1)
+ activerecord (6.1.7.1)
+ activemodel (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
activerecord-import (1.4.0)
activerecord (>= 4.2)
- activestorage (6.1.6.1)
- actionpack (= 6.1.6.1)
- activejob (= 6.1.6.1)
- activerecord (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ activestorage (6.1.7.1)
+ actionpack (= 6.1.7.1)
+ activejob (= 6.1.7.1)
+ activerecord (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (6.1.6.1)
+ activesupport (6.1.7.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -73,8 +73,8 @@ GEM
zeitwerk (~> 2.3)
acts-as-taggable-on (9.0.1)
activerecord (>= 6.0, < 7.1)
- addressable (2.8.0)
- public_suffix (>= 2.0.2, < 5.0)
+ addressable (2.8.1)
+ public_suffix (>= 2.0.2, < 6.0)
administrate (0.17.0)
actionpack (>= 5.0)
actionview (>= 5.0)
@@ -135,12 +135,13 @@ GEM
byebug (11.1.3)
climate_control (1.1.1)
coderay (1.1.3)
- commonmarker (0.23.6)
+ commonmarker (0.23.7)
concurrent-ruby (1.1.10)
connection_pool (2.2.5)
crack (0.4.5)
rexml
crass (1.0.6)
+ csv-safe (3.1.1)
cypress-on-rails (1.13.1)
rack
database_cleaner (2.0.1)
@@ -247,7 +248,7 @@ GEM
grpc (~> 1.36)
geocoder (1.8.0)
gli (2.21.0)
- globalid (1.0.0)
+ globalid (1.0.1)
activesupport (>= 5.0)
gmail_xoauth (0.4.2)
oauth (>= 0.3.6)
@@ -288,6 +289,19 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
+ google-cloud-translate (3.3.0)
+ google-cloud-core (~> 1.6)
+ google-cloud-translate-v2 (>= 0.0, < 2.a)
+ google-cloud-translate-v3 (>= 0.0, < 2.a)
+ google-cloud-translate-v2 (0.4.0)
+ faraday (>= 0.17.3, < 2.a)
+ google-cloud-core (~> 1.6)
+ googleapis-common-protos (>= 1.3.10, < 2.a)
+ googleapis-common-protos-types (>= 1.0.5, < 2.a)
+ googleauth (>= 0.16.2, < 2.a)
+ google-cloud-translate-v3 (0.5.0)
+ gapic-common (>= 0.10, < 2.a)
+ google-cloud-errors (~> 1.0)
google-protobuf (3.21.7)
google-protobuf (3.21.7-x86_64-darwin)
google-protobuf (3.21.7-x86_64-linux)
@@ -323,7 +337,7 @@ GEM
hana (1.3.7)
hashdiff (1.0.1)
hashie (5.0.0)
- hkdf (0.3.0)
+ hkdf (1.0.0)
html2text (0.2.1)
nokogiri (~> 1.6)
http (5.1.0)
@@ -360,7 +374,7 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
- jwt (2.4.1)
+ jwt (2.5.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -426,8 +440,16 @@ GEM
multipart-post (2.2.3)
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
+ net-imap (0.3.1)
+ net-protocol
+ net-pop (0.1.2)
+ net-protocol
+ net-protocol (0.2.1)
+ timeout
+ net-smtp (0.3.3)
+ net-protocol
netrc (0.11.0)
- newrelic_rpm (8.9.0)
+ newrelic_rpm (8.15.0)
nio4r (2.5.8)
nokogiri (1.13.10)
mini_portile2 (~> 2.8.0)
@@ -450,9 +472,18 @@ GEM
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
+ omniauth-google-oauth2 (1.1.1)
+ jwt (>= 2.0)
+ oauth2 (~> 2.0.6)
+ omniauth (~> 2.0)
+ omniauth-oauth2 (~> 1.8.0)
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
+ omniauth-rails_csrf_protection (1.0.1)
+ actionpack (>= 4.2)
+ omniauth (~> 2.0)
+ openssl (3.1.0)
orm_adapter (0.5.0)
os (1.1.4)
parallel (1.22.1)
@@ -469,14 +500,14 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
- public_suffix (4.0.7)
+ public_suffix (5.0.1)
puma (5.6.4)
nio4r (~> 2.0)
pundit (2.2.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.6.1)
- rack (2.2.4)
+ rack (2.2.6.2)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
@@ -488,29 +519,29 @@ GEM
rack-test (2.0.2)
rack (>= 1.3)
rack-timeout (0.6.3)
- rails (6.1.6.1)
- actioncable (= 6.1.6.1)
- actionmailbox (= 6.1.6.1)
- actionmailer (= 6.1.6.1)
- actionpack (= 6.1.6.1)
- actiontext (= 6.1.6.1)
- actionview (= 6.1.6.1)
- activejob (= 6.1.6.1)
- activemodel (= 6.1.6.1)
- activerecord (= 6.1.6.1)
- activestorage (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ rails (6.1.7.1)
+ actioncable (= 6.1.7.1)
+ actionmailbox (= 6.1.7.1)
+ actionmailer (= 6.1.7.1)
+ actionpack (= 6.1.7.1)
+ actiontext (= 6.1.7.1)
+ actionview (= 6.1.7.1)
+ activejob (= 6.1.7.1)
+ activemodel (= 6.1.7.1)
+ activerecord (= 6.1.7.1)
+ activestorage (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
bundler (>= 1.15.0)
- railties (= 6.1.6.1)
+ railties (= 6.1.7.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.4)
loofah (~> 2.19, >= 2.19.1)
- railties (6.1.6.1)
- actionpack (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ railties (6.1.7.1)
+ actionpack (= 6.1.7.1)
+ activesupport (= 6.1.7.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
@@ -662,6 +693,7 @@ GEM
time_diff (0.3.0)
activesupport
i18n
+ timeout (0.3.1)
trailblazer-option (0.1.2)
twilio-ruby (5.68.0)
faraday (>= 0.9, < 3.0)
@@ -693,7 +725,11 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
- webmock (3.14.0)
+ web-push (3.0.0)
+ hkdf (~> 1.0)
+ jwt (~> 2.0)
+ openssl (~> 3.0)
+ webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -702,9 +738,6 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
- webpush (1.1.0)
- hkdf (~> 0.2)
- jwt (~> 2.0)
webrick (1.7.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
@@ -741,7 +774,8 @@ DEPENDENCIES
byebug
climate_control
commonmarker
- cypress-on-rails (~> 1.0)
+ csv-safe
+ cypress-on-rails (~> 1.13, >= 1.13.1)
database_cleaner
ddtrace
devise
@@ -761,6 +795,7 @@ DEPENDENCIES
gmail_xoauth
google-cloud-dialogflow
google-cloud-storage
+ google-cloud-translate
groupdate
haikunator
hairtrigger
@@ -779,8 +814,14 @@ DEPENDENCIES
listen
maxminddb
mock_redis
+ net-imap
+ net-pop
+ net-smtp
newrelic_rpm
+ omniauth
+ omniauth-google-oauth2
omniauth-oauth2
+ omniauth-rails_csrf_protection (~> 1.0)
pg
pg_search
procore-sift
@@ -790,7 +831,7 @@ DEPENDENCIES
rack-attack
rack-cors
rack-timeout
- rails (~> 6.1, >= 6.1.6.1)
+ rails (~> 6.1, >= 6.1.7.1)
redis
redis-namespace
responders
@@ -805,10 +846,10 @@ DEPENDENCIES
seed_dump
sentry-rails (~> 5.3, >= 5.3.1)
sentry-ruby (~> 5.3)
- sentry-sidekiq (~> 5.3)
+ sentry-sidekiq (~> 5.3, >= 5.3.1)
shoulda-matchers
- sidekiq (~> 6.4.0)
- sidekiq-cron (~> 1.3)
+ sidekiq (~> 6.4.2)
+ sidekiq-cron (~> 1.6, >= 1.6.0)
simplecov (= 0.17.1)
slack-ruby-client
spring
@@ -824,14 +865,14 @@ DEPENDENCIES
uglifier
valid_email2
web-console
+ web-push
webmock
webpacker (~> 5.4, >= 5.4.3)
- webpush
wisper (= 2.0.0)
working_hours
RUBY VERSION
- ruby 3.0.4p208
+ ruby 3.1.3p185
BUNDLED WITH
- 2.3.16
+ 2.3.26
diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb
index 69ed786ce..ba5861859 100644
--- a/app/builders/messages/message_builder.rb
+++ b/app/builders/messages/message_builder.rb
@@ -48,13 +48,25 @@ class Messages::MessageBuilder
def process_emails
return unless @conversation.inbox&.inbox_type == 'Email'
- cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails]
- bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails]
+ cc_emails = []
+ cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails].present?
+
+ bcc_emails = []
+ bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails].present?
+
+ all_email_addresses = cc_emails + bcc_emails
+ validate_email_addresses(all_email_addresses)
@message.content_attributes[:cc_emails] = cc_emails
@message.content_attributes[:bcc_emails] = bcc_emails
end
+ def validate_email_addresses(all_emails)
+ all_emails&.each do |email|
+ raise StandardError, 'Invalid email address' unless email.match?(URI::MailTo::EMAIL_REGEXP)
+ end
+ end
+
def message_type
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb
index a7891ffd4..b6e07be5f 100644
--- a/app/controllers/api/v1/accounts/articles_controller.rb
+++ b/app/controllers/api/v1/accounts/articles_controller.rb
@@ -1,14 +1,14 @@
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :portal
before_action :check_authorization
- before_action :fetch_article, except: [:index, :create]
+ before_action :fetch_article, except: [:index, :create, :attach_file]
before_action :set_current_page, only: [:index]
def index
@portal_articles = @portal.articles
@all_articles = @portal_articles.search(list_params)
@articles_count = @all_articles.count
- @articles = @all_articles.page(@current_page)
+ @articles = @all_articles.order_by_updated_at.page(@current_page)
end
def create
@@ -23,7 +23,8 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def show; end
def update
- @article.update!(article_params)
+ @article.update!(article_params) if params[:article].present?
+ render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
end
def destroy
@@ -31,6 +32,17 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
head :ok
end
+ def attach_file
+ file_blob = ActiveStorage::Blob.create_and_upload!(
+ key: nil,
+ io: params[:background_image].tempfile,
+ filename: params[:background_image].original_filename,
+ content_type: params[:background_image].content_type
+ )
+ file_blob.save!
+ render json: { file_url: url_for(file_blob) }
+ end
+
private
def fetch_article
@@ -43,7 +55,8 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def article_params
params.require(:article).permit(
- :title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description,
+ :title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title,
+ :description,
{ tags: [] }]
)
end
diff --git a/app/controllers/api/v1/accounts/canned_responses_controller.rb b/app/controllers/api/v1/accounts/canned_responses_controller.rb
index 031ffc415..5528610ec 100644
--- a/app/controllers/api/v1/accounts/canned_responses_controller.rb
+++ b/app/controllers/api/v1/accounts/canned_responses_controller.rb
@@ -33,7 +33,10 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
def canned_responses
if params[:search]
- Current.account.canned_responses.where('short_code ILIKE :search OR content ILIKE :search', search: "%#{params[:search]}%")
+ Current.account.canned_responses
+ .where('short_code ILIKE :search OR content ILIKE :search', search: "%#{params[:search]}%")
+ .order_by_search(params[:search])
+
else
Current.account.canned_responses
end
diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb
index 77a3a7081..319c6763f 100644
--- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb
@@ -18,6 +18,24 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
end
end
+ def translate
+ return head :ok if already_translated_content_available?
+
+ translated_content = Integrations::GoogleTranslate::ProcessorService.new(
+ message: message,
+ target_language: permitted_params[:target_language]
+ ).perform
+
+ if translated_content.present?
+ translations = {}
+ translations[permitted_params[:target_language]] = translated_content
+ translations = message.translations.merge!(translations) if message.translations.present?
+ message.update!(translations: translations)
+ end
+
+ render json: { content: translated_content }
+ end
+
private
def message
@@ -29,6 +47,10 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
end
def permitted_params
- params.permit(:id)
+ params.permit(:id, :target_language)
+ end
+
+ def already_translated_content_available?
+ message.translations.present? && message.translations[permitted_params[:target_language]].present?
end
end
diff --git a/app/controllers/api/v1/accounts/conversations/participants_controller.rb b/app/controllers/api/v1/accounts/conversations/participants_controller.rb
new file mode 100644
index 000000000..ebd02380f
--- /dev/null
+++ b/app/controllers/api/v1/accounts/conversations/participants_controller.rb
@@ -0,0 +1,41 @@
+class Api::V1::Accounts::Conversations::ParticipantsController < Api::V1::Accounts::Conversations::BaseController
+ def show
+ @participants = @conversation.conversation_participants
+ end
+
+ def create
+ ActiveRecord::Base.transaction do
+ @participants = participants_to_be_added_ids.map { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
+ end
+ end
+
+ def update
+ ActiveRecord::Base.transaction do
+ participants_to_be_added_ids.each { |user_id| @conversation.conversation_participants.find_or_create_by(user_id: user_id) }
+ participants_to_be_removed_ids.each { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
+ end
+ @participants = @conversation.conversation_participants
+ render action: 'show'
+ end
+
+ def destroy
+ ActiveRecord::Base.transaction do
+ params[:user_ids].map { |user_id| @conversation.conversation_participants.find_by(user_id: user_id)&.destroy }
+ end
+ head :ok
+ end
+
+ private
+
+ def participants_to_be_added_ids
+ params[:user_ids] - current_participant_ids
+ end
+
+ def participants_to_be_removed_ids
+ current_participant_ids - params[:user_ids]
+ end
+
+ def current_participant_ids
+ @current_participant_ids ||= @conversation.conversation_participants.pluck(:user_id)
+ end
+end
diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb
index 24507977e..24f797025 100644
--- a/app/controllers/api/v1/accounts/inboxes_controller.rb
+++ b/app/controllers/api/v1/accounts/inboxes_controller.rb
@@ -14,7 +14,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
# Deprecated: This API will be removed in 2.7.0
def assignable_agents
- @assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
+ @assignable_agents = @inbox.assignable_agents
end
def campaigns
diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb
index d28ae54b7..b1837acbd 100644
--- a/app/controllers/api/v1/accounts/portals_controller.rb
+++ b/app/controllers/api/v1/accounts/portals_controller.rb
@@ -1,7 +1,7 @@
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
include ::FileTypeHelper
- before_action :fetch_portal, except: [:index, :create]
+ before_action :fetch_portal, except: [:index, :create, :attach_file]
before_action :check_authorization
before_action :set_current_page, only: [:index]
@@ -48,7 +48,19 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
end
def process_attached_logo
- @portal.logo.attach(params[:logo])
+ blob_id = params[:blob_id]
+ blob = ActiveStorage::Blob.find_by(id: blob_id)
+ @portal.logo.attach(blob)
+ end
+
+ def attach_file
+ file_blob = ActiveStorage::Blob.create_and_upload!(
+ key: nil,
+ io: params[:logo].tempfile,
+ filename: params[:logo].original_filename,
+ content_type: params[:logo].content_type
+ )
+ render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
private
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
index a7764cbc9..23178b5f3 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -2,6 +2,24 @@ class ApiController < ApplicationController
skip_before_action :set_current_user, only: [:index]
def index
- render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) }
+ render json: { version: Chatwoot.config[:version],
+ timestamp: Time.now.utc.to_formatted_s(:db),
+ queue_services: redis_status,
+ data_services: postgres_status }
+ end
+
+ private
+
+ def redis_status
+ r = Redis.new(Redis::Config.app)
+ return 'ok' if r.ping
+ rescue Redis::CannotConnectError
+ 'failing'
+ end
+
+ def postgres_status
+ ActiveRecord::Base.connection.active? ? 'ok' : 'failing'
+ rescue ActiveRecord::ConnectionNotEstablished
+ 'failing'
end
end
diff --git a/app/controllers/concerns/switch_locale.rb b/app/controllers/concerns/switch_locale.rb
index 5f308b8ab..744a70da9 100644
--- a/app/controllers/concerns/switch_locale.rb
+++ b/app/controllers/concerns/switch_locale.rb
@@ -3,25 +3,25 @@ module SwitchLocale
private
- def switch_locale(&action)
+ def switch_locale(&)
# priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= locale_from_params
# if locale is not set in account, let's use DEFAULT_LOCALE env variable
locale ||= locale_from_env_variable
- set_locale(locale, &action)
+ set_locale(locale, &)
end
- def switch_locale_using_account_locale(&action)
+ def switch_locale_using_account_locale(&)
locale = locale_from_account(@current_account)
- set_locale(locale, &action)
+ set_locale(locale, &)
end
- def set_locale(locale, &action)
+ def set_locale(locale, &)
# if locale is empty, use default_locale
locale ||= I18n.default_locale
# Ensure locale won't bleed into other requests
# https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests
- I18n.with_locale(locale, &action)
+ I18n.with_locale(locale, &)
end
def locale_from_params
diff --git a/app/controllers/devise_overrides/omniauth_callbacks_controller.rb b/app/controllers/devise_overrides/omniauth_callbacks_controller.rb
new file mode 100644
index 000000000..e1cf76d6b
--- /dev/null
+++ b/app/controllers/devise_overrides/omniauth_callbacks_controller.rb
@@ -0,0 +1,75 @@
+class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
+ include EmailHelper
+
+ def omniauth_success
+ get_resource_from_auth_hash
+
+ @resource.present? ? sign_in_user : sign_up_user
+ end
+
+ private
+
+ def sign_in_user
+ @resource.skip_confirmation! if confirmable_enabled?
+
+ # once the resource is found and verified
+ # we can just send them to the login page again with the SSO params
+ # that will log them in
+ encoded_email = ERB::Util.url_encode(@resource.email)
+ redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token)
+ end
+
+ def sign_up_user
+ return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed?
+ return redirect_to login_page_url(error: 'business-account-only') unless validate_business_account?
+
+ create_account_for_user
+ token = @resource.send(:set_reset_password_token)
+ frontend_url = ENV.fetch('FRONTEND_URL', nil)
+ redirect_to "#{frontend_url}/app/auth/password/edit?config=default&reset_password_token=#{token}"
+ end
+
+ def login_page_url(error: nil, email: nil, sso_auth_token: nil)
+ frontend_url = ENV.fetch('FRONTEND_URL', nil)
+ params = { email: email, sso_auth_token: sso_auth_token }.compact
+ params[:error] = error if error.present?
+
+ "#{frontend_url}/app/login?#{params.to_query}"
+ end
+
+ def account_signup_allowed?
+ # set it to true by default, this is the behaviour across the app
+ GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') != 'false'
+ end
+
+ def resource_class(_mapping = nil)
+ User
+ end
+
+ def get_resource_from_auth_hash # rubocop:disable Naming/AccessorMethodName
+ # find the user with their email instead of UID and token
+ @resource = resource_class.where(
+ email: auth_hash['info']['email']
+ ).first
+ end
+
+ def validate_business_account?
+ # return true if the user is a business account, false if it is a gmail account
+ auth_hash['info']['email'].exclude?('@gmail.com')
+ end
+
+ def create_account_for_user
+ @resource, @account = AccountBuilder.new(
+ account_name: extract_domain_without_tld(auth_hash['info']['email']),
+ user_full_name: auth_hash['info']['name'],
+ email: auth_hash['info']['email'],
+ locale: I18n.locale,
+ confirmed: auth_hash['info']['email_verified']
+ ).perform
+ Avatar::AvatarFromUrlJob.perform_later(@resource, auth_hash['info']['image'])
+ end
+
+ def default_devise_mapping
+ 'user'
+ end
+end
diff --git a/app/controllers/microsoft_controller.rb b/app/controllers/microsoft_controller.rb
new file mode 100644
index 000000000..07e58d4db
--- /dev/null
+++ b/app/controllers/microsoft_controller.rb
@@ -0,0 +1,17 @@
+class MicrosoftController < ApplicationController
+ after_action :set_version_header
+
+ def identity_association
+ microsoft_indentity
+ end
+
+ private
+
+ def set_version_header
+ response.headers['Content-Length'] = { associatedApplications: [{ applicationId: @identity_json }] }.to_json.length
+ end
+
+ def microsoft_indentity
+ @identity_json = ENV.fetch('AZURE_APP_ID', nil)
+ end
+end
diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb
index 2c8995f81..e0de74da1 100644
--- a/app/controllers/platform/api/v1/users_controller.rb
+++ b/app/controllers/platform/api/v1/users_controller.rb
@@ -13,8 +13,7 @@ class Platform::Api::V1::UsersController < PlatformController
end
def login
- encoded_email = ERB::Util.url_encode(@resource.email)
- render json: { url: "#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
+ render json: { url: @resource.generate_sso_link }
end
def show; end
diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb
index ae0fb2b6a..22ffbbf7a 100644
--- a/app/controllers/public/api/v1/portals/articles_controller.rb
+++ b/app/controllers/public/api/v1/portals/articles_controller.rb
@@ -16,6 +16,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
def set_article
@article = @category.articles.find(params[:id])
+ @article.increment_view_count
@parsed_content = render_article_content(@article.content)
end
diff --git a/app/controllers/super_admin/instance_statuses_controller.rb b/app/controllers/super_admin/instance_statuses_controller.rb
new file mode 100644
index 000000000..e7b037099
--- /dev/null
+++ b/app/controllers/super_admin/instance_statuses_controller.rb
@@ -0,0 +1,44 @@
+class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
+ def show
+ @metrics = {}
+ chatwoot_version
+ sha
+ postgres_status
+ redis_metrics
+ end
+
+ def chatwoot_version
+ @metrics['Chatwoot version'] = Chatwoot.config[:version]
+ end
+
+ def sha
+ sha = `git rev-parse HEAD`
+ @metrics['Git SHA'] = sha.presence || 'n/a'
+ end
+
+ def postgres_status
+ @metrics['Postgres alive'] = if ActiveRecord::Base.connection.active?
+ 'true'
+ else
+ 'false'
+ end
+ end
+
+ def redis_metrics
+ r = Redis.new(Redis::Config.app)
+ if r.ping == 'PONG'
+ redis_server = r.info
+ @metrics['Redis alive'] = 'true'
+ @metrics['Redis version'] = redis_server['redis_version']
+ @metrics['Redis number of connected clients'] = redis_server['connected_clients']
+ @metrics["Redis 'maxclients' setting"] = redis_server['maxclients']
+ @metrics['Redis memory used'] = redis_server['used_memory_human']
+ @metrics['Redis memory peak'] = redis_server['used_memory_peak_human']
+ @metrics['Redis total memory available'] = redis_server['total_system_memory_human']
+ @metrics["Redis 'maxmemory' setting"] = redis_server['maxmemory']
+ @metrics["Redis 'maxmemory_policy' setting"] = redis_server['maxmemory_policy']
+ end
+ rescue Redis::CannotConnectError
+ @metrics['Redis alive'] = false
+ end
+end
diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb
index b337e8458..9472dc623 100644
--- a/app/finders/conversation_finder.rb
+++ b/app/finders/conversation_finder.rb
@@ -97,6 +97,8 @@ class ConversationFinder
when 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = @conversations.where(id: conversation_ids)
+ when 'participating'
+ @conversations = current_user.participating_conversations.where(account_id: current_account.id)
when 'unattended'
@conversations = @conversations.where(first_reply_created_at: nil)
end
diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb
new file mode 100644
index 000000000..256a50387
--- /dev/null
+++ b/app/helpers/email_helper.rb
@@ -0,0 +1,6 @@
+module EmailHelper
+ def extract_domain_without_tld(email)
+ domain = email.split('@').last
+ domain.split('.').first
+ end
+end
diff --git a/app/helpers/message_format_helper.rb b/app/helpers/message_format_helper.rb
index 3dd8d8f23..1e89c56c1 100644
--- a/app/helpers/message_format_helper.rb
+++ b/app/helpers/message_format_helper.rb
@@ -2,7 +2,8 @@ module MessageFormatHelper
include RegexHelper
def transform_user_mention_content(message_content)
- message_content.gsub(MENTION_REGEX, '\1')
+ # attachment message without content, message_content is nil
+ message_content.presence ? message_content.gsub(MENTION_REGEX, '\1') : ''
end
def render_message_content(message_content)
diff --git a/app/javascript/dashboard/api/helpCenter/articles.js b/app/javascript/dashboard/api/helpCenter/articles.js
index 62328e8eb..bcbb4dc5b 100644
--- a/app/javascript/dashboard/api/helpCenter/articles.js
+++ b/app/javascript/dashboard/api/helpCenter/articles.js
@@ -46,6 +46,20 @@ class ArticlesAPI extends PortalsAPI {
deleteArticle({ articleId, portalSlug }) {
return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
}
+
+ uploadImage({ portalSlug, file }) {
+ let formData = new FormData();
+ formData.append('background_image', file);
+ return axios.post(
+ `${this.url}/${portalSlug}/articles/attach_file`,
+ formData,
+ {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ }
+ );
+ }
}
export default new ArticlesAPI();
diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js
index f0096cf23..d671b86ff 100644
--- a/app/javascript/dashboard/api/inbox/message.js
+++ b/app/javascript/dashboard/api/inbox/message.js
@@ -80,6 +80,15 @@ class MessageApi extends ApiClient {
params: { before },
});
}
+
+ translateMessage(conversationId, messageId, targetLanguage) {
+ return axios.post(
+ `${this.url}/${conversationId}/messages/${messageId}/translate`,
+ {
+ target_language: targetLanguage,
+ }
+ );
+ }
}
export default new MessageApi();
diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss
index 650fa884d..2f817b125 100644
--- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss
+++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss
@@ -17,9 +17,10 @@
}
.tooltip {
+ background-color: var(--black-transparent);
border-radius: $space-smaller;
font-size: $font-size-mini;
- max-width: 15rem;
+ max-width: var(--space-giga);
padding: $space-smaller $space-small;
z-index: 999;
}
diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue
index 7d4a9239e..6fd51f747 100644
--- a/app/javascript/dashboard/components/ChatList.vue
+++ b/app/javascript/dashboard/components/ChatList.vue
@@ -151,13 +151,14 @@