Compare commits
20 commits
develop
...
self-hoste
Author | SHA1 | Date | |
---|---|---|---|
|
74c4017d12 | ||
|
2954d938e7 | ||
|
681dbeaf46 | ||
|
0c0f410727 | ||
|
b9a5b15de1 | ||
|
53797d8795 | ||
|
0910eb550f | ||
|
b68f2e9c92 | ||
|
3128529d84 | ||
|
d4bdfa1d53 | ||
|
94eb18ee8a | ||
|
b56fed2435 | ||
|
02c66e5c1d | ||
|
65b057d67c | ||
|
965a8ba3a9 | ||
|
1f42a0f661 | ||
|
457368180f | ||
|
5f41c1211c | ||
|
444ccfd920 | ||
|
f41b30b485 |
1849 changed files with 13106 additions and 97686 deletions
|
@ -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
|
||||||
|
@ -118,79 +113,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
|
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
32
.env.example
32
.env.example
|
@ -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
|
||||||
|
@ -209,7 +199,3 @@ ENABLE_PUSH_RELAY_SERVER=true
|
||||||
# Stripe API key
|
# Stripe API key
|
||||||
STRIPE_SECRET_KEY=
|
STRIPE_SECRET_KEY=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
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=
|
|
||||||
|
|
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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.
|
||||||
|
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -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
|
||||||
|
|
36
.github/workflows/lock.yml
vendored
36
.github/workflows/lock.yml
vendored
|
@ -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.
|
|
46
.github/workflows/nightly_installer.yml
vendored
46
.github/workflows/nightly_installer.yml
vendored
|
@ -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
|
|
||||||
|
|
1
.github/workflows/publish_foss_docker.yml
vendored
1
.github/workflows/publish_foss_docker.yml
vendored
|
@ -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
5
.gitignore
vendored
|
@ -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
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
32
.vscode/extensions.json
vendored
32
.vscode/extensions.json
vendored
|
@ -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",
|
|
||||||
]
|
|
||||||
}
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -1 +0,0 @@
|
||||||
{}
|
|
15
Gemfile
15
Gemfile
|
@ -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', '~>6.1'
|
||||||
# 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'
|
||||||
|
|
||||||
|
@ -91,10 +91,9 @@ gem 'google-cloud-dialogflow'
|
||||||
|
|
||||||
##-- 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'
|
||||||
|
|
||||||
|
@ -131,10 +130,6 @@ gem 'pg_search'
|
||||||
# Subscriptions, Billing
|
# Subscriptions, Billing
|
||||||
gem 'stripe'
|
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'
|
||||||
|
@ -171,11 +166,11 @@ group :development, :test do
|
||||||
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
|
||||||
|
|
53
Gemfile.lock
53
Gemfile.lock
|
@ -135,7 +135,7 @@ GEM
|
||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
climate_control (1.1.1)
|
climate_control (1.1.1)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
commonmarker (0.23.6)
|
commonmarker (0.23.5)
|
||||||
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)
|
||||||
|
@ -182,9 +182,6 @@ GEM
|
||||||
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)
|
||||||
|
@ -229,9 +226,6 @@ GEM
|
||||||
faraday (>= 1.0.0, < 3.0)
|
faraday (>= 1.0.0, < 3.0)
|
||||||
googleauth (~> 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)
|
||||||
|
@ -286,9 +280,9 @@ 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.21.2)
|
||||||
google-protobuf (3.21.7-x86_64-darwin)
|
google-protobuf (3.21.2-x86_64-darwin)
|
||||||
google-protobuf (3.21.7-x86_64-linux)
|
google-protobuf (3.21.2-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)
|
||||||
|
@ -324,15 +318,9 @@ 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.5)
|
||||||
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)
|
||||||
|
@ -395,10 +383,7 @@ GEM
|
||||||
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.18.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)
|
||||||
|
@ -427,14 +412,14 @@ GEM
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic_rpm (8.9.0)
|
newrelic_rpm (8.9.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.10)
|
nokogiri (1.13.7)
|
||||||
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.7-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.10-x86_64-darwin)
|
nokogiri (1.13.7-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.10-x86_64-linux)
|
nokogiri (1.13.7-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.10)
|
oauth (0.5.10)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
@ -459,7 +444,7 @@ GEM
|
||||||
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.4)
|
||||||
rack-attack (6.6.1)
|
rack-attack (6.6.1)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
|
@ -488,8 +473,8 @@ GEM
|
||||||
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.3)
|
||||||
loofah (~> 2.19, >= 2.19.1)
|
loofah (~> 2.3)
|
||||||
railties (6.1.6.1)
|
railties (6.1.6.1)
|
||||||
actionpack (= 6.1.6.1)
|
actionpack (= 6.1.6.1)
|
||||||
activesupport (= 6.1.6.1)
|
activesupport (= 6.1.6.1)
|
||||||
|
@ -536,8 +521,6 @@ 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)
|
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
|
||||||
rubocop (1.31.2)
|
rubocop (1.31.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
@ -726,7 +709,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 +747,19 @@ DEPENDENCIES
|
||||||
rack-attack
|
rack-attack
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 6.1, >= 6.1.6.1)
|
rails (~> 6.1)
|
||||||
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
|
||||||
|
@ -799,7 +780,7 @@ DEPENDENCIES
|
||||||
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 +789,4 @@ RUBY VERSION
|
||||||
ruby 3.0.4p208
|
ruby 3.0.4p208
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.3.16
|
2.3.14
|
||||||
|
|
|
@ -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>
|
||||||
|
|
49
SECURITY.md
49
SECURITY.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2.2.0
|
2.6.0
|
||||||
|
|
15
app.json
15
app.json
|
@ -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"
|
||||||
|
|
|
@ -104,7 +104,7 @@ class ContactIdentifyAction
|
||||||
# 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.discard_invalid_attrs if discard_invalid_attrs
|
||||||
@contact.save!
|
@contact.save!
|
||||||
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
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)
|
||||||
|
|
|
@ -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?
|
||||||
|
|
76
app/builders/contact_builder.rb
Normal file
76
app/builders/contact_builder.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -63,7 +61,6 @@ class Messages::Messenger::MessageBuilder
|
||||||
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
|
||||||
|
@ -76,7 +73,6 @@ class Messages::Messenger::MessageBuilder
|
||||||
raise
|
raise
|
||||||
rescue Koala::Facebook::ClientError => e
|
rescue Koala::Facebook::ClientError => e
|
||||||
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
# 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'))
|
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
{}
|
{}
|
||||||
|
@ -84,10 +80,4 @@ class Messages::Messenger::MessageBuilder
|
||||||
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
|
||||||
|
|
|
@ -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}?")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,13 +2,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
before_action :portal
|
before_action :portal
|
||||||
before_action :check_authorization
|
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 = @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
|
||||||
|
@ -38,21 +35,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, :associated_article_id, :status
|
||||||
{ 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, :page
|
||||||
|
)
|
||||||
def set_current_page
|
|
||||||
@current_page = params[:page] || 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,10 +2,8 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||||
before_action :portal
|
before_action :portal
|
||||||
before_action :check_authorization
|
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
|
||||||
|
|
||||||
|
@ -51,8 +49,4 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||||
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
|
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_current_page
|
|
||||||
@current_page = params[:page] || 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -134,11 +134,8 @@ 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 permitted_params
|
||||||
|
@ -169,7 +166,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_avatar
|
def process_avatar
|
||||||
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
if permitted_params[:avatar].blank? && permitted_params[:avatar_url].present?
|
||||||
|
::ContactAvatarJob.perform_later(@contact, params[:avatar_url])
|
||||||
|
elsif permitted_params[:avatar].blank? && permitted_params[:email].present?
|
||||||
|
hash = Digest::MD5.hexdigest(params[:email])
|
||||||
|
gravatar_url = "https://www.gravatar.com/avatar/#{hash}?d=404"
|
||||||
|
::ContactAvatarJob.perform_later(@contact, gravatar_url)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_error(error, error_status)
|
def render_error(error, error_status)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = [])
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
|
before_action :check_authorization
|
||||||
before_action :check_authorization, only: [:show, :update, :destroy, :execute]
|
before_action :fetch_macro, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@macros = Macro.with_visibility(current_user, params)
|
@macros = Macro.with_visibility(current_user, params)
|
||||||
|
@ -14,34 +14,19 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
||||||
|
|
||||||
@macro.save!
|
@macro.save!
|
||||||
process_attachments
|
|
||||||
@macro
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show; end
|
||||||
head :not_found if @macro.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@macro.destroy!
|
@macro.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
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
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@macro.update!(macros_with_user)
|
@macro.update!(macros_with_user)
|
||||||
@macro.set_visibility(current_user, permitted_params)
|
@macro.set_visibility(current_user, permitted_params)
|
||||||
process_attachments
|
|
||||||
@macro.save!
|
@macro.save!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
|
@ -49,25 +34,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
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
|
def permitted_params
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :account_id, :visibility,
|
:name, :account_id, :visibility,
|
||||||
|
@ -82,8 +48,4 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
def fetch_macro
|
def fetch_macro
|
||||||
@macro = Current.account.macros.find_by(id: params[:id])
|
@macro = Current.account.macros.find_by(id: params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
authorize(@macro) if @macro.present?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
before_action :fetch_portal, except: [:index, :create]
|
before_action :fetch_portal, except: [:index, :create]
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@portals = Current.account.portals
|
@portals = Current.account.portals
|
||||||
|
@ -14,14 +13,12 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
@portal.members << agents
|
@portal.members << agents
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show; end
|
||||||
@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.build(portal_params)
|
||||||
@portal.custom_domain = parsed_custom_domain
|
render json: { error: @portal.errors.messages }, status: :unprocessable_entity and return unless @portal.valid?
|
||||||
|
|
||||||
@portal.save!
|
@portal.save!
|
||||||
process_attached_logo
|
process_attached_logo
|
||||||
end
|
end
|
||||||
|
@ -29,7 +26,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@portal.update!(portal_params) if params[:portal].present?
|
@portal.update!(portal_params) if params[:portal].present?
|
||||||
# @portal.custom_domain = parsed_custom_domain
|
|
||||||
process_attached_logo
|
process_attached_logo
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
|
@ -63,21 +59,11 @@ 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, config: { allowed_locales: [] }
|
||||||
{ allowed_locales: [] }] }
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal_member_params
|
def portal_member_params
|
||||||
params.require(:portal).permit(:account_id, member_ids: [])
|
params.require(:portal).permit(:account_id, member_ids: [])
|
||||||
end
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -36,10 +36,9 @@ 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 || {}
|
||||||
}
|
}
|
||||||
|
@ -50,9 +49,7 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
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
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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
|
||||||
|
|
||||||
|
@ -17,8 +17,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
@contact = ContactIdentifyAction.new(
|
@contact = ContactIdentifyAction.new(
|
||||||
contact: @contact,
|
contact: @contact,
|
||||||
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
|
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
|
||||||
retain_original_contact_name: true,
|
retain_original_contact_name: true
|
||||||
discard_invalid_attrs: true
|
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -60,7 +59,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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]
|
after_action :allow_iframe_requests
|
||||||
|
|
||||||
layout 'vueapp'
|
layout 'vueapp'
|
||||||
|
|
||||||
|
@ -16,7 +16,8 @@ class DashboardController < ActionController::Base
|
||||||
@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,12 @@ 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',
|
'DEPLOYMENT_ENV'
|
||||||
'CSML_EDITOR_HOST'
|
|
||||||
).merge(app_config)
|
).merge(app_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,15 +38,8 @@ 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
|
def allow_iframe_requests
|
||||||
domain = request.host
|
response.headers.delete('X-Frame-Options')
|
||||||
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
|
end
|
||||||
|
|
||||||
def app_config
|
def app_config
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -16,18 +16,18 @@ 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
|
||||||
@resource.invalidate_sso_auth_token(params[:sso_auth_token])
|
# @resource.invalidate_sso_auth_token(params[:sso_auth_token])
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_sso_auth_token
|
def process_sso_auth_token
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
class Public::Api::V1::Portals::ArticlesController < PublicController
|
class Public::Api::V1::Portals::ArticlesController < ApplicationController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :set_portal
|
||||||
before_action :portal
|
|
||||||
before_action :set_category, except: [:index]
|
|
||||||
before_action :set_article, only: [:show]
|
before_action :set_article, only: [:show]
|
||||||
layout 'portal'
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@articles = @portal.articles
|
@articles = @portal.articles
|
||||||
@articles = @articles.search(list_params) if list_params.present?
|
@articles = @articles.search(list_params) if params[:payload].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
@ -15,25 +12,14 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_article
|
def set_article
|
||||||
@article = @category.articles.find(params[:id])
|
@article = @portal.articles.find(params[:id])
|
||||||
@parsed_content = render_article_content(@article.content)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_category
|
def set_portal
|
||||||
@category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present?
|
@portal = ::Portal.find_by!(slug: params[:portal_slug], archived: false)
|
||||||
end
|
|
||||||
|
|
||||||
def portal
|
|
||||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:query)
|
params.require(:payload).permit(:query)
|
||||||
end
|
|
||||||
|
|
||||||
def render_article_content(content)
|
|
||||||
# rubocop:disable Rails/OutputSafety
|
|
||||||
CommonMarker.render_html(content).html_safe
|
|
||||||
# rubocop:enable Rails/OutputSafety
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
class Public::Api::V1::Portals::CategoriesController < PublicController
|
class Public::Api::V1::Portals::CategoriesController < PublicController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :set_portal
|
||||||
before_action :portal
|
|
||||||
before_action :set_category, only: [:show]
|
before_action :set_category, only: [:show]
|
||||||
layout 'portal'
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@categories = @portal.categories
|
@categories = @portal.categories
|
||||||
|
@ -13,10 +11,10 @@ class Public::Api::V1::Portals::CategoriesController < PublicController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_category
|
def set_category
|
||||||
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
@category = @portal.categories.find_by!(slug: params[:slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal
|
def set_portal
|
||||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
@portal = ::Portal.find_by!(slug: params[:portal_slug], archived: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,21 +1,11 @@
|
||||||
class Public::Api::V1::PortalsController < PublicController
|
class Public::Api::V1::PortalsController < PublicController
|
||||||
before_action :ensure_custom_domain_request, only: [:show]
|
before_action :set_portal
|
||||||
before_action :portal
|
|
||||||
before_action :redirect_to_portal_with_locale, only: [:show]
|
|
||||||
layout 'portal'
|
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def portal
|
def set_portal
|
||||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
@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
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,6 @@ class WidgetsController < ActionController::Base
|
||||||
|
|
||||||
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
|
||||||
|
@ -47,10 +46,6 @@ class WidgetsController < ActionController::Base
|
||||||
@contact = @contact_inbox.contact
|
@contact = @contact_inbox.contact
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_account_is_active
|
|
||||||
render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active?
|
|
||||||
end
|
|
||||||
|
|
||||||
def additional_attributes
|
def additional_attributes
|
||||||
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
|
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
|
||||||
{ created_at_ip: request.remote_ip }
|
{ created_at_ip: request.remote_ip }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
require 'administrate/field/base'
|
|
||||||
|
|
||||||
class Enterprise::AccountFeaturesField < Administrate::Field::Base
|
|
||||||
def to_s
|
|
||||||
data
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -56,6 +56,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 +76,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 +96,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -53,7 +53,7 @@ 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)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import ApiClient from './ApiClient';
|
|
||||||
|
|
||||||
class AgentBotsAPI extends ApiClient {
|
|
||||||
constructor() {
|
|
||||||
super('agent_bots', { accountScoped: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new AgentBotsAPI();
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
/* global axios */
|
|
||||||
|
|
||||||
import PortalsAPI from './portals';
|
|
||||||
|
|
||||||
class ArticlesAPI extends PortalsAPI {
|
|
||||||
constructor() {
|
|
||||||
super('articles', { accountScoped: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
getArticles({
|
|
||||||
pageNumber,
|
|
||||||
portalSlug,
|
|
||||||
locale,
|
|
||||||
status,
|
|
||||||
author_id,
|
|
||||||
category_slug,
|
|
||||||
}) {
|
|
||||||
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
|
|
||||||
if (status !== undefined) baseUrl += `&status=${status}`;
|
|
||||||
if (author_id) baseUrl += `&author_id=${author_id}`;
|
|
||||||
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
|
|
||||||
return axios.get(baseUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
getArticle({ id, portalSlug }) {
|
|
||||||
return axios.get(`${this.url}/${portalSlug}/articles/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateArticle({ portalSlug, articleId, articleObj }) {
|
|
||||||
return axios.patch(
|
|
||||||
`${this.url}/${portalSlug}/articles/${articleId}`,
|
|
||||||
articleObj
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createArticle({ portalSlug, articleObj }) {
|
|
||||||
const { content, title, author_id, category_id } = articleObj;
|
|
||||||
return axios.post(`${this.url}/${portalSlug}/articles`, {
|
|
||||||
content,
|
|
||||||
title,
|
|
||||||
author_id,
|
|
||||||
category_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteArticle({ articleId, portalSlug }) {
|
|
||||||
return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new ArticlesAPI();
|
|
|
@ -1,30 +0,0 @@
|
||||||
/* global axios */
|
|
||||||
|
|
||||||
import PortalsAPI from './portals';
|
|
||||||
|
|
||||||
class CategoriesAPI extends PortalsAPI {
|
|
||||||
constructor() {
|
|
||||||
super('categories', { accountScoped: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
get({ portalSlug, locale }) {
|
|
||||||
return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
create({ portalSlug, categoryObj }) {
|
|
||||||
return axios.post(`${this.url}/${portalSlug}/categories`, categoryObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
update({ portalSlug, categoryId, categoryObj }) {
|
|
||||||
return axios.patch(
|
|
||||||
`${this.url}/${portalSlug}/categories/${categoryId}`,
|
|
||||||
categoryObj
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete({ portalSlug, categoryId }) {
|
|
||||||
return axios.delete(`${this.url}/${portalSlug}/categories/${categoryId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new CategoriesAPI();
|
|
|
@ -1,22 +0,0 @@
|
||||||
/* global axios */
|
|
||||||
import ApiClient from '../ApiClient';
|
|
||||||
|
|
||||||
class PortalsAPI extends ApiClient {
|
|
||||||
constructor() {
|
|
||||||
super('portals', { accountScoped: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
getPortal({ portalSlug, locale }) {
|
|
||||||
return axios.get(`${this.url}/${portalSlug}?locale=${locale}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePortal({ portalSlug, portalObj }) {
|
|
||||||
return axios.patch(`${this.url}/${portalSlug}`, portalObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
deletePortal(portalSlug) {
|
|
||||||
return axios.delete(`${this.url}/${portalSlug}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PortalsAPI;
|
|
|
@ -68,10 +68,6 @@ class ConversationApi extends ApiClient {
|
||||||
return axios.post(`${this.url}/${id}/update_last_seen`);
|
return axios.post(`${this.url}/${id}/update_last_seen`);
|
||||||
}
|
}
|
||||||
|
|
||||||
markMessagesUnread({ id }) {
|
|
||||||
return axios.post(`${this.url}/${id}/unread`);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTyping({ conversationId, status, isPrivate }) {
|
toggleTyping({ conversationId, status, isPrivate }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
||||||
typing_status: status,
|
typing_status: status,
|
||||||
|
@ -109,16 +105,6 @@ class ConversationApi extends ApiClient {
|
||||||
custom_attributes: customAttributes,
|
custom_attributes: customAttributes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchParticipants(conversationId) {
|
|
||||||
return axios.get(`${this.url}/${conversationId}/participants`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParticipants({ conversationId, userIds }) {
|
|
||||||
return axios.patch(`${this.url}/${conversationId}/participants`, {
|
|
||||||
user_ids: userIds,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ConversationApi();
|
export default new ConversationApi();
|
||||||
|
|
|
@ -13,16 +13,6 @@ class Inboxes extends ApiClient {
|
||||||
deleteInboxAvatar(inboxId) {
|
deleteInboxAvatar(inboxId) {
|
||||||
return axios.delete(`${this.url}/${inboxId}/avatar`);
|
return axios.delete(`${this.url}/${inboxId}/avatar`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAgentBot(inboxId) {
|
|
||||||
return axios.get(`${this.url}/${inboxId}/agent_bot`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAgentBot(inboxId, botId) {
|
|
||||||
return axios.post(`${this.url}/${inboxId}/set_agent_bot`, {
|
|
||||||
agent_bot: botId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Inboxes();
|
export default new Inboxes();
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
/* global axios */
|
|
||||||
import ApiClient from './ApiClient';
|
|
||||||
|
|
||||||
class MacrosAPI extends ApiClient {
|
|
||||||
constructor() {
|
|
||||||
super('macros', { accountScoped: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
executeMacro({ macroId, conversationIds }) {
|
|
||||||
return axios.post(`${this.url}/${macroId}/execute`, {
|
|
||||||
conversation_ids: conversationIds,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new MacrosAPI();
|
|
|
@ -1,13 +0,0 @@
|
||||||
import AgentBotsAPI from '../agentBots';
|
|
||||||
import ApiClient from '../ApiClient';
|
|
||||||
|
|
||||||
describe('#AgentBotsAPI', () => {
|
|
||||||
it('creates correct instance', () => {
|
|
||||||
expect(AgentBotsAPI).toBeInstanceOf(ApiClient);
|
|
||||||
expect(AgentBotsAPI).toHaveProperty('get');
|
|
||||||
expect(AgentBotsAPI).toHaveProperty('show');
|
|
||||||
expect(AgentBotsAPI).toHaveProperty('create');
|
|
||||||
expect(AgentBotsAPI).toHaveProperty('update');
|
|
||||||
expect(AgentBotsAPI).toHaveProperty('delete');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,66 +0,0 @@
|
||||||
import articlesAPI from '../helpCenter/articles';
|
|
||||||
import ApiClient from 'dashboard/api/helpCenter/portals';
|
|
||||||
import describeWithAPIMock from './apiSpecHelper';
|
|
||||||
|
|
||||||
describe('#PortalAPI', () => {
|
|
||||||
it('creates correct instance', () => {
|
|
||||||
expect(articlesAPI).toBeInstanceOf(ApiClient);
|
|
||||||
expect(articlesAPI).toHaveProperty('get');
|
|
||||||
expect(articlesAPI).toHaveProperty('show');
|
|
||||||
expect(articlesAPI).toHaveProperty('create');
|
|
||||||
expect(articlesAPI).toHaveProperty('update');
|
|
||||||
expect(articlesAPI).toHaveProperty('delete');
|
|
||||||
expect(articlesAPI).toHaveProperty('getArticles');
|
|
||||||
});
|
|
||||||
describeWithAPIMock('API calls', context => {
|
|
||||||
it('#getArticles', () => {
|
|
||||||
articlesAPI.getArticles({
|
|
||||||
pageNumber: 1,
|
|
||||||
portalSlug: 'room-rental',
|
|
||||||
locale: 'en-US',
|
|
||||||
status: 'published',
|
|
||||||
author_id: '1',
|
|
||||||
});
|
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
|
||||||
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describeWithAPIMock('API calls', context => {
|
|
||||||
it('#getArticle', () => {
|
|
||||||
articlesAPI.getArticle({
|
|
||||||
id: 1,
|
|
||||||
portalSlug: 'room-rental',
|
|
||||||
});
|
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
|
||||||
'/api/v1/portals/room-rental/articles/1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describeWithAPIMock('API calls', context => {
|
|
||||||
it('#updateArticle', () => {
|
|
||||||
articlesAPI.updateArticle({
|
|
||||||
articleId: 1,
|
|
||||||
portalSlug: 'room-rental',
|
|
||||||
articleObj: { title: 'Update shipping address' },
|
|
||||||
});
|
|
||||||
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
|
||||||
'/api/v1/portals/room-rental/articles/1',
|
|
||||||
{
|
|
||||||
title: 'Update shipping address',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describeWithAPIMock('API calls', context => {
|
|
||||||
it('#deleteArticle', () => {
|
|
||||||
articlesAPI.deleteArticle({
|
|
||||||
articleId: 1,
|
|
||||||
portalSlug: 'room-rental',
|
|
||||||
});
|
|
||||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
|
||||||
'/api/v1/portals/room-rental/articles/1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,12 +0,0 @@
|
||||||
import categoriesAPI from '../../helpCenter/categories';
|
|
||||||
import ApiClient from '../../ApiClient';
|
|
||||||
|
|
||||||
describe('#BulkActionsAPI', () => {
|
|
||||||
it('creates correct instance', () => {
|
|
||||||
expect(categoriesAPI).toBeInstanceOf(ApiClient);
|
|
||||||
expect(categoriesAPI).toHaveProperty('get');
|
|
||||||
expect(categoriesAPI).toHaveProperty('create');
|
|
||||||
expect(categoriesAPI).toHaveProperty('update');
|
|
||||||
expect(categoriesAPI).toHaveProperty('delete');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -11,8 +11,6 @@ describe('#InboxesAPI', () => {
|
||||||
expect(inboxesAPI).toHaveProperty('update');
|
expect(inboxesAPI).toHaveProperty('update');
|
||||||
expect(inboxesAPI).toHaveProperty('delete');
|
expect(inboxesAPI).toHaveProperty('delete');
|
||||||
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
||||||
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
|
||||||
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
|
||||||
});
|
});
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
it('#getCampaigns', () => {
|
it('#getCampaigns', () => {
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import macros from '../macros';
|
|
||||||
import ApiClient from '../ApiClient';
|
|
||||||
|
|
||||||
describe('#macrosAPI', () => {
|
|
||||||
it('creates correct instance', () => {
|
|
||||||
expect(macros).toBeInstanceOf(ApiClient);
|
|
||||||
expect(macros).toHaveProperty('get');
|
|
||||||
expect(macros).toHaveProperty('create');
|
|
||||||
expect(macros).toHaveProperty('update');
|
|
||||||
expect(macros).toHaveProperty('delete');
|
|
||||||
expect(macros).toHaveProperty('show');
|
|
||||||
expect(macros.url).toBe('/api/v1/macros');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,13 +0,0 @@
|
||||||
import PortalsAPI from '../helpCenter/portals';
|
|
||||||
import ApiClient from '../ApiClient';
|
|
||||||
const portalAPI = new PortalsAPI();
|
|
||||||
describe('#PortalAPI', () => {
|
|
||||||
it('creates correct instance', () => {
|
|
||||||
expect(portalAPI).toBeInstanceOf(ApiClient);
|
|
||||||
expect(portalAPI).toHaveProperty('get');
|
|
||||||
expect(portalAPI).toHaveProperty('show');
|
|
||||||
expect(portalAPI).toHaveProperty('create');
|
|
||||||
expect(portalAPI).toHaveProperty('update');
|
|
||||||
expect(portalAPI).toHaveProperty('delete');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,6 +0,0 @@
|
||||||
/* global axios */
|
|
||||||
import wootConstants from 'dashboard/constants';
|
|
||||||
|
|
||||||
export const getTestimonialContent = () => {
|
|
||||||
return axios.get(wootConstants.TESTIMONIAL_URL);
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M63 63H32.9976C16.4591 63 2.99996 49.5399 2.99996 32.9973C2.99996 16.4601 16.4591 3 32.9979 3C49.5408 3 63 16.4601 63 32.9973V63Z" fill="white" stroke="white" stroke-width="6"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 289 B |
|
@ -1,9 +1,11 @@
|
||||||
|
/* Enter and leave animations can use different */
|
||||||
|
/* durations and timing functions. */
|
||||||
.slide-fade-enter-active {
|
.slide-fade-enter-active {
|
||||||
transition: all 0.3s var(--ease-in-cubic);
|
transition: all .3s $ease-in-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-fade-leave-active {
|
.slide-fade-leave-active {
|
||||||
transition: all 0.3s var(--ease-out-cubic);
|
transition: all .3s $ease-out-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-fade-enter,
|
.slide-fade-enter,
|
||||||
|
@ -22,7 +24,7 @@
|
||||||
|
|
||||||
.conversations-list-enter-active,
|
.conversations-list-enter-active,
|
||||||
.conversations-list-leave-active {
|
.conversations-list-leave-active {
|
||||||
transition: all 0.25s var(--ease-out-cubic);
|
transition: all .25s $ease-out-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversations-list-enter,
|
.conversations-list-enter,
|
||||||
|
@ -33,10 +35,11 @@
|
||||||
|
|
||||||
.menu-list-enter-active,
|
.menu-list-enter-active,
|
||||||
.menu-list-leave-active {
|
.menu-list-leave-active {
|
||||||
transition: opacity 0.3s var(--ease-out-cubic),
|
transition: opacity .3s $ease-out-cubic,
|
||||||
transform 0.2s var(--ease-out-cubic);
|
transform .2s $ease-out-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.menu-list-leave-to {
|
.menu-list-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -49,24 +52,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up-enter-active {
|
.slide-up-enter-active {
|
||||||
transition: all 0.3s var(--ease-in-cubic);
|
transition: all .3s $ease-in-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up-leave-active {
|
.slide-up-leave-active {
|
||||||
transition: all 0.3s var(--ease-out-cubic);
|
transition: all .3s $ease-out-cubic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up-enter,
|
.slide-up-enter,
|
||||||
.slide-up-leave-to {
|
.slide-up-leave-to {
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-$space-medium);
|
transform: translateY(-$space-medium);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-slide-enter-active,
|
.menu-slide-enter-active,
|
||||||
.menu-slide-leave-active {
|
.menu-slide-leave-active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
transition: transform 0.25s var(--ease-in-cubic),
|
transition: transform 0.25s $ease-in-cubic, opacity 0.15s $ease-in-cubic;
|
||||||
opacity 0.15s var(--ease-in-cubic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-slide-enter,
|
.menu-slide-enter,
|
||||||
|
@ -75,12 +77,13 @@
|
||||||
transform: translateY($space-small);
|
transform: translateY($space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.toast-fade-enter-active {
|
.toast-fade-enter-active {
|
||||||
transition: all 0.3s var(--ease-in-sine);
|
transition: all .3s $ease-in-sine;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-fade-leave-active {
|
.toast-fade-leave-active {
|
||||||
transition: all 0.1s var(--ease-out-sine);
|
transition: all .1s $ease-out-sine;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-fade-enter,
|
.toast-fade-enter,
|
||||||
|
@ -90,11 +93,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-fade-enter-active {
|
.modal-fade-enter-active {
|
||||||
transition: all 0.3s var(--ease-in-sine);
|
transition: all .3s $ease-in-sine;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-fade-leave-active {
|
.modal-fade-leave-active {
|
||||||
transition: all 0.1s var(--ease-out-sine);
|
transition: all .1s $ease-out-sine;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-fade-enter,
|
.modal-fade-enter,
|
||||||
|
@ -103,15 +106,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-notification-fade-enter-active {
|
.network-notification-fade-enter-active {
|
||||||
transition: all 0.1s var(--ease-in-sine);
|
transition: all .1s $ease-in-sine;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-notification-fade-leave-active {
|
.network-notification-fade-leave-active {
|
||||||
transition: all 0.1s var(--ease-out-sine);
|
transition: all .1s $ease-out-sine;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-notification-fade-enter,
|
.network-notification-fade-enter,
|
||||||
.network-notification-fade-leave-to {
|
.network-notification-fade-leave-to {
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-$space-small);
|
transform: translateY(-$space-small);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,8 +74,8 @@ Tahoma,
|
||||||
Arial,
|
Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
$body-antialiased: true;
|
$body-antialiased: true;
|
||||||
$global-margin: $space-small;
|
$global-margin: $space-one;
|
||||||
$global-padding: $space-micro;
|
$global-padding: $space-one;
|
||||||
$global-weight-normal: normal;
|
$global-weight-normal: normal;
|
||||||
$global-weight-bold: bold;
|
$global-weight-bold: bold;
|
||||||
$global-radius: 0;
|
$global-radius: 0;
|
||||||
|
@ -370,7 +370,7 @@ $input-font-weight: $global-weight-normal;
|
||||||
$input-background: $white;
|
$input-background: $white;
|
||||||
$input-background-focus: $white;
|
$input-background-focus: $white;
|
||||||
$input-background-disabled: $light-gray;
|
$input-background-disabled: $light-gray;
|
||||||
$input-border: 1px solid var(--s-200);
|
$input-border: 1px solid $color-border;
|
||||||
$input-border-focus: 1px solid lighten($primary-color, 15%);
|
$input-border-focus: 1px solid lighten($primary-color, 15%);
|
||||||
$input-shadow: 0;
|
$input-shadow: 0;
|
||||||
$input-shadow-focus: 0;
|
$input-shadow-focus: 0;
|
||||||
|
|
|
@ -41,22 +41,24 @@ is-closed .app-root {
|
||||||
|
|
||||||
.view-box {
|
.view-box {
|
||||||
@include full-height;
|
@include full-height;
|
||||||
|
@include margin(0);
|
||||||
@include space-between-column;
|
@include space-between-column;
|
||||||
|
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-panel {
|
.view-panel {
|
||||||
|
@include margin($zero);
|
||||||
|
@include padding($space-normal);
|
||||||
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: $space-normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-box {
|
.content-box {
|
||||||
|
@include padding($space-normal);
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: $space-normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
|
@ -89,7 +91,8 @@ is-closed .app-root {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@include padding($space-one);
|
||||||
|
|
||||||
max-width: $space-mega;
|
max-width: $space-mega;
|
||||||
padding: $space-one;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue