Compare commits

..

1 commit

Author SHA1 Message Date
Pranav Raj S
e4aa444007 feat: Load Chatwoot SDK after load 2022-06-19 12:49:12 +05:30
2162 changed files with 15929 additions and 115784 deletions

View file

@ -15,12 +15,8 @@ defaults: &defaults
- image: cimg/postgres:14.1 - image: cimg/postgres:14.1
- image: cimg/redis:6.2.6 - image: cimg/redis:6.2.6
environment: environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
- RAILS_LOG_TO_STDOUT: false - RAILS_LOG_TO_STDOUT: false
- COVERAGE: true
- LOG_LEVEL: warn
parallelism: 4
resource_class: large
jobs: jobs:
build: build:
<<: *defaults <<: *defaults
@ -92,7 +88,6 @@ jobs:
fi fi
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.3.0/openapi-generator-cli-5.3.0.jar > ~/tmp/openapi-generator-cli-5.3.0.jar curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.3.0/openapi-generator-cli-5.3.0.jar > ~/tmp/openapi-generator-cli-5.3.0.jar
java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json
# Database setup # Database setup
- run: yarn install --check-files - run: yarn install --check-files
- run: bundle exec rake db:create - run: bundle exec rake db:create
@ -106,10 +101,6 @@ jobs:
name: Rubocop name: Rubocop
command: bundle exec rubocop command: bundle exec rubocop
# - run:
# name: Brakeman
# command: bundle exec brakeman
- run: - run:
name: eslint name: eslint
command: yarn run eslint command: yarn run eslint
@ -118,79 +109,34 @@ jobs:
- run: - run:
name: Run backend tests name: Run backend tests
command: | command: |
mkdir -p ~/tmp/test-results/rspec bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 --format documentation
mkdir -p ~/tmp/test-artifacts ~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
mkdir -p coverage - persist_to_workspace:
~/tmp/cc-test-reporter before-build root: ~/tmp
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) paths:
bundle exec rspec --format progress \ - codeclimate.backend.json
--format RspecJunitFormatter \
--out ~/tmp/test-results/rspec.xml \
-- ${TESTFILES}
no_output_timeout: 30m
- run:
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
- run: - run:
name: Run frontend tests name: Run frontend tests
command: | command: |
mkdir -p ~/tmp/test-results/frontend_specs yarn test:coverage
~/tmp/cc-test-reporter before-build ~/tmp/cc-test-reporter format-coverage -t lcov -o ~/tmp/codeclimate.frontend.json buildreports/lcov.info
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
--out ~/tmp/test-results/yarn.xml \
-- ${TESTFILES}
- run:
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json buildreports/lcov.info
- persist_to_workspace: - persist_to_workspace:
root: coverage root: ~/tmp
paths: paths:
- codeclimate.*.json - codeclimate.frontend.json
# collect reports # collect reports
- store_test_results: - store_test_results:
path: ~/tmp/test-results path: ~/tmp/test-results
- store_artifacts: - store_artifacts:
path: ~/tmp/test-artifacts path: ~/tmp/test-results
destination: test-results
- store_artifacts: - store_artifacts:
path: log path: log
upload-coverage:
working_directory: ~/build
docker:
# specify the version you desire here
- image: circleci/ruby:3.0.2-node-browsers
environment:
- CC_TEST_REPORTER_ID: caf26a895e937974a90860cfadfded20891cfd1373a5aaafb3f67406ab9d433f
steps:
- attach_workspace:
at: ~/build
- run:
name: Download cc-test-reporter
command: |
mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
chmod +x ~/tmp/cc-test-reporter
- persist_to_workspace:
root: ~/tmp
paths:
- cc-test-reporter
- run: - run:
name: Upload coverage results to Code Climate name: Upload coverage results to Code Climate
command: | command: |
~/tmp/cc-test-reporter sum-coverage --output - codeclimate.*.json | ~/tmp/cc-test-reporter upload-coverage --debug --input - ~/tmp/cc-test-reporter sum-coverage ~/tmp/codeclimate.*.json -p 2 -o ~/tmp/codeclimate.total.json
~/tmp/cc-test-reporter upload-coverage -i ~/tmp/codeclimate.total.json
workflows:
version: 2
commit:
jobs:
- build
- upload-coverage:
requires:
- build

View file

@ -53,6 +53,3 @@ exclude_patterns:
- 'app/javascript/dashboard/i18n/index.js' - 'app/javascript/dashboard/i18n/index.js'
- 'app/javascript/widget/i18n/index.js' - 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js' - 'app/javascript/survey/i18n/index.js'
- 'app/javascript/shared/constants/locales.js'
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'

View file

@ -7,8 +7,8 @@ end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = spaces
tab_width = 2 tab_width = 2
[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}] [{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
indent_size = 2 indent_size = 2

View file

@ -3,8 +3,6 @@ SECRET_KEY_BASE=replace_with_lengthy_secure_hex
# Replace with the URL you are planning to use for your app # Replace with the URL you are planning to use for your app
FRONTEND_URL=http://0.0.0.0:3000 FRONTEND_URL=http://0.0.0.0:3000
# To use a dedicated URL for help center pages
# HELPCENTER_URL=http://0.0.0.0:3000
# If the variable is set, all non-authenticated pages would fallback to the default locale. # If the variable is set, all non-authenticated pages would fallback to the default locale.
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en # Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
@ -34,20 +32,12 @@ REDIS_SENTINELS=
# You can find list of master using "SENTINEL masters" command # You can find list of master using "SENTINEL masters" command
REDIS_SENTINEL_MASTER_NAME= REDIS_SENTINEL_MASTER_NAME=
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
# Use the following environment variable to customize passwords for sentinels.
# Use empty string if sentinels are configured with out passwords
# REDIS_SENTINEL_PASSWORD=
# Redis premium breakage in heroku fix # Redis premium breakage in heroku fix
# enable the following configuration # enable the following configuration
# ref: https://github.com/chatwoot/chatwoot/issues/2420 # ref: https://github.com/chatwoot/chatwoot/issues/2420
# REDIS_OPENSSL_VERIFY_MODE=none # REDIS_OPENSSL_VERIFY_MODE=none
# Postgres Database config variables # Postgres Database config variables
# You can leave POSTGRES_DATABASE blank. The default name of
# the database in the production environment is chatwoot_production
# POSTGRES_DATABASE=
POSTGRES_HOST=postgres POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD= POSTGRES_PASSWORD=
@ -56,14 +46,14 @@ RAILS_MAX_THREADS=5
# The email from which all outgoing emails are sent # The email from which all outgoing emails are sent
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>` # could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com> MAILER_SENDER_EMAIL="Chatwoot <accounts@chatwoot.com>"
#SMTP domain key is set up for HELO checking #SMTP domain key is set up for HELO checking
SMTP_DOMAIN=chatwoot.com SMTP_DOMAIN=chatwoot.com
# Set the value to "mailhog" if using docker-compose for development environments, # the default value is set "mailhog" and is used by docker-compose for development environments,
# Set the value as "localhost" or your SMTP address in other environments # Set the value as "localhost" or your SMTP address in other environments
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix) SMTP_ADDRESS=mailhog
SMTP_ADDRESS=
SMTP_PORT=1025 SMTP_PORT=1025
SMTP_USERNAME= SMTP_USERNAME=
SMTP_PASSWORD= SMTP_PASSWORD=
@ -103,6 +93,7 @@ AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
AWS_REGION= AWS_REGION=
# Log settings # Log settings
# Disable if you want to write logs to a file # Disable if you want to write logs to a file
RAILS_LOG_TO_STDOUT=true RAILS_LOG_TO_STDOUT=true
@ -139,6 +130,7 @@ ANDROID_BUNDLE_ID=com.chatwoot.app
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section) # https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:D4:5D:D4:53:F8:3B:FB:D3:C6:28:64:1D:AA:08:1E:D8 ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:D4:5D:D4:53:F8:3B:FB:D3:C6:28:64:1D:AA:08:1E:D8
### Smart App Banner ### Smart App Banner
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html # https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
# You can find your app-id in https://itunesconnect.apple.com # You can find your app-id in https://itunesconnect.apple.com
@ -155,12 +147,8 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Bot Customizations ## Bot Customizations
USE_INBOX_AVATAR_FOR_BOT=true USE_INBOX_AVATAR_FOR_BOT=true
### APM and Error Monitoring configurations
## Elastic APM
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
# ELASTIC_APM_SERVER_URL=
# ELASTIC_APM_SECRET_TOKEN=
### APM and Error Monitoring configurations
## Sentry ## Sentry
# SENTRY_DSN= # SENTRY_DSN=
@ -181,6 +169,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables ## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
# DD_TRACE_AGENT_URL= # DD_TRACE_AGENT_URL=
## IP look up configuration ## IP look up configuration
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md ## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
## works only on accounts with ip look up feature enabled ## works only on accounts with ip look up feature enabled
@ -192,6 +181,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
## To prevent and throttle abusive requests ## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=true # ENABLE_RACK_ATTACK=true
## Running chatwoot as an API only server ## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints ## setting this value to true will disable the frontend dashboard endpoints
# CW_API_ONLY_SERVER=false # CW_API_ONLY_SERVER=false
@ -205,11 +195,3 @@ USE_INBOX_AVATAR_FOR_BOT=true
# If you want to use official mobile app, # If you want to use official mobile app,
# the notifications would be relayed via a Chatwoot server # the notifications would be relayed via a Chatwoot server
ENABLE_PUSH_RELAY_SERVER=true ENABLE_PUSH_RELAY_SERVER=true
# Stripe API key
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# Set to true if you want to upload files to cloud storage using the signed url
# Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true.
DIRECT_UPLOADS_ENABLED=

View file

@ -1,10 +1,5 @@
module.exports = { module.exports = {
extends: [ extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
'airbnb-base/legacy',
'prettier',
'plugin:vue/recommended',
'plugin:storybook/recommended',
],
parserOptions: { parserOptions: {
parser: 'babel-eslint', parser: 'babel-eslint',
ecmaVersion: 2020, ecmaVersion: 2020,

View file

@ -6,7 +6,6 @@ labels: 'Bug'
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@ -17,11 +16,11 @@ Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See the error 4. See error
**Expected behavior** **Expected behavior**
Share a clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
@ -29,50 +28,27 @@ If applicable, add screenshots to help explain your problem.
**Browser logs** **Browser logs**
Share the browser logs to debug the issue further. Share the browser logs to debug the issue further
**Server logs** **Server logs**
Share the server logs to debug the issue further. Share the server logs to debug the issue further
**Environment** **Environment**
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other). Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self hosted installation of Chatwoot. If you are using a self hosted installation of Chatwoot describe the type of deployment (Docker/Linux VM installation/Heroku)
- [ ] app.chatwoot.com (Chatwoot Cloud) **Desktop (please complete the following information):**
- [ ] Self-hosted - OS: [e.g. iOS]
- - [ ] Linux VM - Browser [e.g. chrome, safari]
- - [ ] Docker
- - [ ] Kubernetes
- - [ ] Heroku
- - [ ] Other (Please specify)
**Desktop (please complete the following information)** (If applicable)
- OS: [e.g. Linux, Windows, MacOS]
- Browser [e.g. chrome, firefox, safari]
- Version [e.g. 22] - Version [e.g. 22]
**Smartphone (please complete the following information)** (If applicable) **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6, Pixel7] - Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1] - OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, firefox, safari] - Browser [e.g. stock browser, safari]
- Version [e.g. 22] - Version [e.g. 22]
**Docker** (If applicable)
Please share the output of the following.
- `docker version`
- `docker info`
- `docker-compose version`
**Cloud Provider** (If applicable)
- [ ] AWS
- [ ] GCP
- [ ] Azure
- [ ] DigitalOcean
- [ ] Others
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View file

@ -2,7 +2,8 @@
## Description ## Description
Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires. Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue) Fixes # (issue)
## Type of change ## Type of change
@ -11,18 +12,18 @@ Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update - [ ] This change requires a documentation update
## How Has This Been Tested? ## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
## Checklist: ## Checklist:
- [ ] My code follows the style guidelines of this project - [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code - [ ] I have performed a self-review of my own code
- [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have commented on my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation - [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings - [ ] My changes generate no new warnings

View file

@ -1,36 +0,0 @@
# We often have cases where users would comment over stale closed Github Issues.
# This creates unnecessary noise for the original reporter and makes it harder for triaging.
# This action locks the closed threads once it is inactive for over a month.
name: 'Lock Threads'
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v3
with:
issue-inactive-days: '30'
issue-lock-reason: 'resolved'
issue-comment: >
This issue has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
pr-inactive-days: '30'
pr-lock-reason: 'resolved'
pr-comment: >
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.

View file

@ -1,46 +0,0 @@
# #
# #
# # Linux nightly installer action
# # This action will try to install and setup
# # chatwoot on an Ubuntu 20.04 machine using
# # the linux installer script.
# #
# # This is set to run daily at midnight.
# #
name: Run Linux nightly installer
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
nightly:
runs-on: ubuntu-20.04
steps:
- name: get installer
run: |
wget https://get.chatwoot.app/linux/install.sh
chmod +x install.sh
#fix for postgtres not starting automatically in gh action env
sed -i '/function configure_db() {/a sudo service postgresql start' install.sh
- name: create input file
run: |
echo "no" > input
echo "yes" >> input
- name: Run the installer
run: |
sudo ./install.sh --install < input
# disabling http verify for now as http
# access to port 3000 fails in gh action env
# - name: Verify
# if: always()
# run: |
# sudo netstat -ntlp | grep 3000
# sudo systemctl restart chatwoot.target
# curl http://localhost:3000/api

View file

@ -58,6 +58,5 @@ jobs:
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
platforms: linux/amd64
push: true push: true
tags: ${{ env.DOCKER_TAG }} tags: ${{ env.DOCKER_TAG }}

5
.gitignore vendored
View file

@ -39,6 +39,9 @@ public/packs*
*.un~ *.un~
.jest-cache .jest-cache
#VS Code files
.vscode
# ignore jetbrains IDE files # ignore jetbrains IDE files
.idea .idea
@ -60,5 +63,3 @@ test/cypress/videos/*
/config/master.key /config/master.key
/config/*.enc /config/*.enc
.vscode/settings.json

View file

@ -183,5 +183,3 @@ AllCops:
- db/migrate/20200503151130_add_account_feature_flag.rb - db/migrate/20200503151130_add_account_feature_flag.rb
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb - db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb - db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
- db/migrate/20220809104508_revert_cascading_indexes.rb

View file

@ -4,7 +4,6 @@ import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import Vuelidate from 'vuelidate'; import Vuelidate from 'vuelidate';
import Multiselect from 'vue-multiselect'; import Multiselect from 'vue-multiselect';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import WootUiKit from '../app/javascript/dashboard/components'; import WootUiKit from '../app/javascript/dashboard/components';
import i18n from '../app/javascript/dashboard/i18n'; import i18n from '../app/javascript/dashboard/i18n';
@ -16,7 +15,6 @@ Vue.use(Vuelidate);
Vue.use(WootUiKit); Vue.use(WootUiKit);
Vue.use(Vuex); Vue.use(Vuex);
Vue.component('multiselect', Multiselect); Vue.component('multiselect', Multiselect);
Vue.component('fluent-icon', FluentIcon);
const store = new Vuex.Store({}); const store = new Vuex.Store({});
const i18nConfig = new VueI18n({ const i18nConfig = new VueI18n({

View file

@ -1,32 +0,0 @@
{
"recommendations": [
// Spell check
"streetsidesoftware.code-spell-checker",
// Better Comments
"aaron-bond.better-comments",
// Rails Test Runner
"davidpallinder.rails-test-runner",
// Eslint
"dbaeumer.vscode-eslint",
// Auto Close Tag
"formulahendry.auto-close-tag",
// Auto Rename Tag
"formulahendry.auto-rename-tag",
// Hight light colors
"naumovs.color-highlight",
// GitLens
"eamodio.gitlens",
// Ruby
"rebornix.ruby",
// Vue
"octref.vetur",
// Prettier
"esbenp.prettier-vscode",
// Dot Env
"mikestead.dotenv",
// HTML CSS Support
"ecmel.vscode-html-css",
// Tailwind CSS Intellisense
"bradlc.vscode-tailwindcss",
]
}

View file

@ -1 +0,0 @@
{}

27
Gemfile
View file

@ -4,7 +4,7 @@ ruby '3.0.4'
##-- base gems for rails --## ##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors' gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 6.1', '>= 6.1.6.1' gem 'rails'
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false gem 'bootsnap', require: false
@ -56,7 +56,7 @@ gem 'activerecord-import'
gem 'dotenv-rails' gem 'dotenv-rails'
gem 'foreman' gem 'foreman'
gem 'puma' gem 'puma'
gem 'webpacker', '~> 5.4', '>= 5.4.3' gem 'webpacker', '~> 5.x'
# metrics on heroku # metrics on heroku
gem 'barnes' gem 'barnes'
@ -78,7 +78,7 @@ gem 'wisper', '2.0.0'
# TODO: bump up gem to 2.0 # TODO: bump up gem to 2.0
gem 'facebook-messenger' gem 'facebook-messenger'
gem 'line-bot-api' gem 'line-bot-api'
gem 'twilio-ruby', '~> 5.66' gem 'twilio-ruby', '~> 5.32.0'
# twitty will handle subscription of twitter account events # twitty will handle subscription of twitter account events
# gem 'twitty', git: 'https://github.com/chatwoot/twitty' # gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty' gem 'twitty'
@ -89,12 +89,15 @@ gem 'slack-ruby-client'
# for dialogflow integrations # for dialogflow integrations
gem 'google-cloud-dialogflow' gem 'google-cloud-dialogflow'
##--- gems for debugging and error reporting ---##
# static analysis
gem 'brakeman'
##-- apm and error monitoring ---# ##-- apm and error monitoring ---#
gem 'ddtrace' gem 'ddtrace'
gem 'elastic-apm'
gem 'newrelic_rpm' gem 'newrelic_rpm'
gem 'scout_apm' gem 'scout_apm'
gem 'sentry-rails', '~> 5.3', '>= 5.3.1' gem 'sentry-rails', '~> 5.3'
gem 'sentry-ruby', '~> 5.3' gem 'sentry-ruby', '~> 5.3'
gem 'sentry-sidekiq', '~> 5.3' gem 'sentry-sidekiq', '~> 5.3'
@ -128,13 +131,6 @@ gem 'working_hours'
# full text search for articles # full text search for articles
gem 'pg_search' gem 'pg_search'
# Subscriptions, Billing
gem 'stripe'
## - helper gems --##
## to populate db with sample data
gem 'faker'
group :production, :staging do group :production, :staging do
# we dont want request timing out in development while using byebug # we dont want request timing out in development while using byebug
gem 'rack-timeout' gem 'rack-timeout'
@ -164,18 +160,15 @@ end
group :development, :test do group :development, :test do
gem 'active_record_query_trace' gem 'active_record_query_trace'
##--- gems for debugging and error reporting ---##
# static analysis
gem 'brakeman'
gem 'bundle-audit', require: false gem 'bundle-audit', require: false
gem 'byebug', platform: :mri gem 'byebug', platform: :mri
gem 'climate_control' gem 'climate_control'
gem 'factory_bot_rails' gem 'factory_bot_rails'
gem 'faker'
gem 'listen' gem 'listen'
gem 'mock_redis' gem 'mock_redis'
gem 'pry-rails' gem 'pry-rails'
gem 'rspec_junit_formatter' gem 'rspec-rails', '~> 5.0.0'
gem 'rspec-rails', '~> 5.0.3'
gem 'rubocop', require: false gem 'rubocop', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false gem 'rubocop-rails', require: false

View file

@ -9,63 +9,63 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.6.1) actioncable (6.1.5.1)
actionpack (= 6.1.6.1) actionpack (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.6.1) actionmailbox (6.1.5.1)
actionpack (= 6.1.6.1) actionpack (= 6.1.5.1)
activejob (= 6.1.6.1) activejob (= 6.1.5.1)
activerecord (= 6.1.6.1) activerecord (= 6.1.5.1)
activestorage (= 6.1.6.1) activestorage (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.6.1) actionmailer (6.1.5.1)
actionpack (= 6.1.6.1) actionpack (= 6.1.5.1)
actionview (= 6.1.6.1) actionview (= 6.1.5.1)
activejob (= 6.1.6.1) activejob (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.6.1) actionpack (6.1.5.1)
actionview (= 6.1.6.1) actionview (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.6.1) actiontext (6.1.5.1)
actionpack (= 6.1.6.1) actionpack (= 6.1.5.1)
activerecord (= 6.1.6.1) activerecord (= 6.1.5.1)
activestorage (= 6.1.6.1) activestorage (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.6.1) actionview (6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (6.1.6.1) activejob (6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.6.1) activemodel (6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
activerecord (6.1.6.1) activerecord (6.1.5.1)
activemodel (= 6.1.6.1) activemodel (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
activerecord-import (1.4.0) activerecord-import (1.3.0)
activerecord (>= 4.2) activerecord (>= 4.2)
activestorage (6.1.6.1) activestorage (6.1.5.1)
actionpack (= 6.1.6.1) actionpack (= 6.1.5.1)
activejob (= 6.1.6.1) activejob (= 6.1.5.1)
activerecord (= 6.1.6.1) activerecord (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.6.1) activesupport (6.1.5.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -91,20 +91,20 @@ GEM
ast (2.4.2) ast (2.4.2)
attr_extras (6.2.5) attr_extras (6.2.5)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.605.0) aws-partitions (1.556.0)
aws-sdk-core (3.131.2) aws-sdk-core (3.126.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1.0)
aws-sdk-kms (1.57.0) aws-sdk-kms (1.54.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.126.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0) aws-sdk-s3 (1.112.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.0) aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@ -117,31 +117,31 @@ GEM
barnes (0.0.9) barnes (0.0.9)
multi_json (~> 1) multi_json (~> 1)
statsd-ruby (~> 1.1) statsd-ruby (~> 1.1)
bcrypt (3.1.18) bcrypt (3.1.16)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.12.0) bootsnap (1.10.3)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (5.2.3) brakeman (5.2.1)
browser (5.3.1) browser (5.3.1)
builder (3.2.4) builder (3.2.4)
bullet (7.0.2) bullet (7.0.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundle-audit (0.1.0) bundle-audit (0.1.0)
bundler-audit bundler-audit
bundler-audit (0.9.1) bundler-audit (0.9.0.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
byebug (11.1.3) byebug (11.1.3)
climate_control (1.1.1) climate_control (1.0.1)
coderay (1.1.3) coderay (1.1.3)
commonmarker (0.23.6) commonmarker (0.23.4)
concurrent-ruby (1.1.10) concurrent-ruby (1.1.10)
connection_pool (2.2.5) connection_pool (2.2.5)
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
cypress-on-rails (1.13.1) cypress-on-rails (1.12.1)
rack rack
database_cleaner (2.0.1) database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0) database_cleaner-active_record (~> 2.0.0)
@ -151,12 +151,10 @@ GEM
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
datetime_picker_rails (0.0.7) datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1) momentjs-rails (>= 2.8.1)
ddtrace (1.2.0) ddtrace (0.54.2)
debase-ruby_core_source (= 0.10.16) debase-ruby_core_source (<= 0.10.14)
libddprof (~> 0.6.0.1.0)
libddwaf (~> 1.3.0.2.0)
msgpack msgpack
debase-ruby_core_source (0.10.16) debase-ruby_core_source (0.10.14)
declarative (0.0.20) declarative (0.0.20)
devise (4.8.1) devise (4.8.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
@ -178,13 +176,10 @@ GEM
dotenv-rails (2.7.6) dotenv-rails (2.7.6)
dotenv (= 2.7.6) dotenv (= 2.7.6)
railties (>= 3.2) railties (>= 3.2)
down (5.3.1) down (5.3.0)
addressable (~> 2.8) addressable (~> 2.8)
ecma-re-validator (0.4.0) ecma-re-validator (0.4.0)
regexp_parser (~> 2.2) regexp_parser (~> 2.2)
elastic-apm (4.5.1)
concurrent-ruby (~> 1.0)
http (>= 3.0)
email_reply_trimmer (0.1.13) email_reply_trimmer (0.1.13)
erubi (1.10.0) erubi (1.10.0)
et-orbi (1.2.7) et-orbi (1.2.7)
@ -193,63 +188,36 @@ GEM
facebook-messenger (2.0.1) facebook-messenger (2.0.1)
httparty (~> 0.13, >= 0.13.7) httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5) rack (>= 1.4.5)
factory_bot (6.2.1) factory_bot (6.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.2.0) factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (2.21.0) faker (2.19.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.6, < 2)
faraday (1.10.0) faraday (1.0.1)
faraday-em_http (~> 1.0) multipart-post (>= 1.2, < 3)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fcm (1.0.8) fcm (1.0.5)
faraday (>= 1.0.0, < 3.0) faraday (~> 1)
googleauth (~> 1)
ffi (1.15.5) ffi (1.15.5)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
foreman (0.87.2) foreman (0.87.2)
fugit (1.5.3) fugit (1.5.3)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
gapic-common (0.10.0) gapic-common (0.3.4)
faraday (>= 1.9, < 3.a) google-protobuf (~> 3.12, >= 3.12.2)
faraday-retry (>= 1.0, < 3.a) googleapis-common-protos (>= 1.3.9, < 2.0)
google-protobuf (~> 3.14) googleapis-common-protos-types (>= 1.0.4, < 2.0)
googleapis-common-protos (>= 1.3.12, < 2.a) googleauth (~> 0.9)
googleapis-common-protos-types (>= 1.3.1, < 2.a) grpc (~> 1.25)
googleauth (~> 1.0) geocoder (1.7.3)
grpc (~> 1.36)
geocoder (1.8.0)
gli (2.21.0) gli (2.21.0)
globalid (1.0.0) globalid (1.0.0)
activesupport (>= 5.0) activesupport (>= 5.0)
google-apis-core (0.7.0) google-apis-core (0.4.2)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -258,27 +226,23 @@ GEM
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick webrick
google-apis-iamcredentials_v1 (0.13.0) google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a) google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.18.0) google-apis-storage_v1 (0.11.0)
google-apis-core (>= 0.7, < 2.a) google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-dialogflow (1.5.0) google-cloud-dialogflow (1.2.0)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.5)
google-cloud-dialogflow-v2 (>= 0.15, < 2.a) google-cloud-dialogflow-v2 (~> 0.1)
google-cloud-dialogflow-v2 (0.17.0) google-cloud-dialogflow-v2 (0.6.4)
gapic-common (>= 0.10, < 2.a) gapic-common (~> 0.3)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-location (>= 0.0, < 2.a) google-cloud-env (1.5.0)
google-cloud-env (1.6.0) faraday (>= 0.17.3, < 2.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0) google-cloud-errors (1.2.0)
google-cloud-location (0.2.0) google-cloud-storage (1.36.1)
gapic-common (>= 0.10, < 2.a)
google-cloud-errors (~> 1.0)
google-cloud-storage (1.37.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
@ -286,32 +250,32 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
google-protobuf (3.21.7) google-protobuf (3.19.4)
google-protobuf (3.21.7-x86_64-darwin) google-protobuf (3.19.4-x86_64-darwin)
google-protobuf (3.21.7-x86_64-linux) google-protobuf (3.19.4-x86_64-linux)
googleapis-common-protos (1.3.12) googleapis-common-protos (1.3.12)
google-protobuf (~> 3.14) google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2) googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27) grpc (~> 1.27)
googleapis-common-protos-types (1.3.2) googleapis-common-protos-types (1.3.0)
google-protobuf (~> 3.14) google-protobuf (~> 3.14)
googleauth (1.2.0) googleauth (0.17.1)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (~> 0.15)
groupdate (6.1.0) groupdate (6.0.1)
activesupport (>= 5.2) activesupport (>= 5.2)
grpc (1.47.0) grpc (1.43.1)
google-protobuf (~> 3.19) google-protobuf (~> 3.18)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.47.0-x86_64-darwin) grpc (1.43.1-universal-darwin)
google-protobuf (~> 3.19) google-protobuf (~> 3.18)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.47.0-x86_64-linux) grpc (1.43.1-x86_64-linux)
google-protobuf (~> 3.19) google-protobuf (~> 3.18)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1) haikunator (1.1.1)
hairtrigger (0.2.25) hairtrigger (0.2.25)
@ -324,20 +288,14 @@ GEM
hkdf (0.3.0) hkdf (0.3.0)
html2text (0.2.1) html2text (0.2.1)
nokogiri (~> 1.6) nokogiri (~> 1.6)
http (5.1.0)
addressable (~> 2.8)
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.4.0)
http-accept (1.7.0) http-accept (1.7.0)
http-cookie (1.0.5) http-cookie (1.0.4)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.3.0)
httparty (0.20.0) httparty (0.20.0)
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.11.0) i18n (1.10.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
@ -346,19 +304,19 @@ GEM
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jmespath (1.6.1) jmespath (1.6.1)
jquery-rails (4.5.0) jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (2.6.2) json (2.6.1)
json_refs (0.1.7) json_refs (0.1.7)
hana hana
json_schemer (0.2.21) json_schemer (0.2.19)
ecma-re-validator (~> 0.3) ecma-re-validator (~> 0.3)
hana (~> 1.3) hana (~> 1.3)
regexp_parser (~> 2.0) regexp_parser (~> 2.0)
uri_template (~> 0.7) uri_template (~> 0.7)
jwt (2.4.1) jwt (2.3.0)
kaminari (1.2.2) kaminari (1.2.2)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2) kaminari-actionview (= 1.2.2)
@ -371,34 +329,21 @@ GEM
activerecord activerecord
kaminari-core (= 1.2.2) kaminari-core (= 1.2.2)
kaminari-core (1.2.2) kaminari-core (1.2.2)
koala (3.2.0) koala (3.1.0)
addressable addressable
faraday (< 2) faraday (< 2)
json (>= 1.8) json (>= 1.8)
rexml rexml
launchy (2.5.0) launchy (2.5.0)
addressable (~> 2.7) addressable (~> 2.7)
letter_opener (1.8.1) letter_opener (1.7.0)
launchy (>= 2.2, < 3) launchy (~> 2.2)
libddprof (0.6.0.1.0) line-bot-api (1.23.0)
libddprof (0.6.0.1.0-x86_64-linux) liquid (5.1.0)
libddwaf (1.3.0.2.0)
ffi (~> 1.0)
libddwaf (1.3.0.2.0-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.3.0.2.0-x86_64-darwin)
ffi (~> 1.0)
libddwaf (1.3.0.2.0-x86_64-linux)
ffi (~> 1.0)
line-bot-api (1.25.0)
liquid (5.3.0)
listen (3.7.1) listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
llhttp-ffi (0.4.0) loofah (2.17.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
loofah (2.19.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
@ -413,36 +358,36 @@ GEM
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.0) mini_portile2 (2.8.0)
minitest (5.16.2) minitest (5.15.0)
mock_redis (0.32.0) mock_redis (0.30.0)
ruby2_keywords ruby2_keywords
momentjs-rails (2.29.1.1) momentjs-rails (2.29.1.1)
railties (>= 3.1) railties (>= 3.1)
msgpack (1.5.3) msgpack (1.4.5)
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.2.3) multipart-post (2.1.1)
net-http-persistent (4.0.1) net-http-persistent (4.0.1)
connection_pool (~> 2.2) connection_pool (~> 2.2)
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (8.9.0) newrelic_rpm (8.7.0)
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.13.10) nokogiri (1.13.6)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-arm64-darwin) nokogiri (1.13.6-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-x86_64-darwin) nokogiri (1.13.6-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-x86_64-linux) nokogiri (1.13.6-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oauth (0.5.10) oauth (0.5.8)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
parallel (1.22.1) parallel (1.21.0)
parser (3.1.2.0) parser (3.1.1.0)
ast (~> 2.4.1) ast (~> 2.4.1)
pg (1.4.1) pg (1.3.2)
pg_search (2.3.6) pg_search (2.3.6)
activerecord (>= 5.2) activerecord (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
@ -453,46 +398,46 @@ GEM
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.7) public_suffix (4.0.6)
puma (5.6.4) puma (5.6.4)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.2.0) pundit (2.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.1) racc (1.6.0)
rack (2.2.4) rack (2.2.3.1)
rack-attack (6.6.1) rack-attack (6.6.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-proxy (0.7.2) rack-proxy (0.7.2)
rack rack
rack-test (2.0.2) rack-test (1.1.0)
rack (>= 1.3) rack (>= 1.0, < 3)
rack-timeout (0.6.3) rack-timeout (0.6.0)
rails (6.1.6.1) rails (6.1.5.1)
actioncable (= 6.1.6.1) actioncable (= 6.1.5.1)
actionmailbox (= 6.1.6.1) actionmailbox (= 6.1.5.1)
actionmailer (= 6.1.6.1) actionmailer (= 6.1.5.1)
actionpack (= 6.1.6.1) actionpack (= 6.1.5.1)
actiontext (= 6.1.6.1) actiontext (= 6.1.5.1)
actionview (= 6.1.6.1) actionview (= 6.1.5.1)
activejob (= 6.1.6.1) activejob (= 6.1.5.1)
activemodel (= 6.1.6.1) activemodel (= 6.1.5.1)
activerecord (= 6.1.6.1) activerecord (= 6.1.5.1)
activestorage (= 6.1.6.1) activestorage (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.6.1) railties (= 6.1.5.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.4.4) rails-html-sanitizer (1.4.2)
loofah (~> 2.19, >= 2.19.1) loofah (~> 2.3)
railties (6.1.6.1) railties (6.1.5.1)
actionpack (= 6.1.6.1) actionpack (= 6.1.5.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.5.1)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@ -501,11 +446,11 @@ GEM
rb-fsevent (0.11.1) rb-fsevent (0.11.1)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redis (4.7.1) redis (4.6.0)
redis-namespace (1.8.2) redis-namespace (1.8.1)
redis (>= 3.0.4) redis (>= 3.0.4)
regexp_parser (2.5.0) regexp_parser (2.2.1)
representable (3.2.0) representable (3.1.1)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
@ -524,7 +469,7 @@ GEM
rspec-expectations (3.11.0) rspec-expectations (3.11.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0) rspec-support (~> 3.11.0)
rspec-mocks (3.11.1) rspec-mocks (3.11.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0) rspec-support (~> 3.11.0)
rspec-rails (5.0.3) rspec-rails (5.0.3)
@ -536,29 +481,26 @@ GEM
rspec-mocks (~> 3.10) rspec-mocks (~> 3.10)
rspec-support (~> 3.10) rspec-support (~> 3.10)
rspec-support (3.11.0) rspec-support (3.11.0)
rspec_junit_formatter (0.6.0) rubocop (1.25.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.31.2)
json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.1.0.0) parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml
rubocop-ast (>= 1.18.0, < 2.0) rubocop-ast (>= 1.15.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.19.1) rubocop-ast (1.16.0)
parser (>= 3.1.1.0) parser (>= 3.1.1.0)
rubocop-performance (1.14.2) rubocop-performance (1.13.2)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.15.2) rubocop-rails (2.13.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-rspec (2.12.1) rubocop-rspec (2.8.0)
rubocop (~> 1.31) rubocop (~> 1.19)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-vips (2.1.4) ruby-vips (2.1.4)
ffi (~> 1.12) ffi (~> 1.12)
@ -566,7 +508,7 @@ GEM
ruby2ruby (2.4.4) ruby2ruby (2.4.4)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.6) sexp_processor (~> 4.6)
ruby_parser (3.19.1) ruby_parser (3.18.1)
sexp_processor (~> 4.16) sexp_processor (~> 4.16)
sassc (2.4.0) sassc (2.4.0)
ffi (~> 1.9) ffi (~> 1.9)
@ -576,37 +518,37 @@ GEM
sprockets (> 3.0) sprockets (> 3.0)
sprockets-rails sprockets-rails
tilt tilt
scout_apm (5.2.0) scout_apm (5.1.1)
parser parser
seed_dump (3.3.1) seed_dump (3.3.1)
activerecord (>= 4) activerecord (>= 4)
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0) semantic_range (3.0.0)
sentry-rails (5.3.1) sentry-rails (5.3.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby-core (~> 5.3.1) sentry-ruby-core (~> 5.3.0)
sentry-ruby (5.3.1) sentry-ruby (5.3.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-ruby-core (= 5.3.1) sentry-ruby-core (= 5.3.0)
sentry-ruby-core (5.3.1) sentry-ruby-core (5.3.0)
concurrent-ruby concurrent-ruby
sentry-sidekiq (5.3.1) sentry-sidekiq (5.3.0)
sentry-ruby-core (~> 5.3.1) sentry-ruby-core (~> 5.3.0)
sidekiq (>= 3.0) sidekiq (>= 3.0)
sexp_processor (4.16.1) sexp_processor (4.16.0)
shoulda-matchers (5.1.0) shoulda-matchers (5.1.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (6.4.2) sidekiq (6.4.1)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.2.0) redis (>= 4.2.0)
sidekiq-cron (1.6.0) sidekiq-cron (1.4.0)
fugit (~> 1) fugit (~> 1)
sidekiq (>= 4.2.1) sidekiq (>= 4.2.1)
signet (0.17.0) signet (0.16.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simplecov (0.17.1) simplecov (0.17.1)
@ -624,7 +566,7 @@ GEM
spring-watcher-listen (2.0.1) spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0) spring (>= 1.2, < 3.0)
sprockets (4.1.1) sprockets (4.0.3)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.4.2) sprockets-rails (3.4.2)
@ -633,32 +575,31 @@ GEM
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
squasher (0.6.2) squasher (0.6.2)
statsd-ruby (1.5.0) statsd-ruby (1.5.0)
stripe (6.5.0) telephone_number (1.4.13)
telephone_number (1.4.16)
thor (1.2.1) thor (1.2.1)
tilt (2.0.10) tilt (2.0.10)
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
i18n i18n
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
twilio-ruby (5.68.0) twilio-ruby (5.32.0)
faraday (>= 0.9, < 3.0) faraday (~> 1.0.0)
jwt (>= 1.5, <= 2.5) jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0) nokogiri (>= 1.6, < 2.0)
twitty (0.1.4) twitty (0.1.4)
oauth oauth
tzinfo (2.0.4) tzinfo (2.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2022.1) tzinfo-data (1.2021.5)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
uglifier (4.2.0) uglifier (4.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.8)
unicode-display_width (2.2.0) unicode-display_width (2.1.0)
uniform_notifier (1.16.0) uniform_notifier (1.14.2)
uri_template (0.7.0) uri_template (0.7.0)
valid_email2 (4.0.3) valid_email2 (4.0.3)
activemodel (>= 3.2) activemodel (>= 3.2)
@ -690,7 +631,7 @@ GEM
working_hours (1.4.1) working_hours (1.4.1)
activesupport (>= 3.2) activesupport (>= 3.2)
tzinfo tzinfo
zeitwerk (2.6.0) zeitwerk (2.5.4)
PLATFORMS PLATFORMS
arm64-darwin-20 arm64-darwin-20
@ -726,7 +667,6 @@ DEPENDENCIES
devise_token_auth devise_token_auth
dotenv-rails dotenv-rails
down (~> 5.0) down (~> 5.0)
elastic-apm
email_reply_trimmer email_reply_trimmer
facebook-messenger facebook-messenger
factory_bot_rails factory_bot_rails
@ -765,20 +705,19 @@ DEPENDENCIES
rack-attack rack-attack
rack-cors rack-cors
rack-timeout rack-timeout
rails (~> 6.1, >= 6.1.6.1) rails
redis redis
redis-namespace redis-namespace
responders responders
rest-client rest-client
rspec-rails (~> 5.0.3) rspec-rails (~> 5.0.0)
rspec_junit_formatter
rubocop rubocop
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
scout_apm scout_apm
seed_dump seed_dump
sentry-rails (~> 5.3, >= 5.3.1) sentry-rails (~> 5.3)
sentry-ruby (~> 5.3) sentry-ruby (~> 5.3)
sentry-sidekiq (~> 5.3) sentry-sidekiq (~> 5.3)
shoulda-matchers shoulda-matchers
@ -789,17 +728,16 @@ DEPENDENCIES
spring spring
spring-watcher-listen spring-watcher-listen
squasher squasher
stripe
telephone_number telephone_number
time_diff time_diff
twilio-ruby (~> 5.66) twilio-ruby (~> 5.32.0)
twitty twitty
tzinfo-data tzinfo-data
uglifier uglifier
valid_email2 valid_email2
web-console web-console
webmock webmock
webpacker (~> 5.4, >= 5.4.3) webpacker (~> 5.x)
webpush webpush
wisper (= 2.0.0) wisper (= 2.0.0)
working_hours working_hours
@ -808,4 +746,4 @@ RUBY VERSION
ruby 3.0.4p208 ruby 3.0.4p208
BUNDLED WITH BUNDLED WITH
2.3.16 2.3.15

View file

@ -16,7 +16,7 @@
___ ___
<p align="center"> <p align="center">
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a> <a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/80f9e1a7c72d186289ad/maintainability" alt="Maintainability"></a>
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge"> <img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a> <a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a> <a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a>
@ -26,7 +26,6 @@ ___
<a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a> <a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a>
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a> <a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a>
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a> <a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a>
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p> </p>
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/> <img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>

View file

@ -1,55 +1,30 @@
Chatwoot is looking forward to working with security researchers worldwide to keep Chatwoot and our users safe. If you have found an issue in our systems/applications, please reach out to us. # Security Policy
Chatwoot is looking forward to working with security researchers across the world to keep Chatwoot and our users safe. If you have found an issue in our systems/applications, please reach out to us.
## Reporting a Vulnerability ## Reporting a Vulnerability
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose). This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts. We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose).
If you have any questions about the process, contact security@chatwoot.com. This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
Please try your best to describe a clear and realistic impact for your report, and please don't open any public issues on GitHub or social media; we're doing our best to respond through Huntr as quickly as possible. If you have any questions about the process, feel free to reach out to security@chatwoot.com.
> Note: Please use the email for questions related to the process. Disclosures should be done via [huntr.dev](https://huntr.dev/)
## Supported versions
| Version | Supported |
| ------- | -------------- |
| latest | ️✅ |
| <latest | |
## Vulnerabilities we care about 🫣 ## Out of scope
> Note: Please do not perform testing against Chatwoot production services. Use a `self-hosted instance` to perform tests.
- Remote command execution
- SQL Injection
- Authentication bypass
- Privilege Escalation
- Cross-site scripting (XSS)
- Performing limited admin actions without authorization
- CSRF
You can learn more about our triaging process [here](https://www.chatwoot.com/docs/contributing-guide/security-reports). Please do not perform testing against Chatwoot production services. Use a self hosted instance to perform tests.
## Non-Qualifying Vulnerabilities We consider the following to be out of scope, though there may be exceptions.
We consider the following out of scope, though there may be exceptions.
- Missing HTTP security headers - Missing HTTP security headers
- Incomplete/Missing SPF/DKIM - Self XSS
- Reports from automated tools or scanners - HTTP Host Header XSS without working proof-of-concept
- Theoretical attacks without proof of exploitability
- Social engineering
- Reflected file download
- Physical attacks
- Weak SSL/TLS/SSH algorithms or protocols
- Attacks involving physical access to a user's device or a device or network that's already seriously compromised (e.g., man-in-the-middle).
- The user attacks themselves
- Incomplete/Missing SPF/DKIM - Incomplete/Missing SPF/DKIM
- Denial of Service attacks - Denial of Service attacks
- Brute force attacks
- DNSSEC - DNSSEC
- Social Engineering attacks
If you are unsure about the scope, please create a [report](https://huntr.dev/repos/chatwoot/chatwoot/). If you are not sure about the scope, please create a report.
## Thanks ## Thanks

View file

@ -1 +0,0 @@
2.2.0

View file

@ -1 +0,0 @@
2.1.0

View file

@ -41,24 +41,15 @@
"formation": { "formation": {
"web": { "web": {
"quantity": 1, "quantity": 1,
"size": "basic" "size": "FREE"
}, },
"worker": { "worker": {
"quantity": 1, "quantity": 1,
"size": "basic" "size": "FREE"
} }
}, },
"stack": "heroku-20",
"image": "heroku/ruby", "image": "heroku/ruby",
"addons": [ "addons": [ "heroku-redis", "heroku-postgresql"],
{
"plan": "heroku-redis:mini"
},
{
"plan": "heroku-postgresql:mini"
}
],
"stack": "heroku-20",
"buildpacks": [ "buildpacks": [
{ {
"url": "heroku/ruby" "url": "heroku/ruby"

View file

@ -1,16 +1,7 @@
# retain_original_contact_name: false / true
# In case of setUser we want to update the name of the identified contact,
# which is the default behaviour
#
# But, In case of contact merge during prechat form contact update.
# We don't want to update the name of the identified original contact.
class ContactIdentifyAction class ContactIdentifyAction
pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }] pattr_initialize [:contact!, :params!]
def perform def perform
@attributes_to_update = [:identifier, :name, :email, :phone_number]
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
merge_if_existing_identified_contact merge_if_existing_identified_contact
merge_if_existing_email_contact merge_if_existing_email_contact
@ -27,89 +18,49 @@ class ContactIdentifyAction
end end
def merge_if_existing_identified_contact def merge_if_existing_identified_contact
return unless merge_contacts?(existing_identified_contact, :identifier) @contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
process_contact_merge(existing_identified_contact)
end end
def merge_if_existing_email_contact def merge_if_existing_email_contact
return unless merge_contacts?(existing_email_contact, :email) @contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
process_contact_merge(existing_email_contact)
end end
def merge_if_existing_phone_number_contact def merge_if_existing_phone_number_contact
return unless merge_contacts?(existing_phone_number_contact, :phone_number) @contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact)
return unless mergable_phone_contact?
process_contact_merge(existing_phone_number_contact)
end
def process_contact_merge(mergee_contact)
@contact = merge_contact(mergee_contact, @contact)
@attributes_to_update.delete(:name) if retain_original_contact_name
end end
def existing_identified_contact def existing_identified_contact
return if params[:identifier].blank? return if params[:identifier].blank?
@existing_identified_contact ||= account.contacts.find_by(identifier: params[:identifier]) @existing_identified_contact ||= Contact.where(account_id: account.id).find_by(identifier: params[:identifier])
end end
def existing_email_contact def existing_email_contact
return if params[:email].blank? return if params[:email].blank?
@existing_email_contact ||= account.contacts.find_by(email: params[:email]) @existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
end end
def existing_phone_number_contact def existing_phone_number_contact
return if params[:phone_number].blank? return if params[:phone_number].blank?
@existing_phone_number_contact ||= account.contacts.find_by(phone_number: params[:phone_number]) @existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number])
end end
def merge_contacts?(existing_contact, key) def merge_contacts?(existing_contact, _contact)
return if existing_contact.blank? existing_contact && existing_contact.id != @contact.id
return true if params[:identifier].blank?
# we want to prevent merging contacts with different identifiers
if existing_contact.identifier.present? && existing_contact.identifier != params[:identifier]
# we will remove attribute from update list
@attributes_to_update.delete(key)
return false
end
true
end
# case: contact 1: email: 1@test.com, phone: 123456789
# params: email: 2@test.com, phone: 123456789
# we don't want to overwrite 1@test.com since email parameter takes higer priority
def mergable_phone_contact?
return true if params[:email].blank?
if existing_phone_number_contact.email.present? && existing_phone_number_contact.email != params[:email]
@attributes_to_update.delete(:phone_number)
return false
end
true
end end
def update_contact def update_contact
@contact.attributes = params.slice(*@attributes_to_update).reject do |_k, v|
v.blank?
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes })
# blank identifier or email will throw unique index error # blank identifier or email will throw unique index error
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.discard_invalid_attrs if discard_invalid_attrs @contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
@contact.save! v.blank?
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes }))
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end end
def merge_contact(base_contact, merge_contact) def merge_contact(base_contact, merge_contact)
return base_contact if base_contact.id == merge_contact.id
ContactMergeAction.new( ContactMergeAction.new(
account: account, account: account,
base_contact: base_contact, base_contact: base_contact,
@ -118,14 +69,14 @@ class ContactIdentifyAction
end end
def custom_attributes def custom_attributes
return @contact.custom_attributes if params[:custom_attributes].blank? params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes
(@contact.custom_attributes || {}).deep_merge(params[:custom_attributes].stringify_keys)
end end
def additional_attributes def additional_attributes
return @contact.additional_attributes if params[:additional_attributes].blank? if params[:additional_attributes]
@contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys)
(@contact.additional_attributes || {}).deep_merge(params[:additional_attributes].stringify_keys) else
@contact.additional_attributes
end
end end
end end

View file

@ -2,7 +2,7 @@
class AccountBuilder class AccountBuilder
include CustomExceptions::Account include CustomExceptions::Account
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale] pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin]
def perform def perform
if @user.nil? if @user.nil?

View file

@ -9,15 +9,12 @@ class Campaigns::CampaignConversationBuilder
@contact_inbox.lock! @contact_inbox.lock!
# We won't send campaigns if a conversation is already present # We won't send campaigns if a conversation is already present
raise 'Conversation alread present' if @contact_inbox.reload.conversations.present? return if @contact_inbox.reload.conversations.present?
@conversation = ::Conversation.create!(conversation_params) @conversation = ::Conversation.create!(conversation_params)
Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform
end end
@conversation @conversation
rescue StandardError => e
Rails.logger.info(e.message)
nil
end end
private private

View file

@ -0,0 +1,76 @@
class ContactBuilder
pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified]
def perform
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
return contact_inbox if contact_inbox
build_contact_inbox
end
private
def account
@account ||= inbox.account
end
def create_contact_inbox(contact)
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: source_id
)
end
def update_contact_avatar(contact)
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
end
def create_contact
account.contacts.create!(
name: contact_attributes[:name] || ::Haikunator.haikunate(1000),
phone_number: contact_attributes[:phone_number],
email: contact_attributes[:email],
identifier: contact_attributes[:identifier],
additional_attributes: contact_attributes[:additional_attributes],
custom_attributes: contact_attributes[:custom_attributes]
)
end
def find_contact
contact = find_contact_by_identifier(contact_attributes[:identifier])
contact ||= find_contact_by_email(contact_attributes[:email])
contact ||= find_contact_by_phone_number(contact_attributes[:phone_number])
contact
end
def find_contact_by_identifier(identifier)
return if identifier.blank?
account.contacts.find_by(identifier: identifier)
end
def find_contact_by_email(email)
return if email.blank?
account.contacts.find_by(email: email.downcase)
end
def find_contact_by_phone_number(phone_number)
return if phone_number.blank?
account.contacts.find_by(phone_number: phone_number)
end
def build_contact_inbox
ActiveRecord::Base.transaction do
contact = find_contact || create_contact
contact_inbox = create_contact_inbox(contact)
update_contact_avatar(contact)
contact_inbox
rescue StandardError => e
Rails.logger.error e
raise e
end
end
end

View file

@ -1,12 +1,13 @@
# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned.
# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided.
class ContactInboxBuilder class ContactInboxBuilder
pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }] pattr_initialize [:contact_id!, :inbox_id!, :source_id]
def perform def perform
@source_id ||= generate_source_id @contact = Contact.find(contact_id)
create_contact_inbox if source_id.present? @inbox = @contact.account.inboxes.find(inbox_id)
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
source_id = @source_id || generate_source_id
create_contact_inbox(source_id) if source_id.present?
end end
private private
@ -18,37 +19,23 @@ class ContactInboxBuilder
when 'Channel::Whatsapp' when 'Channel::Whatsapp'
wa_source_id wa_source_id
when 'Channel::Email' when 'Channel::Email'
email_source_id
when 'Channel::Sms'
phone_source_id
when 'Channel::Api', 'Channel::WebWidget'
SecureRandom.uuid
else
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
end
end
def email_source_id
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
@contact.email @contact.email
end when 'Channel::Sms'
def phone_source_id
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
@contact.phone_number @contact.phone_number
when 'Channel::Api'
SecureRandom.uuid
end
end end
def wa_source_id def wa_source_id
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number return unless @contact.phone_number
# whatsapp doesn't want the + in e164 format # whatsapp doesn't want the + in e164 format
@contact.phone_number.delete('+').to_s "#{@contact.phone_number}.delete('+')"
end end
def twilio_source_id def twilio_source_id
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number return unless @contact.phone_number
case @inbox.channel.medium case @inbox.channel.medium
when 'sms' when 'sms'
@ -58,11 +45,11 @@ class ContactInboxBuilder
end end
end end
def create_contact_inbox def create_contact_inbox(source_id)
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!( ::ContactInbox.find_or_create_by!(
contact_id: @contact.id, contact_id: @contact.id,
inbox_id: @inbox.id, inbox_id: @inbox.id,
source_id: @source_id source_id: source_id
) )
end end
end end

View file

@ -1,86 +0,0 @@
# This Builder will create a contact and contact inbox with specified attributes.
# If an existing identified contact exisits, it will be returned.
# for contact inbox logic it uses the contact inbox builder
class ContactInboxWithContactBuilder
pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified]
def perform
find_or_create_contact_and_contact_inbox
# in case of race conditions where contact is created by another thread
# we will try to find the contact and create a contact inbox
rescue ActiveRecord::RecordNotUnique
find_or_create_contact_and_contact_inbox
end
def find_or_create_contact_and_contact_inbox
@contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present?
return @contact_inbox if @contact_inbox
ActiveRecord::Base.transaction(requires_new: true) do
build_contact_with_contact_inbox
update_contact_avatar(@contact) unless @contact.avatar.attached?
@contact_inbox
end
end
private
def build_contact_with_contact_inbox
@contact = find_contact || create_contact
@contact_inbox = create_contact_inbox
end
def account
@account ||= inbox.account
end
def create_contact_inbox
ContactInboxBuilder.new(
contact: @contact,
inbox: @inbox,
source_id: @source_id,
hmac_verified: hmac_verified
).perform
end
def update_contact_avatar(contact)
::Avatar::AvatarFromUrlJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
end
def create_contact
account.contacts.create!(
name: contact_attributes[:name] || ::Haikunator.haikunate(1000),
phone_number: contact_attributes[:phone_number],
email: contact_attributes[:email],
identifier: contact_attributes[:identifier],
additional_attributes: contact_attributes[:additional_attributes],
custom_attributes: contact_attributes[:custom_attributes]
)
end
def find_contact
contact = find_contact_by_identifier(contact_attributes[:identifier])
contact ||= find_contact_by_email(contact_attributes[:email])
contact ||= find_contact_by_phone_number(contact_attributes[:phone_number])
contact
end
def find_contact_by_identifier(identifier)
return if identifier.blank?
account.contacts.find_by(identifier: identifier)
end
def find_contact_by_email(email)
return if email.blank?
account.contacts.find_by(email: email.downcase)
end
def find_contact_by_phone_number(phone_number)
return if phone_number.blank?
account.contacts.find_by(phone_number: phone_number)
end
end

View file

@ -1,40 +0,0 @@
class ConversationBuilder
pattr_initialize [:params!, :contact_inbox!]
def perform
look_up_exising_conversation || create_new_conversation
end
private
def look_up_exising_conversation
return unless @contact_inbox.inbox.lock_to_single_conversation?
@contact_inbox.conversations.last
end
def create_new_conversation
::Conversation.create!(conversation_params)
end
def conversation_params
additional_attributes = params[:additional_attributes]&.permit! || {}
custom_attributes = params[:custom_attributes]&.permit! || {}
status = params[:status].present? ? { status: params[:status] } : {}
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
# status = { status: 'pending' } if status[:status] == 'bot'
{
account_id: @contact_inbox.inbox.account_id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: additional_attributes,
custom_attributes: custom_attributes,
snoozed_until: params[:snoozed_until],
assignee_id: params[:assignee_id],
team_id: params[:team_id]
}.merge(status)
end
end

View file

@ -22,9 +22,10 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
return if @inbox.channel.reauthorization_required? return if @inbox.channel.reauthorization_required?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
build_contact_inbox build_contact
build_message build_message
end end
ensure_contact_avatar
rescue Koala::Facebook::AuthenticationError rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
rescue StandardError => e rescue StandardError => e
@ -34,12 +35,15 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
private private
def build_contact_inbox def contact
@contact_inbox = ::ContactInboxWithContactBuilder.new( @contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
source_id: @sender_id, end
inbox: @inbox,
contact_attributes: contact_params def build_contact
).perform return if contact.present?
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
end end
def build_message def build_message
@ -50,11 +54,19 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end end
end end
def ensure_contact_avatar
return if contact_params[:remote_avatar_url].blank?
return if @contact.avatar.attached?
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url])
end
def conversation def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation @conversation ||= Conversation.find_by(conversation_params) || build_conversation
end end
def build_conversation def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
Conversation.create!(conversation_params.merge( Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id contact_inbox_id: @contact_inbox.id
)) ))
@ -82,7 +94,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
{ {
account_id: @inbox.account_id, account_id: @inbox.account_id,
inbox_id: @inbox.id, inbox_id: @inbox.id,
contact_id: @contact_inbox.contact_id contact_id: contact.id
} }
end end
@ -93,7 +105,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: @message_type, message_type: @message_type,
content: response.content, content: response.content,
source_id: response.identifier, source_id: response.identifier,
sender: @outgoing_echo ? nil : @contact_inbox.contact sender: @outgoing_echo ? nil : contact
} }
end end
@ -101,7 +113,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
{ {
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id, account_id: @inbox.account_id,
avatar_url: result['profile_pic'] remote_avatar_url: result['profile_pic'] || ''
} }
end end

View file

@ -72,7 +72,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
def build_message def build_message
return if @outgoing_echo && already_sent_from_chatwoot? return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files?
@message = conversation.messages.create!(message_params) @message = conversation.messages.create!(message_params)
@ -118,13 +117,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
cw_message.present? cw_message.present?
end end
def all_unsupported_files?
return if attachments.empty?
attachments_type = attachments.pluck(:type).uniq.first
unsupported_file_type?(attachments_type)
end
### Sample response ### Sample response
# { # {
# "object": "instagram", # "object": "instagram",

View file

@ -35,13 +35,7 @@ class Messages::MessageBuilder
file: uploaded_attachment file: uploaded_attachment
) )
attachment.file_type = if uploaded_attachment.is_a?(String) attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
file_type_by_signed_id(
uploaded_attachment
)
else
file_type(uploaded_attachment&.content_type)
end
end end
end end

View file

@ -2,8 +2,7 @@ class Messages::Messenger::MessageBuilder
include ::FileTypeHelper include ::FileTypeHelper
def process_attachment(attachment) def process_attachment(attachment)
# This check handles very rare case if there are multiple files to attach with only one usupported file return if attachment['type'].to_sym == :template
return if unsupported_file_type?(attachment['type'])
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save! attachment_obj.save!
@ -46,7 +45,6 @@ class Messages::Messenger::MessageBuilder
end end
def update_attachment_file_type(attachment) def update_attachment_file_type(attachment)
return if @message.reload.attachments.blank?
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention' return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
attachment.file_type = file_type(attachment.file&.content_type) attachment.file_type = file_type(attachment.file&.content_type)
@ -56,14 +54,10 @@ class Messages::Messenger::MessageBuilder
def fetch_story_link(attachment) def fetch_story_link(attachment)
message = attachment.message message = attachment.message
result = get_story_object_from_source_id(message.source_id) result = get_story_object_from_source_id(message.source_id)
return if result.blank?
story_id = result['story']['mention']['id'] story_id = result['story']['mention']['id']
story_sender = result['from']['username'] story_sender = result['from']['username']
message.content_attributes[:story_sender] = story_sender message.content_attributes[:story_sender] = story_sender
message.content_attributes[:story_id] = story_id message.content_attributes[:story_id] = story_id
message.content_attributes[:image_type] = 'story_mention'
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender) message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
message.save! message.save!
end end
@ -74,20 +68,8 @@ class Messages::Messenger::MessageBuilder
rescue Koala::Facebook::AuthenticationError rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
raise raise
rescue Koala::Facebook::ClientError => e
# The exception occurs when we are trying fetch the deleted story or blocked story.
@message.attachments.destroy_all
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
Rails.logger.error e
{}
rescue StandardError => e rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
{} {}
end end
private
def unsupported_file_type?(attachment_type)
[:template, :unsupported_type].include? attachment_type.to_sym
end
end end

View file

@ -15,9 +15,6 @@ class NotificationBuilder
def user_subscribed_to_notification? def user_subscribed_to_notification?
notification_setting = user.notification_settings.find_by(account_id: account.id) notification_setting = user.notification_settings.find_by(account_id: account.id)
# added for the case where an assignee might be removed from the account but remains in conversation
return if notification_setting.blank?
return true if notification_setting.public_send("email_#{notification_type}?") return true if notification_setting.public_send("email_#{notification_type}?")
return true if notification_setting.public_send("push_#{notification_type}?") return true if notification_setting.public_send("push_#{notification_type}?")

View file

@ -25,7 +25,7 @@ class NotificationSubscriptionBuilder
end end
def build_identifier_subscription def build_identifier_subscription
@identifier_subscription = user.notification_subscriptions.create!(params.merge(identifier: identifier)) @identifier_subscription = user.notification_subscriptions.create(params.merge(identifier: identifier))
end end
def update_identifier_subscription def update_identifier_subscription

View file

@ -30,6 +30,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
end end
def permitted_params def permitted_params
params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content]) params.permit(:name, :description, :outgoing_url)
end end
end end

View file

@ -39,7 +39,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
# TODO: move this to a builder and combine the save account user method into a builder # TODO: move this to a builder and combine the save account user method into a builder
# ensure the account user association is also created in a single transaction # ensure the account user association is also created in a single transaction
def create_user def create_user
return @user.send_confirmation_instructions if @user return if @user
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation)) @user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
end end
@ -72,6 +72,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
end end
def validate_limit def validate_limit
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents] render_payment_required('Account limit exceeded. Upgrade to a higher plan') if agents.count >= Current.account.usage_limits[:agents]
end end
end end

View file

@ -1,21 +1,14 @@
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :portal before_action :portal
before_action :check_authorization
before_action :fetch_article, except: [:index, :create] before_action :fetch_article, except: [:index, :create]
before_action :set_current_page, only: [:index]
def index def index
@portal_articles = @portal.articles @articles = @portal.articles
@all_articles = @portal_articles.search(list_params) @articles.search(list_params) if params[:payload].present?
@articles_count = @all_articles.count
@articles = @all_articles.page(@current_page)
end end
def create def create
@article = @portal.articles.create!(article_params) @article = @portal.articles.create!(article_params)
@article.associate_root_article(article_params[:associated_article_id])
@article.draft!
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
end end
def edit; end def edit; end
@ -38,21 +31,18 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
end end
def portal def portal
@portal ||= Current.account.portals.find_by!(slug: params[:portal_id]) @portal ||= Current.account.portals.find_by(slug: params[:portal_id])
end end
def article_params def article_params
params.require(:article).permit( params.require(:article).permit(
:title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description, :title, :content, :description, :position, :category_id, :author_id
{ tags: [] }]
) )
end end
def list_params def list_params
params.permit(:locale, :query, :page, :category_slug, :status, :author_id) params.require(:payload).permit(
end :category_slug, :locale, :query
)
def set_current_page
@current_page = params[:page] || 1
end end
end end

View file

@ -49,7 +49,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
def clone def clone
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id]) automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
new_rule = automation_rule.dup new_rule = automation_rule.dup
new_rule.save! new_rule.save
@automation_rule = new_rule @automation_rule = new_rule
end end

View file

@ -90,7 +90,9 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
end end
def set_avatar(facebook_inbox, page_id) def set_avatar(facebook_inbox, page_id)
avatar_url = "https://graph.facebook.com/#{page_id}/picture?type=large" avatar_file = Down.download(
Avatar::AvatarFromUrlJob.perform_later(facebook_inbox, avatar_url) "http://graph.facebook.com/#{page_id}/picture?type=large"
)
facebook_inbox.avatar.attach(io: avatar_file, filename: avatar_file.original_filename, content_type: avatar_file.content_type)
end end
end end

View file

@ -1,30 +1,17 @@
class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController
before_action :portal before_action :portal
before_action :check_authorization
before_action :fetch_category, except: [:index, :create] before_action :fetch_category, except: [:index, :create]
before_action :set_current_page, only: [:index]
def index def index
@current_locale = params[:locale]
@categories = @portal.categories.search(params) @categories = @portal.categories.search(params)
end end
def create def create
@category = @portal.categories.create!(category_params) @category = @portal.categories.create!(category_params)
@category.related_categories << related_categories_records
render json: { error: @category.errors.messages }, status: :unprocessable_entity and return unless @category.valid?
@category.save!
end end
def show; end
def update def update
@category.update!(category_params) @category.update!(category_params)
@category.related_categories = related_categories_records if related_categories_records.any?
render json: { error: @category.errors.messages }, status: :unprocessable_entity and return unless @category.valid?
@category.save!
end end
def destroy def destroy
@ -42,17 +29,9 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
@portal ||= Current.account.portals.find_by(slug: params[:portal_id]) @portal ||= Current.account.portals.find_by(slug: params[:portal_id])
end end
def related_categories_records
@portal.categories.where(id: params[:category][:related_category_ids])
end
def category_params def category_params
params.require(:category).permit( params.require(:category).permit(
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id :name, :description, :position, :slug, :locale
) )
end end
def set_current_page
@current_page = params[:page] || 1
end
end end

View file

@ -27,8 +27,6 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
end end
def phone_number def phone_number
return if permitted_params[:phone_number].blank?
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}" medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
end end
@ -40,11 +38,10 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
@twilio_channel = Current.account.twilio_sms.create!( @twilio_channel = Current.account.twilio_sms.create!(
account_sid: permitted_params[:account_sid], account_sid: permitted_params[:account_sid],
auth_token: permitted_params[:auth_token], auth_token: permitted_params[:auth_token],
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
phone_number: phone_number, phone_number: phone_number,
medium: medium medium: medium
) )
@inbox = Current.account.inboxes.create!( @inbox = Current.account.inboxes.create(
name: permitted_params[:name], name: permitted_params[:name],
channel: @twilio_channel channel: @twilio_channel
) )
@ -52,7 +49,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
def permitted_params def permitted_params
params.require(:twilio_channel).permit( params.require(:twilio_channel).permit(
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium :account_id, :phone_number, :account_sid, :auth_token, :name, :medium
) )
end end
end end

View file

@ -2,11 +2,8 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
before_action :ensure_inbox, only: [:create] before_action :ensure_inbox, only: [:create]
def create def create
@contact_inbox = ContactInboxBuilder.new( source_id = params[:source_id] || SecureRandom.uuid
contact: @contact, @contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id)
inbox: @inbox,
source_id: params[:source_id]
).perform
end end
private private

View file

@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
def index def index
@conversations = Current.account.conversations.includes( @conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox, :taggings :assignee, :contact, :inbox, :taggings
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20) ).where(inbox_id: inbox_ids, contact_id: @contact.id)
end end
private private

View file

@ -12,7 +12,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :check_authorization before_action :check_authorization
before_action :set_current_page, only: [:index, :active, :search, :filter] before_action :set_current_page, only: [:index, :active, :search, :filter]
before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes] before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes, :destroy_custom_attributes]
before_action :set_include_contact_inboxes, only: [:index, :search, :filter] before_action :set_include_contact_inboxes, only: [:index, :search, :filter]
def index def index
@ -25,7 +25,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
contacts = resolved_contacts.where( contacts = resolved_contacts.where(
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search', 'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
search: "%#{params[:q].strip}%" search: "%#{params[:q]}%"
) )
@contacts_count = contacts.count @contacts_count = contacts.count
@contacts = fetch_contacts_with_conversation_count(contacts) @contacts = fetch_contacts_with_conversation_count(contacts)
@ -72,17 +72,15 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url)) @contact = Current.account.contacts.new(contact_params)
@contact.save! @contact.save!
@contact_inbox = build_contact_inbox @contact_inbox = build_contact_inbox
process_avatar
end end
end end
def update def update
@contact.assign_attributes(contact_update_params) @contact.assign_attributes(contact_update_params)
@contact.save! @contact.save!
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
end end
def destroy def destroy
@ -97,11 +95,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def avatar
@contact.avatar.purge if @contact.avatar.attached?
@contact
end
private private
# TODO: Move this to a finder class # TODO: Move this to a finder class
@ -134,26 +127,23 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
return if params[:inbox_id].blank? return if params[:inbox_id].blank?
inbox = Current.account.inboxes.find(params[:inbox_id]) inbox = Current.account.inboxes.find(params[:inbox_id])
ContactInboxBuilder.new( source_id = params[:source_id] || SecureRandom.uuid
contact: @contact, ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id)
inbox: inbox,
source_id: params[:source_id]
).perform
end end
def permitted_params def contact_params
params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {}) params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
end end
def contact_custom_attributes def contact_custom_attributes
return @contact.custom_attributes.merge(permitted_params[:custom_attributes]) if permitted_params[:custom_attributes] return @contact.custom_attributes.merge(contact_params[:custom_attributes]) if contact_params[:custom_attributes]
@contact.custom_attributes @contact.custom_attributes
end end
def contact_update_params def contact_update_params
# we want the merged custom attributes not the original one # we want the merged custom attributes not the original one
permitted_params.except(:custom_attributes, :avatar_url).merge({ custom_attributes: contact_custom_attributes }) contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes })
end end
def set_include_contact_inboxes def set_include_contact_inboxes
@ -168,10 +158,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id]) @contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
end end
def process_avatar
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end
def render_error(error, error_status) def render_error(error, error_status)
render json: error, status: error_status render json: error, status: error_status
end end

View file

@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
include DateRangeHelper include DateRangeHelper
before_action :conversation, except: [:index, :meta, :search, :create, :filter] before_action :conversation, except: [:index, :meta, :search, :create, :filter]
before_action :inbox, :contact, :contact_inbox, only: [:create] before_action :contact_inbox, only: [:create]
def index def index
result = conversation_finder.perform result = conversation_finder.perform
@ -24,7 +24,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform @conversation = ::Conversation.create!(conversation_params)
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present? Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
end end
end end
@ -75,13 +75,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
def update_last_seen def update_last_seen
update_last_seen_on_conversation(DateTime.now.utc, assignee?) # rubocop:disable Rails/SkipsModelValidations
end @conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
@conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee?
def unread # rubocop:enable Rails/SkipsModelValidations
last_incoming_message = @conversation.messages.incoming.last
last_seen_at = last_incoming_message.created_at - 1.second if last_incoming_message.present?
update_last_seen_on_conversation(last_seen_at, true)
end end
def custom_attributes def custom_attributes
@ -91,18 +88,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
private private
def update_last_seen_on_conversation(last_seen_at, update_assignee)
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, last_seen_at)
@conversation.update_column(:assignee_last_seen_at, last_seen_at) if update_assignee.present?
# rubocop:enable Rails/SkipsModelValidations
end
def set_conversation_status def set_conversation_status
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases status = params[:status] == 'bot' ? 'pending' : params[:status]
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases @conversation.status = status
# status = params[:status] == 'bot' ? 'pending' : params[:status]
@conversation.status = params[:status]
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until] @conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
end end
@ -121,44 +109,51 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
authorize @conversation.inbox, :show? authorize @conversation.inbox, :show?
end end
def inbox
return if params[:inbox_id].blank?
@inbox = Current.account.inboxes.find(params[:inbox_id])
authorize @inbox, :show?
end
def contact
return if params[:contact_id].blank?
@contact = Current.account.contacts.find(params[:contact_id])
end
def contact_inbox def contact_inbox
@contact_inbox = build_contact_inbox @contact_inbox = build_contact_inbox
# fallback for the old case where we do look up only using source id
# In future we need to change this and make sure we do look up on combination of inbox_id and source_id
# and deprecate the support of passing only source_id as the param
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
authorize @contact_inbox.inbox, :show? authorize @contact_inbox.inbox, :show?
end end
def build_contact_inbox def build_contact_inbox
return if @inbox.blank? || @contact.blank? return if params[:contact_id].blank? || params[:inbox_id].blank?
inbox = Current.account.inboxes.find(params[:inbox_id])
authorize inbox, :show?
ContactInboxBuilder.new( ContactInboxBuilder.new(
contact: @contact, contact_id: params[:contact_id],
inbox: @inbox, inbox_id: inbox.id,
source_id: params[:source_id] source_id: params[:source_id]
).perform ).perform
end end
def conversation_params
additional_attributes = params[:additional_attributes]&.permit! || {}
custom_attributes = params[:custom_attributes]&.permit! || {}
status = params[:status].present? ? { status: params[:status] } : {}
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
status = { status: 'pending' } if status[:status] == 'bot'
{
account_id: Current.account.id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: additional_attributes,
custom_attributes: custom_attributes,
snoozed_until: params[:snoozed_until],
assignee_id: params[:assignee_id],
team_id: params[:team_id]
}.merge(status)
end
def conversation_finder def conversation_finder
@conversation_finder ||= ConversationFinder.new(Current.user, params) @conversation_finder ||= ConversationFinder.new(current_user, params)
end end
def assignee? def assignee?
@conversation.assignee_id? && Current.user == @conversation.assignee @conversation.assignee_id? && current_user == @conversation.assignee
end end
end end

View file

@ -22,7 +22,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
def download def download
response.headers['Content-Type'] = 'text/csv' response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv' response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv'
render layout: false, template: 'api/v1/accounts/csat_survey_responses/download', formats: [:csv] render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv'
end end
private private

View file

@ -113,8 +113,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def inbox_attributes def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
:lock_to_single_conversation]
end end
def permitted_params(channel_attributes = []) def permitted_params(channel_attributes = [])

View file

@ -1,89 +0,0 @@
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
before_action :check_authorization, only: [:show, :update, :destroy, :execute]
def index
@macros = Macro.with_visibility(current_user, params)
end
def create
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
@macro.set_visibility(current_user, permitted_params)
@macro.actions = params[:actions]
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
@macro.save!
process_attachments
@macro
end
def show
head :not_found if @macro.nil?
end
def destroy
@macro.destroy!
head :ok
end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
def update
ActiveRecord::Base.transaction do
@macro.update!(macros_with_user)
@macro.set_visibility(current_user, permitted_params)
process_attachments
@macro.save!
rescue StandardError => e
Rails.logger.error e
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
end
end
def execute
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
head :ok
end
private
def process_attachments
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
return if actions.blank?
actions.each do |action|
blob_id = action['action_params']
blob = ActiveStorage::Blob.find_by(id: blob_id)
@macro.files.attach(blob)
end
end
def permitted_params
params.permit(
:name, :account_id, :visibility,
actions: [:action_name, { action_params: [] }]
)
end
def macros_with_user
permitted_params.merge(updated_by_id: current_user.id)
end
def fetch_macro
@macro = Current.account.macros.find_by(id: params[:id])
end
def check_authorization
authorize(@macro) if @macro.present?
end
end

View file

@ -1,40 +1,18 @@
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController 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]
before_action :check_authorization
before_action :set_current_page, only: [:index]
def index def index
@portals = Current.account.portals @portals = Current.account.portals
end end
def add_members def show; end
agents = Current.account.agents.where(id: portal_member_params[:member_ids])
@portal.members << agents
end
def show
@all_articles = @portal.articles
@articles = @all_articles.search(locale: params[:locale])
end
def create def create
@portal = Current.account.portals.build(portal_params) @portal = Current.account.portals.create!(portal_params)
@portal.custom_domain = parsed_custom_domain
@portal.save!
process_attached_logo
end end
def update def update
ActiveRecord::Base.transaction do @portal.update!(portal_params)
@portal.update!(portal_params) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain
process_attached_logo
rescue StandardError => e
Rails.logger.error e
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
end
end end
def destroy def destroy
@ -42,15 +20,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def archive
@portal.update(archive: true)
head :ok
end
def process_attached_logo
@portal.logo.attach(params[:logo])
end
private private
def fetch_portal def fetch_portal
@ -63,21 +32,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params def portal_params
params.require(:portal).permit( params.require(:portal).permit(
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived, { config: [:default_locale, :account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived
{ allowed_locales: [] }] }
) )
end end
def portal_member_params
params.require(:portal).permit(:account_id, member_ids: [])
end
def set_current_page
@current_page = params[:page] || 1
end
def parsed_custom_domain
domain = URI.parse(@portal.custom_domain)
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
end
end end

View file

@ -1,7 +1,6 @@
class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseController class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseController
before_action :fetch_team before_action :fetch_team
before_action :check_authorization before_action :check_authorization
before_action :validate_member_id_params, only: [:create, :update, :destroy]
def index def index
@team_members = @team.team_members.map(&:user) @team_members = @team.team_members.map(&:user)
@ -46,10 +45,4 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
def fetch_team def fetch_team
@team = Current.account.teams.find(params[:team_id]) @team = Current.account.teams.find(params[:team_id])
end end
def validate_member_id_params
invalid_ids = params[:user_ids].map(&:to_i) - @team.account.user_ids
render json: { error: 'Invalid User IDs' }, status: :unauthorized and return if invalid_ids.present?
end
end end

View file

@ -19,12 +19,11 @@ class Api::V1::AccountsController < Api::BaseController
user_full_name: account_params[:user_full_name], user_full_name: account_params[:user_full_name],
email: account_params[:email], email: account_params[:email],
user_password: account_params[:password], user_password: account_params[:password],
locale: account_params[:locale],
user: current_user user: current_user
).perform ).perform
if @user if @user
send_auth_headers(@user) send_auth_headers(@user)
render 'api/v1/accounts/create', format: :json, locals: { resource: @user } render 'api/v1/accounts/create.json', locals: { resource: @user }
else else
render_error_response(CustomExceptions::Account::SignupFailed.new({})) render_error_response(CustomExceptions::Account::SignupFailed.new({}))
end end
@ -32,7 +31,7 @@ class Api::V1::AccountsController < Api::BaseController
def show def show
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION) @latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
render 'api/v1/accounts/show', format: :json render 'api/v1/accounts/show.json'
end end
def update def update

View file

@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
def destroy def destroy
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription.destroy! if notification_subscription.present? notification_subscription.destroy!
head :ok head :ok
end end

View file

@ -18,19 +18,10 @@ class Api::V1::ProfilesController < Api::BaseController
head :ok head :ok
end end
def auto_offline
@user.account_users.find_by!(account_id: auto_offline_params[:account_id]).update!(auto_offline: auto_offline_params[:auto_offline] || false)
end
def availability def availability
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability]) @user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
end end
def set_active_account
@user.account_users.find_by(account_id: profile_params[:account_id]).update(active_at: Time.now.utc)
head :ok
end
private private
def set_user def set_user
@ -41,10 +32,6 @@ class Api::V1::ProfilesController < Api::BaseController
params.require(:profile).permit(:account_id, :availability) params.require(:profile).permit(:account_id, :availability)
end end
def auto_offline_params
params.require(:profile).permit(:account_id, :auto_offline)
end
def profile_params def profile_params
params.require(:profile).permit( params.require(:profile).permit(
:email, :email,
@ -52,7 +39,6 @@ class Api::V1::ProfilesController < Api::BaseController
:display_name, :display_name,
:avatar, :avatar,
:message_signature, :message_signature,
:account_id,
ui_settings: {} ui_settings: {}
) )
end end

View file

@ -36,23 +36,52 @@ class Api::V1::Widget::BaseController < ApplicationController
contact_id: @contact.id, contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id, contact_inbox_id: @contact_inbox.id,
additional_attributes: { additional_attributes: {
browser_language: browser.accept_language&.first&.code,
browser: browser_params, browser: browser_params,
initiated_at: timestamp_params, referer: permitted_params[:message][:referer_url],
referer: permitted_params[:message][:referer_url] initiated_at: timestamp_params
}, },
custom_attributes: permitted_params[:custom_attributes].presence || {} custom_attributes: permitted_params[:custom_attributes].presence || {}
} }
end end
def update_contact(email)
contact_with_email = @current_account.contacts.find_by(email: email)
if contact_with_email
@contact = ::ContactMergeAction.new(
account: @current_account,
base_contact: contact_with_email,
mergee_contact: @contact
).perform
else
@contact.update!(email: email)
update_contact_name
end
end
def update_contact_phone_number(phone_number)
contact_with_phone_number = @current_account.contacts.find_by(phone_number: phone_number)
if contact_with_phone_number
@contact = ::ContactMergeAction.new(
account: @current_account,
base_contact: contact_with_phone_number,
mergee_contact: @contact
).perform
else
@contact.update!(phone_number: phone_number)
update_contact_name
end
end
def update_contact_name
@contact.update!(name: contact_name) if contact_name.present?
end
def contact_email def contact_email
permitted_params.dig(:contact, :email)&.downcase permitted_params.dig(:contact, :email)&.downcase
end end
def contact_name def contact_name
return if @contact.email.present? || @contact.phone_number.present? || @contact.identifier.present? params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
permitted_params.dig(:contact, :name) || (contact_email.split('@')[0] if contact_email.present?)
end end
def contact_phone_number def contact_phone_number

View file

@ -1,27 +1,14 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
include WidgetHelper before_action :process_hmac, only: [:update]
before_action :validate_hmac, only: [:set_user]
def show; end def show; end
def update def update
identify_contact(@contact) contact_identify_action = ContactIdentifyAction.new(
end contact: @contact,
params: permitted_params.to_h.deep_symbolize_keys
def set_user )
contact = nil @contact = contact_identify_action.perform
if a_different_contact?
@contact_inbox, @widget_auth_token = build_contact_inbox_with_token(@web_widget)
contact = @contact_inbox.contact
else
contact = @contact
end
@contact_inbox.update(hmac_verified: true) if should_verify_hmac? && valid_hmac?
identify_contact(contact)
end end
# TODO : clean up this with proper routes delete contacts/custom_attributes # TODO : clean up this with proper routes delete contacts/custom_attributes
@ -33,23 +20,12 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
private private
def identify_contact(contact) def process_hmac
contact_identify_action = ContactIdentifyAction.new(
contact: contact,
params: permitted_params.to_h.deep_symbolize_keys,
discard_invalid_attrs: true
)
@contact = contact_identify_action.perform
end
def a_different_contact?
@contact.identifier.present? && @contact.identifier != permitted_params[:identifier]
end
def validate_hmac
return unless should_verify_hmac? return unless should_verify_hmac?
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac? render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
@contact_inbox.update(hmac_verified: true)
end end
def should_verify_hmac? def should_verify_hmac?

View file

@ -9,17 +9,13 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
process_update_contact process_update_contact
@conversation = create_conversation @conversation = create_conversation
conversation.messages.create!(message_params) conversation.messages.create(message_params)
end end
end end
def process_update_contact def process_update_contact
@contact = ContactIdentifyAction.new( update_contact(contact_email) if @contact.email.blank? && contact_email.present?
contact: @contact, update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present?
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
retain_original_contact_name: true,
discard_invalid_attrs: true
).perform
end end
def update_last_seen def update_last_seen
@ -60,7 +56,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
unless conversation.resolved? unless conversation.resolved?
conversation.status = :resolved conversation.status = :resolved
conversation.save! conversation.save
end end
head :ok head :ok
end end

View file

@ -15,10 +15,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def update def update
if @message.content_type == 'input_email' if @message.content_type == 'input_email'
@message.update!(submitted_email: contact_email) @message.update!(submitted_email: contact_email)
ContactIdentifyAction.new( update_contact(contact_email)
contact: @contact,
params: { email: contact_email }
).perform
else else
@message.update!(message_update_params[:message]) @message.update!(message_update_params[:message])
end end

View file

@ -14,22 +14,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def agents def agents
@report_data = generate_agents_report @report_data = generate_agents_report
generate_csv('agents_report', 'api/v2/accounts/reports/agents') generate_csv('agents_report', 'api/v2/accounts/reports/agents.csv.erb')
end end
def inboxes def inboxes
@report_data = generate_inboxes_report @report_data = generate_inboxes_report
generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes') generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes.csv.erb')
end end
def labels def labels
@report_data = generate_labels_report @report_data = generate_labels_report
generate_csv('labels_report', 'api/v2/accounts/reports/labels') generate_csv('labels_report', 'api/v2/accounts/reports/labels.csv.erb')
end end
def teams def teams
@report_data = generate_teams_report @report_data = generate_teams_report
generate_csv('teams_report', 'api/v2/accounts/reports/teams') generate_csv('teams_report', 'api/v2/accounts/reports/teams.csv.erb')
end end
def conversations def conversations
@ -43,7 +43,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def generate_csv(filename, template) def generate_csv(filename, template)
response.headers['Content-Type'] = 'text/csv' response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv" response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv"
render layout: false, template: template, formats: [:csv] render layout: false, template: template, format: 'csv'
end end
def check_authorization def check_authorization

View file

@ -17,6 +17,10 @@ class ApplicationController < ActionController::Base
Current.user = @user Current.user = @user
end end
def current_subscription
@subscription ||= Current.account.subscription
end
def pundit_user def pundit_user
{ {
user: Current.user, user: Current.user,

View file

@ -8,8 +8,6 @@ module EnsureCurrentAccountHelper
def ensure_current_account def ensure_current_account
account = Account.find(params[:account_id]) account = Account.find(params[:account_id])
ensure_account_is_active?(account)
if current_user if current_user
account_accessible_for_user?(account) account_accessible_for_user?(account)
elsif @resource.is_a?(AgentBot) elsif @resource.is_a?(AgentBot)
@ -27,8 +25,4 @@ module EnsureCurrentAccountHelper
def account_accessible_for_bot?(account) def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id) render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end end
def ensure_account_is_active?(account)
render_unauthorized('Account is suspended') unless account.active?
end
end end

View file

@ -1,20 +0,0 @@
# services from Meta (Prev: Facebook) needs a token verification step for webhook subscriptions,
# This concern handles the token verification step.
module MetaTokenVerifyConcern
def verify
service = is_a?(Webhooks::WhatsappController) ? 'whatsapp' : 'instagram'
if valid_token?(params['hub.verify_token'])
Rails.logger.info("#{service.capitalize} webhook verified")
render json: params['hub.challenge']
else
render status: :unauthorized, json: { error: 'Error; wrong verify token' }
end
end
private
def valid_token?(_token)
raise 'Overwrite this method your controller'
end
end

View file

@ -13,8 +13,6 @@ module RequestExceptionHandler
render_not_found_error('Resource could not be found') render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError rescue Pundit::NotAuthorizedError
render_unauthorized('You are not authorized to do this action') render_unauthorized('You are not authorized to do this action')
rescue ActionController::ParameterMissing => e
render_could_not_create_error(e.message)
ensure ensure
# to address the thread variable leak issues in Puma/Thin webserver # to address the thread variable leak issues in Puma/Thin webserver
Current.reset Current.reset

View file

@ -5,9 +5,7 @@ module WebsiteTokenHelper
def set_web_widget def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token]) @web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
@current_account = @web_widget.inbox.account @current_account = @web_widget.account
render json: { error: 'Account is suspended' }, status: :unauthorized unless @current_account.active?
end end
def set_contact def set_contact

View file

@ -4,7 +4,6 @@ class DashboardController < ActionController::Base
before_action :set_global_config before_action :set_global_config
around_action :switch_locale around_action :switch_locale
before_action :ensure_installation_onboarding, only: [:index] before_action :ensure_installation_onboarding, only: [:index]
before_action :render_hc_if_custom_domain, only: [:index]
layout 'vueapp' layout 'vueapp'
@ -14,9 +13,11 @@ class DashboardController < ActionController::Base
def set_global_config def set_global_config
@global_config = GlobalConfig.get( @global_config = GlobalConfig.get(
'LOGO', 'LOGO_THUMBNAIL', 'LOGO',
'LOGO_THUMBNAIL',
'INSTALLATION_NAME', 'INSTALLATION_NAME',
'WIDGET_BRAND_URL', 'TERMS_URL', 'WIDGET_BRAND_URL',
'TERMS_URL',
'PRIVACY_URL', 'PRIVACY_URL',
'DISPLAY_MANIFEST', 'DISPLAY_MANIFEST',
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD', 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
@ -24,12 +25,11 @@ class DashboardController < ActionController::Base
'API_CHANNEL_NAME', 'API_CHANNEL_NAME',
'API_CHANNEL_THUMBNAIL', 'API_CHANNEL_THUMBNAIL',
'ANALYTICS_TOKEN', 'ANALYTICS_TOKEN',
'ANALYTICS_HOST',
'DIRECT_UPLOADS_ENABLED', 'DIRECT_UPLOADS_ENABLED',
'HCAPTCHA_SITE_KEY', 'HCAPTCHA_SITE_KEY',
'LOGOUT_REDIRECT_LINK', 'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE', 'DISABLE_USER_PROFILE_UPDATE'
'DEPLOYMENT_ENV',
'CSML_EDITOR_HOST'
).merge(app_config) ).merge(app_config)
end end
@ -37,17 +37,6 @@ class DashboardController < ActionController::Base
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING) redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
end end
def render_hc_if_custom_domain
domain = request.host
return if domain == URI.parse(ENV.fetch('FRONTEND_URL', '')).host
@portal = Portal.find_by(custom_domain: domain)
return unless @portal
@locale = @portal.default_locale
render 'public/api/v1/portals/show', layout: 'portal', portal: @portal and return
end
def app_config def app_config
{ {
APP_VERSION: Chatwoot.config[:version], APP_VERSION: Chatwoot.config[:version],

View file

@ -14,7 +14,7 @@ class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
def render_confirmation_success def render_confirmation_success
send_auth_headers(@confirmable) send_auth_headers(@confirmable)
render partial: 'devise/auth', formats: [:json], locals: { resource: @confirmable } render partial: 'devise/auth.json', locals: { resource: @confirmable }
end end
def render_confirmation_error def render_confirmation_error

View file

@ -11,7 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
@recoverable = User.find_by(reset_password_token: reset_password_token) @recoverable = User.find_by(reset_password_token: reset_password_token)
if @recoverable && reset_password_and_confirmation(@recoverable) if @recoverable && reset_password_and_confirmation(@recoverable)
send_auth_headers(@recoverable) send_auth_headers(@recoverable)
render partial: 'devise/auth', formats: [:json], locals: { resource: @recoverable } render partial: 'devise/auth.json', locals: { resource: @recoverable }
else else
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
end end

View file

@ -16,14 +16,14 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
end end
def render_create_success def render_create_success
render partial: 'devise/auth', formats: [:json], locals: { resource: @resource } render partial: 'devise/auth.json', locals: { resource: @resource }
end end
private private
def authenticate_resource_with_sso_token def authenticate_resource_with_sso_token
@token = @resource.create_token @token = @resource.create_token
@resource.save! @resource.save
sign_in(:user, @resource, store: false, bypass: false) sign_in(:user, @resource, store: false, bypass: false)
# invalidate the token after the user is signed in # invalidate the token after the user is signed in

View file

@ -2,7 +2,7 @@ class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenVali
def validate_token def validate_token
# @resource will have been set by set_user_by_token concern # @resource will have been set by set_user_by_token concern
if @resource if @resource
render 'devise/token', formats: [:json] render 'devise/token.json'
else else
render_validate_token_error render_validate_token_error
end end

View file

@ -1,16 +1,18 @@
class Platform::Api::V1::AccountsController < PlatformController class Platform::Api::V1::AccountsController < PlatformController
def create def create
@resource = Account.create!(account_params) @resource = Account.new(account_params)
update_resource_features @resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
render json: @resource
end end
def show; end def show
render json: @resource
end
def update def update
@resource.assign_attributes(account_params) @resource.update!(account_params)
update_resource_features render json: @resource
@resource.save!
end end
def destroy def destroy
@ -25,18 +27,6 @@ class Platform::Api::V1::AccountsController < PlatformController
end end
def account_params def account_params
permitted_params.except(:features) params.permit(:name)
end
def update_resource_features
return if permitted_params[:features].blank?
permitted_params[:features].each do |key, value|
value.present? ? @resource.enable_features(key) : @resource.disable_features(key)
end
end
def permitted_params
params.permit(:name, :locale, :domain, :support_email, :status, features: {}, limits: {}, custom_attributes: {})
end end
end end

View file

@ -14,7 +14,7 @@ class Platform::Api::V1::UsersController < PlatformController
def login def login
encoded_email = ERB::Util.url_encode(@resource.email) 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: "#{ENV['FRONTEND_URL']}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
end end
def show; end def show; end
@ -51,6 +51,6 @@ class Platform::Api::V1::UsersController < PlatformController
end end
def user_params def user_params
params.permit(:name, :display_name, :email, :password, custom_attributes: {}) params.permit(:name, :email, :password, custom_attributes: {})
end end
end end

View file

@ -4,10 +4,10 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
def create def create
source_id = params[:source_id] || SecureRandom.uuid source_id = params[:source_id] || SecureRandom.uuid
@contact_inbox = ::ContactInboxWithContactBuilder.new( @contact_inbox = ::ContactBuilder.new(
source_id: source_id, source_id: source_id,
inbox: @inbox_channel.inbox, inbox: @inbox_channel.inbox,
contact_attributes: permitted_params.except(:identifier_hash) contact_attributes: permitted_params.except(:identifier, :identifier_hash)
).perform ).perform
end end

View file

@ -3,15 +3,9 @@ class Public::Api::V1::InboxesController < PublicController
before_action :set_contact_inbox before_action :set_contact_inbox
before_action :set_conversation before_action :set_conversation
def show
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:id])
end
private private
def set_inbox_channel def set_inbox_channel
return if params[:inbox_id].blank?
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:inbox_id]) @inbox_channel = ::Channel::Api.find_by!(identifier: params[:inbox_id])
end end

View file

@ -1,39 +0,0 @@
class Public::Api::V1::Portals::ArticlesController < PublicController
before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :portal
before_action :set_category, except: [:index]
before_action :set_article, only: [:show]
layout 'portal'
def index
@articles = @portal.articles
@articles = @articles.search(list_params) if list_params.present?
end
def show; end
private
def set_article
@article = @category.articles.find(params[:id])
@parsed_content = render_article_content(@article.content)
end
def set_category
@category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present?
end
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
end
def list_params
params.permit(:query)
end
def render_article_content(content)
# rubocop:disable Rails/OutputSafety
CommonMarker.render_html(content).html_safe
# rubocop:enable Rails/OutputSafety
end
end

View file

@ -1,22 +0,0 @@
class Public::Api::V1::Portals::CategoriesController < PublicController
before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :portal
before_action :set_category, only: [:show]
layout 'portal'
def index
@categories = @portal.categories
end
def show; end
private
def set_category
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
end
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
end
end

View file

@ -1,21 +0,0 @@
class Public::Api::V1::PortalsController < PublicController
before_action :ensure_custom_domain_request, only: [:show]
before_action :portal
before_action :redirect_to_portal_with_locale, only: [:show]
layout 'portal'
def show; end
private
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
@locale = params[:locale] || @portal.default_locale
end
def redirect_to_portal_with_locale
return if params[:locale].present?
redirect_to "/hc/#{@portal.slug}/#{@portal.default_locale}"
end
end

View file

@ -3,20 +3,4 @@
class PublicController < ActionController::Base class PublicController < ActionController::Base
include RequestExceptionHandler include RequestExceptionHandler
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
private
def ensure_custom_domain_request
domain = request.host
return if [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain)
@portal = ::Portal.find_by(custom_domain: domain)
return if @portal.present?
render json: {
error: "Domain: #{domain} is not registered with us. \
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
}, status: :unauthorized and return
end
end end

View file

@ -36,15 +36,9 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
def resource_params def resource_params
permitted_params = super permitted_params = super
permitted_params[:limits] = permitted_params[:limits].to_h.compact permitted_params[:limits] = permitted_params[:limits].to_h.compact
permitted_params[:selected_feature_flags] = params[:enabled_features].keys.map(&:to_sym) if params[:enabled_features].present?
permitted_params permitted_params
end end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions # See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information # for more information
def seed
Internal::SeedAccountJob.perform_later(requested_resource)
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
end
end end

View file

@ -7,7 +7,7 @@ class Twilio::CallbackController < ApplicationController
private private
def permitted_params # rubocop:disable Metrics/MethodLength def permitted_params
params.permit( params.permit(
:ApiVersion, :ApiVersion,
:SmsSid, :SmsSid,
@ -25,8 +25,7 @@ class Twilio::CallbackController < ApplicationController
:ToCountry, :ToCountry,
:FromState, :FromState,
:MediaUrl0, :MediaUrl0,
:MediaContentType0, :MediaContentType0
:MessagingServiceSid
) )
end end
end end

View file

@ -44,12 +44,12 @@ class Twitter::CallbacksController < Twitter::BaseController
end end
def create_inbox def create_inbox
twitter_profile = account.twitter_profiles.create!( twitter_profile = account.twitter_profiles.create(
twitter_access_token: parsed_body['oauth_token'], twitter_access_token: parsed_body['oauth_token'],
twitter_access_token_secret: parsed_body['oauth_token_secret'], twitter_access_token_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id'] profile_id: parsed_body['user_id']
) )
account.inboxes.create!( account.inboxes.create(
name: parsed_body['screen_name'], name: parsed_body['screen_name'],
channel: twitter_profile channel: twitter_profile
) )

View file

@ -1,5 +1,15 @@
class Webhooks::InstagramController < ActionController::API class Webhooks::InstagramController < ApplicationController
include MetaTokenVerifyConcern skip_before_action :authenticate_user!, raise: false
skip_before_action :set_current_user
def verify
if valid_instagram_token?(params['hub.verify_token'])
Rails.logger.info('Instagram webhook verified')
render json: params['hub.challenge']
else
render json: { error: 'Error; wrong verify token', status: 403 }
end
end
def events def events
Rails.logger.info('Instagram webhook received events') Rails.logger.info('Instagram webhook received events')
@ -14,7 +24,7 @@ class Webhooks::InstagramController < ActionController::API
private private
def valid_token?(token) def valid_instagram_token?(token)
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '') token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
end end
end end

View file

@ -1,16 +1,6 @@
class Webhooks::WhatsappController < ActionController::API class Webhooks::WhatsappController < ActionController::API
include MetaTokenVerifyConcern
def process_payload def process_payload
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash) Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
head :ok head :ok
end end
private
def valid_token?(token)
channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number])
whatsapp_webhook_verify_token = channel.provider_config['webhook_verify_token'] if channel.present?
token == whatsapp_webhook_verify_token if whatsapp_webhook_verify_token.present?
end
end end

View file

@ -1,9 +1,10 @@
class WidgetTestsController < ActionController::Base class WidgetTestsController < ActionController::Base
before_action :ensure_dark_mode
before_action :ensure_loading_type
before_action :ensure_web_widget before_action :ensure_web_widget
before_action :ensure_widget_position before_action :ensure_widget_position
before_action :ensure_widget_type
before_action :ensure_widget_style before_action :ensure_widget_style
before_action :ensure_dark_mode before_action :ensure_widget_type
def index def index
render render
@ -19,6 +20,10 @@ class WidgetTestsController < ActionController::Base
@dark_mode = params[:dark_mode] || 'light' @dark_mode = params[:dark_mode] || 'light'
end end
def ensure_loading_type
@loading_type = params[:loading_type] || 'immediate'
end
def ensure_widget_position def ensure_widget_position
@widget_position = params[:position] || 'left' @widget_position = params[:position] || 'left'
end end

View file

@ -1,10 +1,7 @@
# TODO : Delete this and associated spec once 'api/widget/config' end point is merged # TODO : Delete this and associated spec once 'api/widget/config' end point is merged
class WidgetsController < ActionController::Base class WidgetsController < ActionController::Base
include WidgetHelper
before_action :set_global_config before_action :set_global_config
before_action :set_web_widget before_action :set_web_widget
before_action :ensure_account_is_active
before_action :set_token before_action :set_token
before_action :set_contact before_action :set_contact
before_action :build_contact before_action :build_contact
@ -43,12 +40,11 @@ class WidgetsController < ActionController::Base
def build_contact def build_contact
return if @contact.present? return if @contact.present?
@contact_inbox, @token = build_contact_inbox_with_token(@web_widget, additional_attributes) @contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
@contact = @contact_inbox.contact @contact = @contact_inbox.contact
end
def ensure_account_is_active payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active? @token = ::Widget::TokenService.new(payload: payload).generate_token
end end
def additional_attributes def additional_attributes

View file

@ -8,15 +8,7 @@ class AccountDashboard < Administrate::BaseDashboard
# which determines how the attribute is displayed # which determines how the attribute is displayed
# on pages throughout the dashboard. # on pages throughout the dashboard.
enterprise_attribute_types = if ChatwootApp.enterprise? enterprise_attribute_types = ChatwootApp.enterprise? ? { limits: Enterprise::AccountLimitsField } : {}
{
limits: Enterprise::AccountLimitsField,
all_features: Enterprise::AccountFeaturesField
}
else
{}
end
ATTRIBUTE_TYPES = { ATTRIBUTE_TYPES = {
id: Field::Number, id: Field::Number,
name: Field::String, name: Field::String,
@ -25,7 +17,6 @@ class AccountDashboard < Administrate::BaseDashboard
users: CountField, users: CountField,
conversations: CountField, conversations: CountField,
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }), locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
status: Field::Select.with_options(collection: [%w[Active active], %w[Suspended suspended]]),
account_users: Field::HasMany account_users: Field::HasMany
}.merge(enterprise_attribute_types).freeze }.merge(enterprise_attribute_types).freeze
@ -40,19 +31,17 @@ class AccountDashboard < Administrate::BaseDashboard
locale locale
users users
conversations conversations
status
].freeze ].freeze
# SHOW_PAGE_ATTRIBUTES # SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page. # an array of attributes that will be displayed on the model's show page.
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : [] enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits] : []
SHOW_PAGE_ATTRIBUTES = (%i[ SHOW_PAGE_ATTRIBUTES = (%i[
id id
name name
created_at created_at
updated_at updated_at
locale locale
status
conversations conversations
account_users account_users
] + enterprise_show_page_attributes).freeze ] + enterprise_show_page_attributes).freeze
@ -60,11 +49,10 @@ class AccountDashboard < Administrate::BaseDashboard
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
# an array of attributes that will be displayed # an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages. # on the model's form (`new` and `edit`) pages.
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : [] enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits] : []
FORM_ATTRIBUTES = (%i[ FORM_ATTRIBUTES = (%i[
name name
locale locale
status
] + enterprise_form_attributes).freeze ] + enterprise_form_attributes).freeze
# COLLECTION_FILTERS # COLLECTION_FILTERS

View file

@ -6,7 +6,7 @@ class ConversationDrop < BaseDrop
end end
def contact_name def contact_name
@obj.try(:contact).name.try(:capitalize) || 'Customer' @obj.try(:contact).name.capitalize || 'Customer'
end end
def recent_messages def recent_messages

View file

@ -2,8 +2,6 @@ require 'administrate/field/base'
class AvatarField < Administrate::Field::Base class AvatarField < Administrate::Field::Base
def avatar_url def avatar_url
return data.presence if data.presence data.presence&.gsub('?d=404', '?d=mp')
resource.is_a?(User) ? '/assets/administrate/user/avatar.png' : '/assets/administrate/bot/avatar.png'
end end
end end

View file

@ -1,7 +0,0 @@
require 'administrate/field/base'
class Enterprise::AccountFeaturesField < Administrate::Field::Base
def to_s
data
end
end

View file

@ -2,11 +2,6 @@ class ConversationFinder
attr_reader :current_user, :current_account, :params attr_reader :current_user, :current_account, :params
DEFAULT_STATUS = 'open'.freeze DEFAULT_STATUS = 'open'.freeze
SORT_OPTIONS = {
latest: 'latest',
sort_on_created_at: 'sort_on_created_at',
last_user_message_at: 'last_user_message_at'
}.with_indifferent_access
# assumptions # assumptions
# inbox_id if not given, take from all conversations, else specific to inbox # inbox_id if not given, take from all conversations, else specific to inbox
@ -56,6 +51,7 @@ class ConversationFinder
filter_by_team if @team filter_by_team if @team
filter_by_labels if params[:labels] filter_by_labels if params[:labels]
filter_by_query if params[:q] filter_by_query if params[:q]
filter_by_reply_status
end end
def set_inboxes def set_inboxes
@ -75,9 +71,12 @@ class ConversationFinder
end end
def find_all_conversations def find_all_conversations
if params[:conversation_type] == 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = current_account.conversations.where(id: conversation_ids)
else
@conversations = current_account.conversations.where(inbox_id: @inbox_ids) @conversations = current_account.conversations.where(inbox_id: @inbox_ids)
filter_by_conversation_type if params[:conversation_type] end
@conversations
end end
def filter_by_assignee_type def filter_by_assignee_type
@ -92,15 +91,8 @@ class ConversationFinder
@conversations @conversations
end end
def filter_by_conversation_type def filter_by_reply_status
case @params[:conversation_type] @conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended'
when 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = @conversations.where(id: conversation_ids)
when 'unattended'
@conversations = @conversations.where(first_reply_created_at: nil)
end
@conversations
end end
def filter_by_query def filter_by_query
@ -141,7 +133,10 @@ class ConversationFinder
@conversations = @conversations.includes( @conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox :taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
) )
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest'] if params[:conversation_type] == 'mention'
@conversations.send(sort_by).page(current_page) @conversations.page(current_page)
else
@conversations.latest.page(current_page)
end
end end
end end

View file

@ -21,9 +21,7 @@ class MessageFinder
end end
def current_messages def current_messages
if @params[:after].present? if @params[:before].present?
messages.reorder('created_at asc').where('id >= ?', @params[:before].to_i).limit(20)
elsif @params[:before].present?
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
else else
messages.reorder('created_at desc').limit(20).reverse messages.reorder('created_at desc').limit(20).reverse

View file

@ -53,13 +53,13 @@ module Api::V1::InboxesHelper
rescue StandardError => e rescue StandardError => e
raise StandardError, e.message raise StandardError, e.message
ensure ensure
ChatwootExceptionTracker.new(e).capture_exception if e.present? Rails.logger.error e if e.present?
end end
def check_smtp_connection(channel_data, smtp) def check_smtp_connection(channel_data, smtp)
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password], smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
channel_data[:smtp_authentication]&.to_sym || :login) channel_data[:smtp_authentication]&.to_sym || :login)
smtp.finish smtp.finish unless smtp&.nil?
end end
def set_smtp_encryption(channel_data, smtp) def set_smtp_encryption(channel_data, smtp)

View file

@ -8,12 +8,6 @@ module FileTypeHelper
:file :file
end end
# Used in case of DIRECT_UPLOADS_ENABLED=true
def file_type_by_signed_id(signed_id)
blob = ActiveStorage::Blob.find_signed(signed_id)
file_type(blob&.content_type)
end
def image_file?(content_type) def image_file?(content_type)
[ [
'image/jpeg', 'image/jpeg',

View file

@ -1,9 +0,0 @@
module WidgetHelper
def build_contact_inbox_with_token(web_widget, additional_attributes = {})
contact_inbox = web_widget.create_contact_inbox(additional_attributes)
payload = { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id }
token = ::Widget::TokenService.new(payload: payload).generate_token
[contact_inbox, token]
end
end

View file

@ -87,9 +87,6 @@ export default {
}, },
async initializeAccount() { async initializeAccount() {
await this.$store.dispatch('accounts/get'); await this.$store.dispatch('accounts/get');
this.$store.dispatch('setActiveAccount', {
accountId: this.currentAccountId,
});
const { const {
locale, locale,
latest_chatwoot_version: latestChatwootVersion, latest_chatwoot_version: latestChatwootVersion,

View file

@ -15,11 +15,6 @@ class ApiClient {
baseUrl() { baseUrl() {
let url = this.apiVersion; let url = this.apiVersion;
if (this.options.enterprise) {
url = `/enterprise${url}`;
}
if (this.options.accountScoped) { if (this.options.accountScoped) {
const isInsideAccountScopedURLs = window.location.pathname.includes( const isInsideAccountScopedURLs = window.location.pathname.includes(
'/app/accounts' '/app/accounts'

View file

@ -1,9 +0,0 @@
import ApiClient from './ApiClient';
class AgentBotsAPI extends ApiClient {
constructor() {
super('agent_bots', { accountScoped: true });
}
}
export default new AgentBotsAPI();

View file

@ -144,22 +144,7 @@ export default {
}); });
}, },
updateAutoOffline(accountId, autoOffline = false) {
return axios.post(endPoints('autoOffline').url, {
profile: { account_id: accountId, auto_offline: autoOffline },
});
},
deleteAvatar() { deleteAvatar() {
return axios.delete(endPoints('deleteAvatar').url); return axios.delete(endPoints('deleteAvatar').url);
}, },
setActiveAccount({ accountId }) {
const urlData = endPoints('setActiveAccount');
return axios.put(urlData.url, {
profile: {
account_id: accountId,
},
});
},
}; };

View file

@ -71,10 +71,6 @@ class ContactAPI extends ApiClient {
custom_attributes: customAttributes, custom_attributes: customAttributes,
}); });
} }
destroyAvatar(contactId) {
return axios.delete(`${this.url}/${contactId}/avatar`);
}
} }
export default new ContactAPI(); export default new ContactAPI();

View file

@ -16,9 +16,6 @@ const endPoints = {
availabilityUpdate: { availabilityUpdate: {
url: '/api/v1/profile/availability', url: '/api/v1/profile/availability',
}, },
autoOffline: {
url: '/api/v1/profile/auto_offline',
},
logout: { logout: {
url: 'auth/sign_out', url: 'auth/sign_out',
}, },
@ -43,10 +40,6 @@ const endPoints = {
deleteAvatar: { deleteAvatar: {
url: '/api/v1/profile/avatar', url: '/api/v1/profile/avatar',
}, },
setActiveAccount: {
url: '/api/v1/profile/set_active_account',
},
}; };
export default page => { export default page => {

View file

@ -1,18 +0,0 @@
/* global axios */
import ApiClient from '../ApiClient';
class EnterpriseAccountAPI extends ApiClient {
constructor() {
super('', { accountScoped: true, enterprise: true });
}
checkout() {
return axios.post(`${this.url}checkout`);
}
subscription() {
return axios.post(`${this.url}subscription`);
}
}
export default new EnterpriseAccountAPI();

Some files were not shown because too many files have changed in this diff Show more