Merge branch 'develop' into feat/new-auth-screens

This commit is contained in:
Sivin Varghese 2022-09-15 09:25:24 +05:30 committed by GitHub
commit 8101fa42bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1499 changed files with 65853 additions and 8259 deletions

View file

@ -15,8 +15,11 @@ defaults: &defaults
- image: cimg/postgres:14.1
- image: cimg/redis:6.2.6
environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
- RAILS_LOG_TO_STDOUT: false
- COVERAGE: true
- LOG_LEVEL: warn
parallelism: 4
jobs:
build:
<<: *defaults
@ -88,6 +91,7 @@ jobs:
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
java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json
# Database setup
- run: yarn install --check-files
- run: bundle exec rake db:create
@ -100,6 +104,10 @@ jobs:
- run:
name: Rubocop
command: bundle exec rubocop
# - run:
# name: Brakeman
# command: bundle exec brakeman
- run:
name: eslint
@ -109,34 +117,77 @@ jobs:
- run:
name: Run backend tests
command: |
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 --format documentation
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
- persist_to_workspace:
root: ~/tmp
paths:
- codeclimate.backend.json
mkdir -p ~/tmp/test-results/rspec
mkdir -p ~/tmp/test-artifacts
mkdir -p coverage
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
bundle exec rspec --profile 10 \
--out test-results/rspec/rspec.xml \
-- ${TESTFILES}
- run:
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
- run:
name: Run frontend tests
command: |
yarn test:coverage
~/tmp/cc-test-reporter format-coverage -t lcov -o ~/tmp/codeclimate.frontend.json buildreports/lcov.info
mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
--out test-results/frontend_specs/rspec.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:
root: ~/tmp
root: coverage
paths:
- codeclimate.frontend.json
- codeclimate.*.json
# collect reports
- store_test_results:
path: ~/tmp/test-results
- store_artifacts:
path: ~/tmp/test-results
destination: test-results
path: ~/tmp/test-artifacts
- store_artifacts:
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:
name: Upload coverage results to Code Climate
command: |
~/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
~/tmp/cc-test-reporter sum-coverage --output - codeclimate.*.json | ~/tmp/cc-test-reporter upload-coverage --debug --input -
workflows:
version: 2
commit:
jobs:
- build
- upload-coverage:
requires:
- build

View file

@ -53,3 +53,4 @@ exclude_patterns:
- 'app/javascript/dashboard/i18n/index.js'
- 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js'
- 'app/javascript/shared/constants/locales.js'

View file

@ -5,4 +5,4 @@ FROM ghcr.io/chatwoot/chatwoot_codespace:latest
# Do the set up required for chatwoot app
WORKDIR /workspace
COPY . /workspace
RUN yarn && gem install bundler && bundle install
RUN yarn && gem install bundler && bundle install

View file

@ -1,6 +1,6 @@
# pre-build stage
ARG VARIANT=3
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
ARG VARIANT=ubuntu-20.04
FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
ARG USER_UID=1000
@ -11,23 +11,36 @@ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
fi
# [Option] Install Node.js
ARG INSTALL_NODE="true"
ARG NODE_VERSION="lts/*"
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends \
build-essential \
libssl-dev \
zlib1g-dev \
gnupg2 \
tar \
tzdata \
postgresql-client \
libpq-dev \
yarn \
git \
imagemagick \
tmux \
zsh
zsh \
git-flow \
npm
# Install rbenv and ruby
ARG RUBY_VERSION="3.0.4"
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH"
RUN git clone https://github.com/rbenv/ruby-build.git && \
PREFIX=/usr/local ./ruby-build/install.sh
RUN rbenv install $RUBY_VERSION && \
rbenv global $RUBY_VERSION && \
rbenv versions
# Install overmind
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
@ -35,11 +48,25 @@ RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmi
&& sudo mv overmind /usr/local/bin \
&& chmod +x /usr/local/bin/overmind
# Install gh
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& sudo apt update \
&& sudo apt install gh
# Do the set up required for chatwoot app
WORKDIR /workspace
COPY . /workspace
RUN yarn
# set up ruby
COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install
# set up node js
RUN npm install npm@latest -g && \
npm install n -g && \
n latest
RUN npm install --global yarn
RUN yarn

View file

@ -23,17 +23,18 @@
// 5432 postgres
// 6379 redis
// 1025,8025 mailhog
"forwardPorts": [8025],
//your application may need to listen on all interfaces (0.0.0.0) not just localhost for it to be available externally. Defaults to []
"appPort": [3000, 3035],
"forwardPorts": [8025, 3000, 3035],
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
"portsAttributes": {
"3000": {
"label": "Rails Server"
},
"3035": {
"label": "Webpack Dev Server"
},
"8025": {
"label": "Mailhog UI"
}
},
}
}

View file

@ -6,3 +6,8 @@ sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.de
sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
# uncomment the webpacker env variable
sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env
# fix the error with webpacker
echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc
# codespaces make the ports public
gh codespace ports visibility 3000:public 3035:public 8025:public -c $CODESPACE_NAME

View file

@ -38,6 +38,9 @@ REDIS_SENTINEL_MASTER_NAME=
# REDIS_OPENSSL_VERIFY_MODE=none
# 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_USERNAME=postgres
POSTGRES_PASSWORD=
@ -48,7 +51,6 @@ RAILS_MAX_THREADS=5
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
MAILER_SENDER_EMAIL="Chatwoot <accounts@chatwoot.com>"
#SMTP domain key is set up for HELO checking
SMTP_DOMAIN=chatwoot.com
# the default value is set "mailhog" and is used by docker-compose for development environments,
@ -93,7 +95,6 @@ AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
# Log settings
# Disable if you want to write logs to a file
RAILS_LOG_TO_STDOUT=true
@ -130,7 +131,6 @@ ANDROID_BUNDLE_ID=com.chatwoot.app
# 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
### Smart App Banner
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
# You can find your app-id in https://itunesconnect.apple.com
@ -147,8 +147,12 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Bot Customizations
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=
## Sentry
# SENTRY_DSN=
@ -169,7 +173,6 @@ USE_INBOX_AVATAR_FOR_BOT=true
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
# DD_TRACE_AGENT_URL=
## IP look up configuration
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
## works only on accounts with ip look up feature enabled
@ -181,7 +184,6 @@ USE_INBOX_AVATAR_FOR_BOT=true
## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=true
## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints
# CW_API_ONLY_SERVER=false
@ -195,3 +197,11 @@ USE_INBOX_AVATAR_FOR_BOT=true
# If you want to use official mobile app,
# the notifications would be relayed via a Chatwoot server
ENABLE_PUSH_RELAY_SERVER=true
# Stripe API key
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# Set to true if you want to upload files to cloud storage using the signed url
# Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true.
DIRECT_UPLOADS_ENABLED=

View file

@ -1,5 +1,10 @@
module.exports = {
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
extends: [
'airbnb-base/legacy',
'prettier',
'plugin:vue/recommended',
'plugin:storybook/recommended',
],
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2020,
@ -19,18 +24,32 @@ module.exports = {
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'import/no-unresolved': 'off',
'vue/max-attributes-per-line': ['error', {
'singleline': 20,
'multiline': {
'max': 1,
'allowFirstLine': false
'vue/max-attributes-per-line': [
'error',
{
singleline: 20,
multiline: {
max: 1,
allowFirstLine: false,
},
},
}],
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off',
],
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'always',
component: 'always',
},
svg: 'always',
math: 'always',
},
],
'vue/no-v-html': 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off'],
'no-console': 'error'
'no-console': 'error',
},
settings: {
'import/resolver': {
@ -41,12 +60,10 @@ module.exports = {
},
env: {
browser: true,
node: true,
jest: true,
jasmine: true
node: true,
},
globals: {
__WEBPACK_ENV__: true,
bus: true,
},
};

36
.github/workflows/lock.yml vendored Normal file
View file

@ -0,0 +1,36 @@
# 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.

58
.github/workflows/nightly_installer.yml vendored Normal file
View file

@ -0,0 +1,58 @@
# #
# #
# # 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
- name: create input file
run: |
echo "no" > input
echo "yes" >> input
- name: Run the installer
run: |
sudo ./install.sh --install < input
# temp fix for postgresql not starting
# automatically in gh action env
- name: start postgresql service
if: always()
run: |
sudo service postgresql start
#re-running the installer again
- name: Run the installer again
if: always()
run: |
sudo ./install.sh --install < input
# disabling http verify for now as http
# access to port 3000 fails in gh action env
# - name: Verify
# if: always()
# run: |
# sudo netstat -ntlp | grep 3000
# sudo systemctl restart chatwoot.target
# curl http://localhost:3000/api

View file

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

View file

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

23
Gemfile
View file

@ -4,7 +4,7 @@ ruby '3.0.4'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
gem 'rails'
gem 'rails', '~>6.1'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@ -78,7 +78,7 @@ gem 'wisper', '2.0.0'
# TODO: bump up gem to 2.0
gem 'facebook-messenger'
gem 'line-bot-api'
gem 'twilio-ruby', '~> 5.32.0'
gem 'twilio-ruby', '~> 5.66'
# twitty will handle subscription of twitter account events
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty'
@ -89,12 +89,9 @@ gem 'slack-ruby-client'
# for dialogflow integrations
gem 'google-cloud-dialogflow'
##--- gems for debugging and error reporting ---##
# static analysis
gem 'brakeman'
##-- apm and error monitoring ---#
gem 'ddtrace'
gem 'elastic-apm'
gem 'newrelic_rpm'
gem 'scout_apm'
gem 'sentry-rails', '~> 5.3'
@ -128,6 +125,16 @@ gem 'html2text'
# to calculate working hours
gem 'working_hours'
# full text search for articles
gem 'pg_search'
# Subscriptions, Billing
gem 'stripe'
## - helper gems --##
## to populate db with sample data
gem 'faker'
group :production, :staging do
# we dont want request timing out in development while using byebug
gem 'rack-timeout'
@ -157,11 +164,13 @@ end
group :development, :test do
gem 'active_record_query_trace'
##--- gems for debugging and error reporting ---##
# static analysis
gem 'brakeman'
gem 'bundle-audit', require: false
gem 'byebug', platform: :mri
gem 'climate_control'
gem 'factory_bot_rails'
gem 'faker'
gem 'listen'
gem 'mock_redis'
gem 'pry-rails'

View file

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

View file

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

View file

@ -1,30 +1,55 @@
# 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.
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.
## 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).
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.
This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
If you have any questions about the process, contact security@chatwoot.com.
If you have any questions about the process, feel free to reach out to security@chatwoot.com.
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.
> 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 | |
## Out of scope
## Vulnerabilities we care about 🫣
> 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
Please do not perform testing against Chatwoot production services. Use a self hosted instance to perform tests.
You can learn more about our triaging process [here](https://www.chatwoot.com/docs/contributing-guide/security-reports).
We consider the following to be out of scope, though there may be exceptions.
## Non-Qualifying Vulnerabilities
We consider the following out of scope, though there may be exceptions.
- Missing HTTP security headers
- Self XSS
- HTTP Host Header XSS without working proof-of-concept
- Incomplete/Missing SPF/DKIM
- Reports from automated tools or scanners
- 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
- Denial of Service attacks
- Brute force attacks
- DNSSEC
- Social Engineering attacks
If you are not sure about the scope, please create a report.
If you are unsure about the scope, please create a [report](https://huntr.dev/repos/chatwoot/chatwoot/).
## Thanks

1
VERSION_CW Normal file
View file

@ -0,0 +1 @@
2.2.0

1
VERSION_CWCTL Normal file
View file

@ -0,0 +1 @@
2.1.0

View file

@ -48,6 +48,7 @@
"size": "FREE"
}
},
"stack": "heroku-20",
"image": "heroku/ruby",
"addons": [ "heroku-redis", "heroku-postgresql"],
"buildpacks": [

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ class ContactBuilder
end
def update_contact_avatar(contact)
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
::Avatar::AvatarFromUrlJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
end
def create_contact

View file

@ -27,7 +27,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
ensure_contact_avatar
rescue Koala::Facebook::AuthenticationError
Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}"
@inbox.channel.authorization_error!
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
true
@ -58,7 +58,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
return if contact_params[:remote_avatar_url].blank?
return if @contact.avatar.attached?
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url])
Avatar::AvatarFromUrlJob.perform_later(@contact, contact_params[:remote_avatar_url])
end
def conversation

View file

@ -73,6 +73,10 @@ class Messages::MessageBuilder
@params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {}
end
def template_params
@params[:template_params].present? ? { additional_attributes: { template_params: JSON.parse(@params[:template_params].to_json) } } : {}
end
def message_sender
return if @params[:sender_type] != 'AgentBot'
@ -91,6 +95,6 @@ class Messages::MessageBuilder
items: @items,
in_reply_to: @in_reply_to,
echo_id: @params[:echo_id]
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id)
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
end
end

View file

@ -54,6 +54,9 @@ class Messages::Messenger::MessageBuilder
def fetch_story_link(attachment)
message = attachment.message
result = get_story_object_from_source_id(message.source_id)
return if result.blank?
story_id = result['story']['mention']['id']
story_sender = result['from']['username']
message.content_attributes[:story_sender] = story_sender
@ -68,6 +71,11 @@ class Messages::Messenger::MessageBuilder
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue Koala::Facebook::ClientError => e
# The exception occurs when we are trying fetch the deleted story or blocked story.
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
Rails.logger.error e
{}
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
{}

View file

@ -15,6 +15,9 @@ class NotificationBuilder
def user_subscribed_to_notification?
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("push_#{notification_type}?")

View file

@ -25,7 +25,7 @@ class NotificationSubscriptionBuilder
end
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
def update_identifier_subscription

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
def create
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
@automation_rule.actions = params[:actions]
@automation_rule.conditions = params[:conditions]
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
@ -31,9 +32,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
def update
ActiveRecord::Base.transaction do
@automation_rule.update!(automation_rules_permit)
@automation_rule.actions = params[:actions] if params[:actions]
@automation_rule.save!
automation_rule_update
process_attachments
rescue StandardError => e
@ -50,7 +49,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
def clone
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
new_rule = automation_rule.dup
new_rule.save
new_rule.save!
@automation_rule = new_rule
end
@ -67,10 +66,17 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
private
def automation_rule_update
@automation_rule.update!(automation_rules_permit)
@automation_rule.actions = params[:actions] if params[:actions]
@automation_rule.conditions = params[:conditions] if params[:conditions]
@automation_rule.save!
end
def automation_rules_permit
params.permit(
:name, :description, :event_name, :account_id, :active,
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
conditions: [:attribute_key, :filter_operator, :query_operator, :custom_attribute_type, { values: [] }],
actions: [:action_name, { action_params: [] }]
)
end

View file

@ -90,9 +90,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
end
def set_avatar(facebook_inbox, page_id)
avatar_file = Down.download(
"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)
avatar_url = "https://graph.facebook.com/#{page_id}/picture?type=large"
Avatar::AvatarFromUrlJob.perform_later(facebook_inbox, avatar_url)
end
end

View file

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

View file

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

View file

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

View file

@ -42,15 +42,19 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def update
@inbox.update(permitted_params.except(:channel))
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
@inbox.update!(permitted_params.except(:channel))
update_inbox_working_hours
channel_attributes = get_channel_attributes(@inbox.channel_type)
# Inbox update doesn't necessarily need channel attributes
return if permitted_params(channel_attributes)[:channel].blank?
if @inbox.inbox_type == 'Email'
validate_email_channel(channel_attributes)
begin
validate_email_channel(channel_attributes)
rescue StandardError => e
render json: { message: e }, status: :unprocessable_entity and return
end
@inbox.channel.reauthorized!
end
@ -58,6 +62,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
update_channel_feature_flags
end
def update_inbox_working_hours
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
end
def agent_bot
@agent_bot = @inbox.agent_bot
end
@ -89,12 +97,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
end
def inbox_name(channel)
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
permitted_params[:name]
end
def create_channel
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
@ -109,10 +111,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@inbox.channel.save!
end
def inbox_attributes
[: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]
end
def permitted_params(channel_attributes = [])
params.permit(
: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,
*inbox_attributes,
channel: [:type, *channel_attributes]
)
end
@ -129,18 +135,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
}[permitted_params[:channel][:type]]
end
def account_channels_method
{
'web_widget' => Current.account.web_widgets,
'api' => Current.account.api_channels,
'email' => Current.account.email_channels,
'line' => Current.account.line_channels,
'telegram' => Current.account.telegram_channels,
'whatsapp' => Current.account.whatsapp_channels,
'sms' => Current.account.sms_channels
}[permitted_params[:channel][:type]]
end
def get_channel_attributes(channel_type)
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
channel_type.constantize::EDITABLE_ATTRS.presence
@ -148,10 +142,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
[]
end
end
def validate_limit
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
end
end
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')

View file

@ -0,0 +1,57 @@
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
def index
@macros = Macro.with_visibility(current_user, params)
end
def create
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
@macro.set_visibility(current_user, permitted_params)
@macro.actions = params[:actions]
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
@macro.save!
end
def show; end
def destroy
@macro.destroy!
head :ok
end
def update
ActiveRecord::Base.transaction do
@macro.update!(macros_with_user)
@macro.set_visibility(current_user, permitted_params)
@macro.save!
rescue StandardError => e
Rails.logger.error e
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
end
end
def execute
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
head :ok
end
def permitted_params
params.permit(
:name, :account_id, :visibility,
actions: [:action_name, { action_params: [] }]
)
end
def macros_with_user
permitted_params.merge(updated_by_id: current_user.id)
end
def fetch_macro
@macro = Current.account.macros.find_by(id: params[:id])
end
end

View file

@ -1,18 +1,35 @@
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
include ::FileTypeHelper
before_action :fetch_portal, except: [:index, :create]
before_action :check_authorization
before_action :set_current_page, only: [:index]
def index
@portals = Current.account.portals
end
def add_members
agents = Current.account.agents.where(id: portal_member_params[:member_ids])
@portal.members << agents
end
def show; end
def create
@portal = Current.account.portals.create!(portal_params)
@portal = Current.account.portals.build(portal_params)
@portal.save!
process_attached_logo
end
def update
@portal.update!(portal_params)
ActiveRecord::Base.transaction do
@portal.update!(portal_params) if params[:portal].present?
process_attached_logo
rescue StandardError => e
Rails.logger.error e
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
end
end
def destroy
@ -20,6 +37,15 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok
end
def archive
@portal.update(archive: true)
head :ok
end
def process_attached_logo
@portal.logo.attach(params[:logo])
end
private
def fetch_portal
@ -32,7 +58,16 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params
params.require(:portal).permit(
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived, { config: [:default_locale,
{ allowed_locales: [] }] }
)
end
def portal_member_params
params.require(:portal).permit(:account_id, member_ids: [])
end
def set_current_page
@current_page = params[:page] || 1
end
end

View file

@ -1,6 +1,7 @@
class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseController
before_action :fetch_team
before_action :check_authorization
before_action :validate_member_id_params, only: [:create, :update, :destroy]
def index
@team_members = @team.team_members.map(&:user)
@ -45,4 +46,10 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
def fetch_team
@team = Current.account.teams.find(params[:team_id])
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

View file

@ -19,6 +19,7 @@ class Api::V1::AccountsController < Api::BaseController
user_full_name: account_params[:user_full_name],
email: account_params[:email],
user_password: account_params[:password],
locale: account_params[:locale],
user: current_user
).perform
if @user

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
class ApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
include RequestExceptionHandler
include Pundit
include Pundit::Authorization
include SwitchLocale
skip_before_action :verify_authenticity_token
@ -17,10 +17,6 @@ class ApplicationController < ActionController::Base
Current.user = @user
end
def current_subscription
@subscription ||= Current.account.subscription
end
def pundit_user
{
user: Current.user,

View file

@ -8,6 +8,8 @@ module EnsureCurrentAccountHelper
def ensure_current_account
account = Account.find(params[:account_id])
ensure_account_is_active?(account)
if current_user
account_accessible_for_user?(account)
elsif @resource.is_a?(AgentBot)
@ -25,4 +27,8 @@ module EnsureCurrentAccountHelper
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)
end
def ensure_account_is_active?(account)
render_unauthorized('Account is suspended') unless account.active?
end
end

View file

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

View file

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

View file

@ -4,6 +4,7 @@ class DashboardController < ActionController::Base
before_action :set_global_config
around_action :switch_locale
before_action :ensure_installation_onboarding, only: [:index]
before_action :redirect_to_custom_domain_page
layout 'vueapp'
@ -13,8 +14,7 @@ class DashboardController < ActionController::Base
def set_global_config
@global_config = GlobalConfig.get(
'LOGO',
'LOGO_THUMBNAIL',
'LOGO', 'LOGO_THUMBNAIL',
'INSTALLATION_NAME',
'WIDGET_BRAND_URL',
'TERMS_URL',
@ -29,7 +29,8 @@ class DashboardController < ActionController::Base
'DIRECT_UPLOADS_ENABLED',
'HCAPTCHA_SITE_KEY',
'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE'
'DISABLE_USER_PROFILE_UPDATE',
'DEPLOYMENT_ENV'
).merge(app_config)
end
@ -37,13 +38,23 @@ class DashboardController < ActionController::Base
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
end
def redirect_to_custom_domain_page
custom_domain = request.host
portal = Portal.find_by(custom_domain: custom_domain)
return unless portal
redirect_to "/hc/#{portal.slug}"
end
def app_config
{
APP_VERSION: Chatwoot.config[:version],
VAPID_PUBLIC_KEY: VapidService.public_key,
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: 'v13.0'
FACEBOOK_API_VERSION: 'v14.0',
IS_ENTERPRISE: ChatwootApp.enterprise?
}
end
end

View file

@ -23,7 +23,7 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
def authenticate_resource_with_sso_token
@token = @resource.create_token
@resource.save
@resource.save!
sign_in(:user, @resource, store: false, bypass: false)
# invalidate the token after the user is signed in

View file

@ -27,6 +27,6 @@ class Platform::Api::V1::AccountsController < PlatformController
end
def account_params
params.permit(:name)
params.permit(:name, :locale)
end
end

View file

@ -14,7 +14,7 @@ class Platform::Api::V1::UsersController < PlatformController
def login
encoded_email = ERB::Util.url_encode(@resource.email)
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
render json: { url: "#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
end
def show; end

View file

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

View file

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

View file

@ -0,0 +1,13 @@
class Public::Api::V1::PortalsController < PublicController
before_action :ensure_custom_domain_request, only: [:show]
before_action :set_portal
layout 'portal'
def show; end
private
def set_portal
@portal = @portals.find_by!(slug: params[:slug], archived: false)
end
end

View file

@ -3,4 +3,19 @@
class PublicController < ActionController::Base
include RequestExceptionHandler
skip_before_action :verify_authenticity_token
private
def ensure_custom_domain_request
custom_domain = request.host
@portals = ::Portal.where(custom_domain: custom_domain)
return if @portals.present?
render json: {
error: "Domain: #{custom_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

View file

@ -41,4 +41,9 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
def seed
Internal::SeedAccountJob.perform_later(requested_resource)
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
end
end

View file

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

View file

@ -44,12 +44,12 @@ class Twitter::CallbacksController < Twitter::BaseController
end
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_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id']
)
account.inboxes.create(
account.inboxes.create!(
name: parsed_body['screen_name'],
channel: twitter_profile
)

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ class AccountDashboard < Administrate::BaseDashboard
users: CountField,
conversations: CountField,
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
}.merge(enterprise_attribute_types).freeze
@ -31,6 +32,7 @@ class AccountDashboard < Administrate::BaseDashboard
locale
users
conversations
status
].freeze
# SHOW_PAGE_ATTRIBUTES
@ -42,6 +44,7 @@ class AccountDashboard < Administrate::BaseDashboard
created_at
updated_at
locale
status
conversations
account_users
] + enterprise_show_page_attributes).freeze
@ -53,6 +56,7 @@ class AccountDashboard < Administrate::BaseDashboard
FORM_ATTRIBUTES = (%i[
name
locale
status
] + enterprise_form_attributes).freeze
# COLLECTION_FILTERS

View file

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

View file

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

View file

@ -2,6 +2,11 @@ class ConversationFinder
attr_reader :current_user, :current_account, :params
DEFAULT_STATUS = 'open'.freeze
SORT_OPTIONS = {
latest: 'latest',
sort_on_created_at: 'sort_on_created_at',
last_user_message_at: 'last_user_message_at'
}.with_indifferent_access
# assumptions
# inbox_id if not given, take from all conversations, else specific to inbox
@ -133,10 +138,7 @@ class ConversationFinder
@conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
)
if params[:conversation_type] == 'mention'
@conversations.page(current_page)
else
@conversations.latest.page(current_page)
end
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
@conversations.send(sort_by).page(current_page)
end
end

View file

@ -1,4 +1,10 @@
module Api::V1::InboxesHelper
def inbox_name(channel)
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
permitted_params[:name]
end
def validate_email_channel(attributes)
channel_data = permitted_params(attributes)[:channel]
@ -19,8 +25,7 @@ module Api::V1::InboxesHelper
enable_ssl: channel_data[:imap_enable_ssl] }
end
Mail.connection do # rubocop:disable:block
end
check_imap_connection(channel_data)
end
def validate_smtp(channel_data)
@ -32,10 +37,29 @@ module Api::V1::InboxesHelper
check_smtp_connection(channel_data, smtp)
end
def check_imap_connection(channel_data)
Mail.connection {} # rubocop:disable:block
rescue SocketError => e
raise StandardError, I18n.t('errors.inboxes.imap.socket_error')
rescue Net::IMAP::NoResponseError => e
raise StandardError, I18n.t('errors.inboxes.imap.no_response_error')
rescue Errno::EHOSTUNREACH => e
raise StandardError, I18n.t('errors.inboxes.imap.host_unreachable_error')
rescue Net::OpenTimeout => e
raise StandardError,
I18n.t('errors.inboxes.imap.connection_timed_out_error', address: channel_data[:imap_address], port: channel_data[:imap_port])
rescue Net::IMAP::Error => e
raise StandardError, I18n.t('errors.inboxes.imap.connection_closed_error')
rescue StandardError => e
raise StandardError, e.message
ensure
ChatwootExceptionTracker.new(e).capture_exception if e.present?
end
def check_smtp_connection(channel_data, smtp)
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
channel_data[:smtp_authentication]&.to_sym || :login)
smtp.finish unless smtp&.nil?
smtp.finish
end
def set_smtp_encryption(channel_data, smtp)
@ -74,4 +98,22 @@ module Api::V1::InboxesHelper
context.verify_mode = openssl_verify_mode
context
end
def account_channels_method
{
'web_widget' => Current.account.web_widgets,
'api' => Current.account.api_channels,
'email' => Current.account.email_channels,
'line' => Current.account.line_channels,
'telegram' => Current.account.telegram_channels,
'whatsapp' => Current.account.whatsapp_channels,
'sms' => Current.account.sms_channels
}[permitted_params[:channel][:type]]
end
def validate_limit
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
end
end

View file

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

View file

@ -2,7 +2,7 @@
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<transition name="fade" mode="out-in">
<router-view></router-view>
<router-view />
</transition>
<add-account-modal
:show="showAddAccountModal"

View file

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

View file

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

View file

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

View file

@ -0,0 +1,31 @@
import accountAPI from '../account';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../../specs/apiSpecHelper';
describe('#enterpriseAccountAPI', () => {
it('creates correct instance', () => {
expect(accountAPI).toBeInstanceOf(ApiClient);
expect(accountAPI).toHaveProperty('get');
expect(accountAPI).toHaveProperty('show');
expect(accountAPI).toHaveProperty('create');
expect(accountAPI).toHaveProperty('update');
expect(accountAPI).toHaveProperty('delete');
expect(accountAPI).toHaveProperty('checkout');
});
describeWithAPIMock('API calls', context => {
it('#checkout', () => {
accountAPI.checkout();
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/checkout'
);
});
it('#subscription', () => {
accountAPI.subscription();
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/subscription'
);
});
});
});

View file

@ -0,0 +1,51 @@
/* 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();

View file

@ -0,0 +1,30 @@
/* global axios */
import PortalsAPI from './portals';
class CategoriesAPI extends PortalsAPI {
constructor() {
super('categories', { accountScoped: true });
}
get({ portalSlug }) {
return axios.get(`${this.url}/${portalSlug}/categories`);
}
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();

View file

@ -0,0 +1,18 @@
/* global axios */
import ApiClient from '../ApiClient';
class PortalsAPI extends ApiClient {
constructor() {
super('portals', { accountScoped: true });
}
updatePortal({ portalSlug, portalObj }) {
return axios.patch(`${this.url}/${portalSlug}`, portalObj);
}
deletePortal(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}`);
}
}
export default PortalsAPI;

View file

@ -10,6 +10,7 @@ export const buildCreatePayload = ({
files,
ccEmails = '',
bccEmails = '',
templateParams,
}) => {
let payload;
if (files && files.length !== 0) {
@ -32,6 +33,7 @@ export const buildCreatePayload = ({
content_attributes: contentAttributes,
cc_emails: ccEmails,
bcc_emails: bccEmails,
template_params: templateParams,
};
}
return payload;
@ -51,6 +53,7 @@ class MessageApi extends ApiClient {
files,
ccEmails = '',
bccEmails = '',
templateParams,
}) {
return axios({
method: 'post',
@ -63,6 +66,7 @@ class MessageApi extends ApiClient {
files,
ccEmails,
bccEmails,
templateParams,
}),
});
}

View file

@ -0,0 +1,66 @@
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'
);
});
});
});

View file

@ -12,6 +12,7 @@ describe('#ContactsAPI', () => {
expect(contactAPI).toHaveProperty('delete');
expect(contactAPI).toHaveProperty('getConversations');
expect(contactAPI).toHaveProperty('filter');
expect(contactAPI).toHaveProperty('destroyAvatar');
});
describeWithAPIMock('API calls', context => {
@ -100,6 +101,13 @@ describe('#ContactsAPI', () => {
queryPayload
);
});
it('#destroyAvatar', () => {
contactAPI.destroyAvatar(1);
expect(context.axiosMock.delete).toHaveBeenCalledWith(
'/api/v1/contacts/1/avatar'
);
});
});
});

View file

@ -0,0 +1,12 @@
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');
});
});

View file

@ -0,0 +1,13 @@
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');
});
});

View file

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -1,11 +1,9 @@
/* Enter and leave animations can use different */
/* durations and timing functions. */
.slide-fade-enter-active {
transition: all .3s $ease-in-cubic;
transition: all 0.3s var(--ease-in-cubic);
}
.slide-fade-leave-active {
transition: all .3s $ease-out-cubic;
transition: all 0.3s var(--ease-out-cubic);
}
.slide-fade-enter,
@ -24,7 +22,7 @@
.conversations-list-enter-active,
.conversations-list-leave-active {
transition: all .25s $ease-out-cubic;
transition: all 0.25s var(--ease-out-cubic);
}
.conversations-list-enter,
@ -35,11 +33,10 @@
.menu-list-enter-active,
.menu-list-leave-active {
transition: opacity .3s $ease-out-cubic,
transform .2s $ease-out-cubic;
transition: opacity 0.3s var(--ease-out-cubic),
transform 0.2s var(--ease-out-cubic);
}
.menu-list-leave-to {
opacity: 0;
position: absolute;
@ -52,23 +49,24 @@
}
.slide-up-enter-active {
transition: all .3s $ease-in-cubic;
transition: all 0.3s var(--ease-in-cubic);
}
.slide-up-leave-active {
transition: all .3s $ease-out-cubic;
transition: all 0.3s var(--ease-out-cubic);
}
.slide-up-enter,
.slide-up-leave-to {
transform: translateY(-$space-medium);
opacity: 0;
transform: translateY(-$space-medium);
}
.menu-slide-enter-active,
.menu-slide-leave-active {
transform: translateY(0);
transition: transform 0.25s $ease-in-cubic, opacity 0.15s $ease-in-cubic;
transition: transform 0.25s var(--ease-in-cubic),
opacity 0.15s var(--ease-in-cubic);
}
.menu-slide-enter,
@ -77,13 +75,12 @@
transform: translateY($space-small);
}
.toast-fade-enter-active {
transition: all .3s $ease-in-sine;
transition: all 0.3s var(--ease-in-sine);
}
.toast-fade-leave-active {
transition: all .1s $ease-out-sine;
transition: all 0.1s var(--ease-out-sine);
}
.toast-fade-enter,
@ -93,11 +90,11 @@
}
.modal-fade-enter-active {
transition: all .3s $ease-in-sine;
transition: all 0.3s var(--ease-in-sine);
}
.modal-fade-leave-active {
transition: all .1s $ease-out-sine;
transition: all 0.1s var(--ease-out-sine);
}
.modal-fade-enter,
@ -106,15 +103,15 @@
}
.network-notification-fade-enter-active {
transition: all .1s $ease-in-sine;
transition: all 0.1s var(--ease-in-sine);
}
.network-notification-fade-leave-active {
transition: all .1s $ease-out-sine;
transition: all 0.1s var(--ease-out-sine);
}
.network-notification-fade-enter,
.network-notification-fade-leave-to {
transform: translateY(-$space-small);
opacity: 0;
transform: translateY(-$space-small);
}

View file

@ -370,7 +370,7 @@ $input-font-weight: $global-weight-normal;
$input-background: $white;
$input-background-focus: $white;
$input-background-disabled: $light-gray;
$input-border: 1px solid $color-border;
$input-border: 1px solid var(--s-200);
$input-border-focus: 1px solid lighten($primary-color, 15%);
$input-shadow: 0;
$input-shadow-focus: 0;
@ -383,8 +383,8 @@ $form-button-radius: var(--border-radius-normal);
// 20. Label
// ---------
$label-background: $primary-color;
$label-color: $white;
$label-background: $white;
$label-color: $black;
$label-color-alt: $black;
$label-palette: $foundation-palette;
$label-font-size: $font-size-mini;

View file

@ -41,24 +41,22 @@ is-closed .app-root {
.view-box {
@include full-height;
@include margin(0);
@include space-between-column;
height: 100vh;
margin: 0;
}
.view-panel {
@include margin($zero);
@include padding($space-normal);
flex-direction: column;
margin: 0;
overflow-y: auto;
padding: $space-normal;
}
.content-box {
@include padding($space-normal);
overflow: auto;
padding: $space-normal;
}
.back-button {
@ -91,8 +89,7 @@ is-closed .app-root {
justify-content: center;
img {
@include padding($space-one);
max-width: $space-mega;
padding: $space-one;
}
}

View file

@ -4,6 +4,7 @@
.page-sub-title {
font-size: $font-size-large;
word-wrap: break-word;
}
.block-title {

View file

@ -42,7 +42,6 @@
overflow: hidden;
}
.border-right {
border-right: 1px solid var(--color-border);
}
@ -60,3 +59,19 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.flex-between {
align-items: center;
display: flex;
justify-content: space-between;
}
.flex-end {
display: flex;
justify-content: end;
}
.flex-align-center {
align-items: center;
display: flex;
}

View file

@ -99,3 +99,7 @@ $ionicons-font-path: '~ionicons/fonts';
// Transitions
$transition-ease-in: all 0.250s ease-in;
:root {
--dashboard-app-tabs-height: 3.9rem;
}

View file

@ -1,4 +1,5 @@
@import 'shared/assets/fonts/inter';
@import 'shared/assets/stylesheets/animations';
@import 'shared/assets/stylesheets/colors';
@import 'shared/assets/stylesheets/spacing';
@import 'shared/assets/stylesheets/font-size';
@ -16,7 +17,6 @@
@import 'date-picker';
@import 'foundation-sites/scss/foundation';
@import '~bourbon/core/bourbon';
@include foundation-everything($flex: true);

View file

@ -16,7 +16,7 @@
margin-bottom: var(--space-normal);
&.multiselect--disabled {
opacity: .8;
opacity: 0.8;
}
.multiselect--active {
@ -96,9 +96,9 @@
}
.multiselect__tags {
@include margin(0);
border: 1px solid $color-border;
border-color: $color-border;
border: 1px solid var(--s-200);
border-color: var(--s-200);
margin: 0;
min-height: 4.4rem;
padding-top: $zero;
}
@ -130,10 +130,10 @@
.multiselect__input {
@include ghost-input;
@include padding($zero);
font-size: $font-size-small;
height: 4.4rem;
margin-bottom: $zero;
padding: 0;
}
.multiselect__single {

View file

@ -1,4 +1,5 @@
@import 'shared/assets/fonts/inter';
@import 'shared/assets/stylesheets/animations';
@import 'shared/assets/stylesheets/colors';
@import 'shared/assets/stylesheets/spacing';
@import 'shared/assets/stylesheets/font-size';
@ -17,8 +18,6 @@
@import 'foundation-sites/scss/foundation';
@include foundation-prototype-spacing;
@import '~bourbon/core/bourbon';
@include foundation-everything($flex: true);
@import 'typography';

View file

@ -1,3 +1,5 @@
$channel-hover-color: rgba(0, 0, 0, 0.1);
.channels {
margin-top: $space-medium;
@ -7,14 +9,14 @@
.channel {
@include flex;
@include padding($space-normal $zero);
@include background-white;
@include border-light;
cursor: pointer;
flex-direction: column;
margin: -1px;
transition: all 0.200s ease-in;
padding: $space-normal $zero;
transition: all 0.2s ease-in;
&:last-child {
@include border-light;
@ -22,16 +24,16 @@
&:hover {
border: 1px solid $primary-color;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px $channel-hover-color;
z-index: 999;
}
&.disabled {
opacity: .6;
opacity: 0.6;
}
img {
@include margin($space-normal auto);
margin: $space-normal auto;
width: 50%;
}
@ -43,8 +45,8 @@
}
p {
width: 100%;
color: $medium-gray;
width: 100%;
}
}
}

View file

@ -4,33 +4,33 @@
// Conversation header - Light BG
.settings-header {
@include padding($space-small $space-normal);
@include background-white;
@include flex;
@include flex-align($x: justify, $y: middle);
border-bottom: 1px solid var(--s-50);
height: $header-height;
min-height: $header-height;
padding: $space-small $space-normal;
// Resolve Button
.button {
@include margin(0);
margin: 0;
}
// User thumbnail and text
.page-title {
@include flex;
@include flex-align($x: center, $y: middle);
@include margin($zero);
margin: 0;
}
}
.wizard-box {
.item {
@include padding($space-normal $space-normal $space-normal $space-medium);
@include background-light;
cursor: pointer;
padding: $space-normal $space-normal $space-normal $space-medium;
position: relative;
&::before,
@ -91,10 +91,11 @@
font-size: $font-size-default;
line-height: 1;
padding-left: $space-medium;
}
.completed {
color: $success-color;
}
.completed {
color: $success-color;
margin-left: $space-smaller;
}
p {
@ -128,89 +129,27 @@
.wizard-body {
@include background-white;
@include padding($space-medium);
@include border-light;
@include full-height();
padding: $space-medium;
&.height-auto {
height: auto;
}
}
.inoboxes-list {
.inbox-item {
@include margin($space-normal);
@include flex;
@include flex-shrink;
@include padding($space-normal $space-normal);
@include border-light-bottom();
background: $color-white;
cursor: pointer;
flex-direction: column;
float: left;
min-height: 10rem;
width: 20%;
&:last-child {
@include border-nil;
margin-bottom: $zero;
}
&:hover {
@include background-gray;
.arrow {
opacity: 1;
transform: translateX($space-small);
}
}
.switch {
align-self: center;
margin-bottom: $zero;
margin-right: $space-normal;
}
.item--details {
@include padding($space-normal $zero);
.item--name {
font-size: $font-size-large;
line-height: 1;
}
.item--sub {
font-size: $font-size-small;
margin-bottom: 0;
}
}
.arrow {
align-self: center;
color: $medium-gray;
font-size: $font-size-small;
opacity: 0.7;
transform: translateX(0);
transition: opacity 0.1s ease-in 0s, transform 0.2s ease-in 0.03s;
}
}
}
.settings--content {
@include margin($space-small $space-large);
margin: $space-small $space-large;
.title {
font-weight: $font-weight-medium;
}
.code {
@include padding($space-one);
background: $color-background;
max-height: $space-mega;
overflow: auto;
padding: $space-one;
white-space: nowrap;
code {
@ -225,7 +164,7 @@
text-align: center;
p {
@include padding($space-medium);
padding: $space-medium;
}
> a > img {

View file

@ -44,6 +44,52 @@ $default-button-height: 4.0rem;
padding-top: 0;
}
&.hollow {
border-color: var(--s-200);
color: var(--w-700);
&.secondary {
border-color: var(--s-200);
color: var(--s-700)
}
&.success {
border-color: var(--s-200);
color: var(--g-700)
}
&.alert {
border-color: var(--s-200);
color: var(--r-700)
}
&.warning {
border-color: var(--s-200);
color: var(--y-700)
}
&:hover {
background: var(--s-75);
border-color: var(--s-100);
&.secondary {
border-color: var(--s-100);
}
&.success {
border-color: var(--s-100);
}
&.alert {
border-color: var(--s-100);
}
&.warning {
border-color: var(--s-100);
}
}
}
// Smooth style
&.smooth {
@include button-style(var(--w-50), var(--w-100), var(--w-700));
@ -67,11 +113,12 @@ $default-button-height: 4.0rem;
}
&.clear {
&.warning {
color: var(--y-600);
}
&.button--only-icon:hover {
&:hover {
background: var(--w-50);
&.secondary {

View file

@ -1,5 +1,4 @@
@keyframes left-shift-animation {
0%,
100% {
transform: translateX(0);
@ -13,15 +12,15 @@
.conversation {
@include flex;
@include flex-shrink;
@include padding(0 0 0 $space-normal);
border-bottom: 1px solid transparent;
border-left: $space-micro solid transparent;
border-top: 1px solid transparent;
cursor: pointer;
padding: 0 0 0 $space-normal;
position: relative;
&.active {
animation: left-shift-animation .25s $swift-ease-out-function;
animation: left-shift-animation 0.25s $swift-ease-out-function;
background: $color-background;
border-bottom-color: $color-border-light;
border-left-color: $color-woot;
@ -31,7 +30,7 @@
border-top-color: transparent;
}
+.conversation .conversation--details {
+ .conversation .conversation--details {
border-top-color: transparent;
}
}
@ -48,13 +47,12 @@
}
}
.conversation--details {
@include margin(0 0 0 $space-one);
@include border-light-bottom;
@include border-light-top;
@include padding($space-slab 0);
border-bottom-color: transparent;
margin: 0 0 0 $space-one;
padding: $space-slab 0;
}
.conversation--user {

View file

@ -1,6 +1,8 @@
// scss-lint:disable MergeableSelector
@mixin bubble-with-types {
@include padding($space-small $space-normal);
@include margin($zero);
padding: $space-small $space-normal;
margin: 0;
background: $color-woot;
border-radius: $space-one;
color: var(--white);
@ -37,7 +39,11 @@
}
&::before {
background-image: linear-gradient(-180deg, transparent 3%, $color-heading 130%);
background-image: linear-gradient(
-180deg,
transparent 3%,
$color-heading 130%
);
bottom: 0;
content: '';
height: 20%;
@ -75,16 +81,15 @@
}
.conversations-list {
@include flex-weight(1);
@include scroll-on-hover;
flex: 1 1;
}
.chat-list__top {
@include flex;
@include padding($zero $zero $space-micro $zero);
align-items: center;
justify-content: space-between;
padding: $zero $zero $space-micro;
.page-title {
margin-bottom: $zero;
@ -92,13 +97,13 @@
}
.status--filter {
@include padding($zero null $zero $space-normal);
@include margin($zero);
background-color: $color-background-light;
border: 1px solid $color-border;
float: right;
font-size: $font-size-mini;
height: $space-medium;
margin: 0;
padding: $zero $space-medium $zero $space-normal;
width: auto;
}
}
@ -110,19 +115,19 @@
.conversation-panel {
@include flex;
@include flex-weight(1 1 1px);
@include margin($zero);
flex: 1 1 1px;
flex-direction: column;
height: 100%;
margin: 0;
overflow-y: auto;
padding-bottom: var(--space-normal);
position: relative;
}
.conversation-panel>li {
.conversation-panel > li {
@include flex;
@include flex-shrink;
@include margin($zero $zero $space-micro);
margin: $zero $zero $space-micro;
position: relative;
&:first-child {
@ -134,11 +139,11 @@
}
&.unread--toast {
+.right {
+ .right {
margin-bottom: var(--space-micro);
}
+.left {
+ .left {
margin-bottom: 0;
}
@ -165,9 +170,7 @@
}
}
&.left {
.bubble {
@include border-normal;
background: $white;
@ -198,10 +201,9 @@
color: $color-primary-dark;
}
}
}
+.right {
+ .right {
margin-top: $space-one;
.bubble {
@ -209,8 +211,8 @@
}
}
+.unread--toast {
+.right {
+ .unread--toast {
+ .right {
margin-top: $space-one;
.bubble {
@ -218,7 +220,7 @@
}
}
+.left {
+ .left {
margin-top: 0;
}
}
@ -264,7 +266,7 @@
}
}
+.left {
+ .left {
margin-top: $space-one;
.bubble {
@ -272,8 +274,8 @@
}
}
+.unread--toast {
+.left {
+ .unread--toast {
+ .left {
margin-top: $space-one;
.bubble {
@ -281,11 +283,10 @@
}
}
+.right {
+ .right {
margin-top: 0;
}
}
}
&.center {
@ -293,10 +294,9 @@
}
.wrap {
@include margin($zero $space-normal);
--bubble-max-width: 49.6rem;
max-width: Min(var(--bubble-max-width), 85%);
margin: $zero $space-normal;
max-width: Min(var(--bubble-max-width), 84%);
.sender--name {
font-size: $font-size-mini;
@ -320,7 +320,8 @@
font-size: var(--font-size-small);
justify-content: center;
margin: var(--space-smaller) 0;
padding: var(--space-smaller) var(--space-micro) var(--space-smaller) var(--space-one);
padding: var(--space-smaller) var(--space-micro) var(--space-smaller)
var(--space-one);
.is-text {
display: inline-flex;
@ -371,7 +372,6 @@
}
.left .bubble .text-content {
h1,
h2,
h3,
@ -400,7 +400,6 @@
}
.right .bubble .text-content {
h1,
h2,
h3,

View file

@ -1,5 +1,22 @@
// scss-lint:disable QualifyingElement
.error {
#{$all-text-inputs},
input[type='color'],
input[type='date'],
input[type='datetime'],
input[type='datetime-local'],
input[type='email'],
input[type='month'],
input[type='number'],
input[type='password'],
input[type='search'],
input[type='tel'],
input[type='text'],
input[type='time'],
input[type='url'],
input[type='week'],
input:not([type]),
textarea,
select,
.multiselect > .multiselect__tags {
@include thin-border(var(--r-400));

View file

@ -30,7 +30,7 @@
}
.page-top-bar {
@include padding($space-large $space-large $zero);
padding: $space-large $space-large $zero;
img {
max-height: 6rem;
@ -53,8 +53,8 @@
}
.content-box {
@include padding($zero);
height: auto;
padding: 0;
}
h2 {
@ -64,29 +64,29 @@
}
p {
@include margin($zero);
@include padding($zero);
font-size: $font-size-small;
margin: 0;
padding: 0;
}
.content {
@include padding($space-large);
padding: $space-large;
}
form,
.modal-content {
@include padding($space-large);
align-self: center;
padding: $space-large;
a {
@include padding($space-normal);
padding: $space-normal;
}
}
.modal-footer {
@include flex;
@include flex-align($x: flex-start, $y: middle);
@include padding($space-small $zero);
@include flex-align($x: flex-end, $y: middle);
padding: $space-small $zero;
button {
font-size: $font-size-small;
@ -98,10 +98,10 @@
}
.delete-item {
@include padding($space-large);
padding: $space-large;
button {
@include margin($zero);
margin: 0;
}
}
}

View file

@ -1,14 +1,12 @@
.reply-box {
transition: box-shadow .35s $swift-ease-out-function,
transition: box-shadow 0.35s $swift-ease-out-function,
height 2s $swift-ease-out-function;
&.is-focused {
box-shadow: var(--shadow);
}
.reply-box__top {
.icon {
color: $medium-gray;
cursor: pointer;
@ -20,7 +18,6 @@
}
}
.attachment {
cursor: pointer;
margin-right: $space-one;
@ -37,13 +34,12 @@
resize: none;
}
>textarea {
> textarea {
@include ghost-input();
@include margin(0);
background: transparent;
// Override min-height : 50px in foundation
//
margin: 0;
max-height: $space-mega * 2.4;
// Override min-height : 50px in foundation
min-height: 4.8rem;
padding: var(--space-normal) 0 0;
resize: none;
@ -56,10 +52,9 @@
.reply-box__top {
background: var(--y-50);
>input {
> input {
background: var(--y-50);
}
}
}
}

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