Merge branch 'develop' into chore/conversation-participants
This commit is contained in:
commit
ac1e698a1e
2331 changed files with 113825 additions and 17341 deletions
|
@ -7,16 +7,19 @@ defaults: &defaults
|
|||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: cimg/ruby:3.0.2-browsers
|
||||
- image: cimg/ruby:3.0.4-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||
- image: circleci/postgres:alpine
|
||||
- image: circleci/redis:alpine
|
||||
- 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
|
||||
|
@ -40,14 +43,13 @@ jobs:
|
|||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
||||
- chatwoot-bundle
|
||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
- run: bundle install --frozen --path ~/.bundle
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.bundle
|
||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
|
||||
# Only necessary if app uses webpacker or yarn in some other way
|
||||
|
@ -89,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
|
||||
|
@ -101,6 +104,10 @@ jobs:
|
|||
- run:
|
||||
name: Rubocop
|
||||
command: bundle exec rubocop
|
||||
|
||||
# - run:
|
||||
# name: Brakeman
|
||||
# command: bundle exec brakeman
|
||||
|
||||
- run:
|
||||
name: eslint
|
||||
|
@ -110,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
|
||||
~/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
|
||||
|
||||
|
|
|
@ -50,3 +50,7 @@ exclude_patterns:
|
|||
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
|
||||
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
|
||||
- 'app/javascript/dashboard/i18n/index.js'
|
||||
- 'app/javascript/widget/i18n/index.js'
|
||||
- 'app/javascript/survey/i18n/index.js'
|
||||
- 'app/javascript/shared/constants/locales.js'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
33
.env.example
33
.env.example
|
@ -3,6 +3,8 @@ SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
|||
|
||||
# Replace with the URL you are planning to use for your app
|
||||
FRONTEND_URL=http://0.0.0.0:3000
|
||||
# To use a dedicated URL for help center pages
|
||||
# HELPCENTER_URL=http://0.0.0.0:3000
|
||||
|
||||
# If the variable is set, all non-authenticated pages would fallback to the default locale.
|
||||
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
|
||||
|
@ -32,7 +34,15 @@ REDIS_SENTINELS=
|
|||
# You can find list of master using "SENTINEL masters" command
|
||||
REDIS_SENTINEL_MASTER_NAME=
|
||||
|
||||
# Redis premium breakage in heroku fix
|
||||
# enable the following configuration
|
||||
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||
# 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=
|
||||
|
@ -43,7 +53,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,
|
||||
|
@ -88,7 +97,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
|
||||
|
@ -125,7 +133,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
|
||||
|
@ -142,8 +149,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=
|
||||
|
||||
|
@ -156,13 +167,14 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||
## NewRelic
|
||||
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
|
||||
# NEW_RELIC_LICENSE_KEY=
|
||||
# Set this to true to allow newrelic apm to send logs.
|
||||
# This is turned off by default.
|
||||
# NEW_RELIC_APPLICATION_LOGGING_ENABLED=
|
||||
|
||||
## Datadog
|
||||
## 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
|
||||
|
@ -174,7 +186,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
|
||||
|
@ -188,3 +199,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=
|
||||
|
|
45
.eslintrc.js
45
.eslintrc.js
|
@ -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']
|
||||
|
||||
'import/extensions': ['off'],
|
||||
'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
36
.github/workflows/lock.yml
vendored
Normal 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.
|
46
.github/workflows/nightly_installer.yml
vendored
Normal file
46
.github/workflows/nightly_installer.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
# #
|
||||
# #
|
||||
# # Linux nightly installer action
|
||||
# # This action will try to install and setup
|
||||
# # chatwoot on an Ubuntu 20.04 machine using
|
||||
# # the linux installer script.
|
||||
# #
|
||||
# # This is set to run daily at midnight.
|
||||
# #
|
||||
|
||||
name: Run Linux nightly installer
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
|
||||
- name: get installer
|
||||
run: |
|
||||
wget https://get.chatwoot.app/linux/install.sh
|
||||
chmod +x install.sh
|
||||
#fix for postgtres not starting automatically in gh action env
|
||||
sed -i '/function configure_db() {/a sudo service postgresql start' install.sh
|
||||
|
||||
- name: create input file
|
||||
run: |
|
||||
echo "no" > input
|
||||
echo "yes" >> input
|
||||
|
||||
- name: Run the installer
|
||||
run: |
|
||||
sudo ./install.sh --install < input
|
||||
|
||||
# disabling http verify for now as http
|
||||
# access to port 3000 fails in gh action env
|
||||
# - name: Verify
|
||||
# if: always()
|
||||
# run: |
|
||||
# sudo netstat -ntlp | grep 3000
|
||||
# sudo systemctl restart chatwoot.target
|
||||
# curl http://localhost:3000/api
|
||||
|
62
.github/workflows/publish_foss_docker.yml
vendored
Normal file
62
.github/workflows/publish_foss_docker.yml
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
# #
|
||||
# # This action will publish Chatwoot CE docker image.
|
||||
# # This is set to run against merges to develop, master
|
||||
# # and when tags are created.
|
||||
# #
|
||||
|
||||
name: Publish Chatwoot CE docker images
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Set Chatwoot edition
|
||||
run: |
|
||||
echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile
|
||||
|
||||
- name: set docker tag
|
||||
run: |
|
||||
echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV
|
||||
|
||||
- name: replace docker tag if master
|
||||
if: github.ref_name == 'master'
|
||||
run: |
|
||||
echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_TAG }}
|
73
.github/workflows/run_foss_spec.yml
vendored
Normal file
73
.github/workflows/run_foss_spec.yml
vendored
Normal file
|
@ -0,0 +1,73 @@
|
|||
# #
|
||||
# # This action will strip the enterprise folder
|
||||
# # and run the spec.
|
||||
# # This is set to run against every PR.
|
||||
# #
|
||||
|
||||
name: Run Chatwoot CE spec
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:10.8
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ""
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
# tmpfs makes DB faster by using RAM
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.0.4 # Not needed with a .ruby-version file
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
|
||||
- name: Seed database
|
||||
run: bundle exec rake db:schema:load
|
||||
|
||||
- name: yarn check-files
|
||||
run: yarn install --check-files
|
||||
|
||||
# Run rails tests
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
|
@ -16,6 +16,8 @@ Metrics/ClassLength:
|
|||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
|
@ -181,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
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.0.2
|
||||
3.0.4
|
||||
|
|
|
@ -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({
|
||||
|
|
43
Gemfile
43
Gemfile
|
@ -1,10 +1,10 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.0.2'
|
||||
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
|
||||
|
||||
|
@ -42,7 +42,7 @@ gem 'down', '~> 5.0'
|
|||
gem 'aws-sdk-s3', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'image_processing'
|
||||
gem 'image_processing', '~> 1.12.2'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
|
@ -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,22 +89,19 @@ 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'
|
||||
gem 'sentry-ruby'
|
||||
gem 'sentry-sidekiq'
|
||||
gem 'sentry-rails', '~> 5.3'
|
||||
gem 'sentry-ruby', '~> 5.3'
|
||||
gem 'sentry-sidekiq', '~> 5.3'
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '~> 6.4.0'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron'
|
||||
gem 'sidekiq-cron', '~> 1.3'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'fcm'
|
||||
|
@ -125,6 +122,19 @@ gem 'procore-sift'
|
|||
gem 'email_reply_trimmer'
|
||||
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'
|
||||
|
@ -153,17 +163,14 @@ group :test do
|
|||
end
|
||||
|
||||
group :development, :test do
|
||||
# TODO: is this needed ?
|
||||
# errors thrown by devise password gem
|
||||
gem 'flay'
|
||||
gem 'rspec'
|
||||
# for error thrown by devise password gem
|
||||
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'
|
||||
|
|
484
Gemfile.lock
484
Gemfile.lock
|
@ -1,6 +1,6 @@
|
|||
GIT
|
||||
remote: https://github.com/chatwoot/devise-secure_password
|
||||
revision: de11e8765654b8242d42101ee9c8ffc8126f7975
|
||||
revision: d777b04f12652d576b1272b8f39857e3e0b3fc26
|
||||
specs:
|
||||
devise-secure_password (2.0.1)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
|
@ -9,63 +9,63 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activejob (6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activerecord (6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
marcel (~> 1.0.0)
|
||||
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.4.7)
|
||||
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)
|
||||
concurrent-ruby (1.1.9)
|
||||
commonmarker (0.23.6)
|
||||
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,54 +178,78 @@ 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)
|
||||
erubis (2.7.0)
|
||||
et-orbi (1.2.6)
|
||||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
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)
|
||||
flay (2.12.1)
|
||||
erubis (~> 2.7.0)
|
||||
path_expander (~> 1.0)
|
||||
ruby_parser (~> 3.0)
|
||||
sexp_processor (~> 4.0)
|
||||
foreman (0.87.2)
|
||||
fugit (1.5.2)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
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)
|
||||
|
@ -232,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)
|
||||
|
@ -256,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)
|
||||
|
@ -294,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)
|
||||
|
@ -309,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)
|
||||
|
@ -335,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.14.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)
|
||||
|
@ -364,37 +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.4.0)
|
||||
newrelic_rpm (8.9.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.13.3)
|
||||
nokogiri (1.13.7)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.3-arm64-darwin)
|
||||
nokogiri (1.13.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.3-x86_64-darwin)
|
||||
nokogiri (1.13.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.3-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)
|
||||
path_expander (1.1.0)
|
||||
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)
|
||||
|
@ -402,59 +453,59 @@ GEM
|
|||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.6.2)
|
||||
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)
|
||||
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.4.7)
|
||||
actioncable (= 6.1.4.7)
|
||||
actionmailbox (= 6.1.4.7)
|
||||
actionmailer (= 6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actiontext (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.4.7)
|
||||
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.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
railties (6.1.6.1)
|
||||
actionpack (= 6.1.6.1)
|
||||
activesupport (= 6.1.6.1)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
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)
|
||||
|
@ -468,16 +519,12 @@ GEM
|
|||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rspec (3.11.0)
|
||||
rspec-core (~> 3.11.0)
|
||||
rspec-expectations (~> 3.11.0)
|
||||
rspec-mocks (~> 3.11.0)
|
||||
rspec-core (3.11.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
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)
|
||||
|
@ -489,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)
|
||||
|
@ -516,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)
|
||||
|
@ -526,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.1.0)
|
||||
sentry-rails (5.3.1)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby-core (~> 5.1.0)
|
||||
sentry-ruby (5.1.0)
|
||||
sentry-ruby-core (~> 5.3.1)
|
||||
sentry-ruby (5.3.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-ruby-core (= 5.1.0)
|
||||
sentry-ruby-core (5.1.0)
|
||||
sentry-ruby-core (= 5.3.1)
|
||||
sentry-ruby-core (5.3.1)
|
||||
concurrent-ruby
|
||||
sentry-sidekiq (5.1.0)
|
||||
sentry-ruby-core (~> 5.1.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.2.0)
|
||||
fugit (~> 1.1)
|
||||
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)
|
||||
|
@ -574,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)
|
||||
|
@ -583,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)
|
||||
|
@ -636,7 +685,10 @@ GEM
|
|||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.5.4)
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
|
@ -672,13 +724,13 @@ DEPENDENCIES
|
|||
devise_token_auth
|
||||
dotenv-rails
|
||||
down (~> 5.0)
|
||||
elastic-apm
|
||||
email_reply_trimmer
|
||||
facebook-messenger
|
||||
factory_bot_rails
|
||||
faker
|
||||
fcm
|
||||
flag_shih_tzu
|
||||
flay
|
||||
foreman
|
||||
geocoder
|
||||
google-cloud-dialogflow
|
||||
|
@ -688,7 +740,7 @@ DEPENDENCIES
|
|||
hairtrigger
|
||||
hashie
|
||||
html2text
|
||||
image_processing
|
||||
image_processing (~> 1.12.2)
|
||||
jbuilder
|
||||
json_refs
|
||||
json_schemer
|
||||
|
@ -703,6 +755,7 @@ DEPENDENCIES
|
|||
mock_redis
|
||||
newrelic_rpm
|
||||
pg
|
||||
pg_search
|
||||
procore-sift
|
||||
pry-rails
|
||||
puma
|
||||
|
@ -710,12 +763,11 @@ DEPENDENCIES
|
|||
rack-attack
|
||||
rack-cors
|
||||
rack-timeout
|
||||
rails
|
||||
rails (~> 6.1)
|
||||
redis
|
||||
redis-namespace
|
||||
responders
|
||||
rest-client
|
||||
rspec
|
||||
rspec-rails (~> 5.0.0)
|
||||
rubocop
|
||||
rubocop-performance
|
||||
|
@ -723,20 +775,21 @@ DEPENDENCIES
|
|||
rubocop-rspec
|
||||
scout_apm
|
||||
seed_dump
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
sentry-rails (~> 5.3)
|
||||
sentry-ruby (~> 5.3)
|
||||
sentry-sidekiq (~> 5.3)
|
||||
shoulda-matchers
|
||||
sidekiq (~> 6.4.0)
|
||||
sidekiq-cron
|
||||
sidekiq-cron (~> 1.3)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
stripe
|
||||
telephone_number
|
||||
time_diff
|
||||
twilio-ruby (~> 5.32.0)
|
||||
twilio-ruby (~> 5.66)
|
||||
twitty
|
||||
tzinfo-data
|
||||
uglifier
|
||||
|
@ -746,9 +799,10 @@ DEPENDENCIES
|
|||
webpacker (~> 5.x)
|
||||
webpush
|
||||
wisper (= 2.0.0)
|
||||
working_hours
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.0.2p107
|
||||
ruby 3.0.4p208
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.8
|
||||
2.3.17
|
||||
|
|
|
@ -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"/>
|
||||
|
|
49
SECURITY.md
49
SECURITY.md
|
@ -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
1
VERSION_CW
Normal file
|
@ -0,0 +1 @@
|
|||
2.2.0
|
1
VERSION_CWCTL
Normal file
1
VERSION_CWCTL
Normal file
|
@ -0,0 +1 @@
|
|||
2.1.0
|
5
app.json
5
app.json
|
@ -32,6 +32,10 @@
|
|||
"INSTALLATION_ENV": {
|
||||
"description": "Installation method used for Chatwoot.",
|
||||
"value": "heroku"
|
||||
},
|
||||
"REDIS_OPENSSL_VERIFY_MODE":{
|
||||
"description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues",
|
||||
"value": "none"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
@ -44,6 +48,7 @@
|
|||
"size": "FREE"
|
||||
}
|
||||
},
|
||||
"stack": "heroku-20",
|
||||
"image": "heroku/ruby",
|
||||
"addons": [ "heroku-redis", "heroku-postgresql"],
|
||||
"buildpacks": [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Campaigns::CampaignConversationBuilder
|
||||
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes]
|
||||
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes, :custom_attributes]
|
||||
|
||||
def perform
|
||||
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
||||
|
@ -9,19 +9,23 @@ 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
|
||||
|
||||
def message_params
|
||||
ActionController::Parameters.new({
|
||||
content: @campaign.message
|
||||
content: @campaign.message,
|
||||
campaign_id: @campaign.id
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -32,7 +36,8 @@ class Campaigns::CampaignConversationBuilder
|
|||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
campaign_id: @campaign.id,
|
||||
additional_attributes: conversation_additional_attributes
|
||||
additional_attributes: conversation_additional_attributes,
|
||||
custom_attributes: custom_attributes || {}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,16 +15,15 @@ class ContactBuilder
|
|||
end
|
||||
|
||||
def create_contact_inbox(contact)
|
||||
::ContactInbox.create!(
|
||||
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id,
|
||||
source_id: source_id,
|
||||
hmac_verified: hmac_verified || false
|
||||
source_id: source_id
|
||||
)
|
||||
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
|
||||
|
@ -70,7 +69,7 @@ class ContactBuilder
|
|||
update_contact_avatar(contact)
|
||||
contact_inbox
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Rails.logger.error e
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,9 +27,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
end
|
||||
ensure_contact_avatar
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||
@inbox.channel.authorization_error!
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||
true
|
||||
end
|
||||
|
||||
|
@ -43,7 +43,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
end
|
||||
|
||||
def build_message
|
||||
|
@ -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
|
||||
|
@ -128,10 +128,10 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
result = {}
|
||||
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
||||
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
||||
Sentry.capture_exception(e) unless @outgoing_echo
|
||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Sentry.capture_exception(e)
|
||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||
end
|
||||
process_contact_params_result(result)
|
||||
end
|
||||
|
|
|
@ -24,7 +24,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||
true
|
||||
end
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ class Messages::MessageBuilder
|
|||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
|
@ -64,6 +65,18 @@ class Messages::MessageBuilder
|
|||
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
||||
end
|
||||
|
||||
def automation_rule_id
|
||||
@automation_rule.present? ? { content_attributes: { automation_rule_id: @automation_rule } } : {}
|
||||
end
|
||||
|
||||
def campaign_id
|
||||
@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'
|
||||
|
||||
|
@ -82,6 +95,6 @@ class Messages::MessageBuilder
|
|||
items: @items,
|
||||
in_reply_to: @in_reply_to,
|
||||
echo_id: @params[:echo_id]
|
||||
}.merge(external_created_at)
|
||||
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -53,16 +53,10 @@ class Messages::Messenger::MessageBuilder
|
|||
|
||||
def fetch_story_link(attachment)
|
||||
message = attachment.message
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(message.source_id, fields: %w[story from]) || {}
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
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
|
||||
|
@ -70,4 +64,20 @@ class Messages::Messenger::MessageBuilder
|
|||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
||||
message.save!
|
||||
end
|
||||
|
||||
def get_story_object_from_source_id(source_id)
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
k.get_object(source_id, fields: %w[story from]) || {}
|
||||
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
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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}?")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
class V2::ReportBuilder
|
||||
include DateRangeHelper
|
||||
include ReportHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
DEFAULT_GROUP_BY = 'day'.freeze
|
||||
AGENT_RESULTS_PER_PAGE = 25
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
|
@ -18,8 +20,14 @@ class V2::ReportBuilder
|
|||
|
||||
# For backward compatible with old report
|
||||
def build
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].to_time.to_i }
|
||||
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
|
||||
end
|
||||
else
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -34,23 +42,16 @@ class V2::ReportBuilder
|
|||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope
|
||||
case params[:type]
|
||||
when :account
|
||||
account
|
||||
when :inbox
|
||||
inbox
|
||||
when :agent
|
||||
user
|
||||
when :label
|
||||
label
|
||||
when :team
|
||||
team
|
||||
def conversation_metrics
|
||||
if params[:type].equal?(:account)
|
||||
conversations
|
||||
else
|
||||
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
|
@ -68,7 +69,7 @@ class V2::ReportBuilder
|
|||
end
|
||||
|
||||
def get_grouped_values(object_scope)
|
||||
object_scope.group_by_period(
|
||||
@grouped_values = object_scope.group_by_period(
|
||||
params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at,
|
||||
default_value: 0,
|
||||
|
@ -78,47 +79,29 @@ class V2::ReportBuilder
|
|||
)
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
(get_grouped_values scope.conversations).count
|
||||
def agent_metrics
|
||||
account_users = @account.account_users.page(params[:page]).per(AGENT_RESULTS_PER_PAGE)
|
||||
account_users.each_with_object([]) do |account_user, arr|
|
||||
@user = account_user.user
|
||||
arr << {
|
||||
id: @user.id,
|
||||
name: @user.name,
|
||||
email: @user.email,
|
||||
thumbnail: @user.avatar_url,
|
||||
availability: account_user.availability_status,
|
||||
metric: conversations
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
(get_grouped_values scope.conversations.resolved).count
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time
|
||||
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time_summary
|
||||
avg_rt = scope.reporting_events
|
||||
.where(name: 'conversation_resolved', created_at: range)
|
||||
.average(:value)
|
||||
|
||||
return 0 if avg_rt.blank?
|
||||
|
||||
avg_rt
|
||||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
avg_frt = scope.reporting_events
|
||||
.where(name: 'first_response', created_at: range)
|
||||
.average(:value)
|
||||
|
||||
return 0 if avg_frt.blank?
|
||||
|
||||
avg_frt
|
||||
def conversations
|
||||
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
||||
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||
metric = {
|
||||
open: @open_conversations.count,
|
||||
unattended: @open_conversations.count - first_response_count
|
||||
}
|
||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||
metric
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
class RoomChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
# TODO: should we only do ensure stream if current account is present?
|
||||
# for now going ahead with guard clauses in update_subscription and broadcast_presence
|
||||
|
||||
ensure_stream
|
||||
current_user
|
||||
current_account
|
||||
|
@ -15,6 +18,8 @@ class RoomChannel < ApplicationCable::Channel
|
|||
private
|
||||
|
||||
def broadcast_presence
|
||||
return if @current_account.blank?
|
||||
|
||||
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
|
||||
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
|
||||
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
|
||||
|
@ -26,6 +31,8 @@ class RoomChannel < ApplicationCable::Channel
|
|||
end
|
||||
|
||||
def update_subscription
|
||||
return if @current_account.blank?
|
||||
|
||||
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||
end
|
||||
|
||||
|
@ -38,6 +45,8 @@ class RoomChannel < ApplicationCable::Channel
|
|||
end
|
||||
|
||||
def current_account
|
||||
return if current_user.blank?
|
||||
|
||||
@current_account ||= if @current_user.is_a? Contact
|
||||
@current_user.account
|
||||
else
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@agent_bot.destroy
|
||||
@agent_bot.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@agent.current_account_user.destroy
|
||||
@agent.current_account_user.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
@ -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
|
||||
|
@ -68,10 +68,10 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def agents
|
||||
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
||||
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
|
||||
|
|
57
app/controllers/api/v1/accounts/articles_controller.rb
Normal file
57
app/controllers/api/v1/accounts/articles_controller.rb
Normal 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, :slug, :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
|
|
@ -0,0 +1,24 @@
|
|||
class Api::V1::Accounts::AssignableAgentsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inboxes
|
||||
|
||||
def index
|
||||
agent_ids = @inboxes.map do |inbox|
|
||||
authorize inbox, :show?
|
||||
member_ids = inbox.members.pluck(:user_id)
|
||||
member_ids
|
||||
end
|
||||
agent_ids = agent_ids.inject(:&)
|
||||
agents = Current.account.users.where(id: agent_ids)
|
||||
@assignable_agents = (agents + Current.account.administrators).uniq
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_inboxes
|
||||
@inboxes = Current.account.inboxes.find(permitted_params[:inbox_ids])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(inbox_ids: [])
|
||||
end
|
||||
end
|
|
@ -7,13 +7,38 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||
end
|
||||
|
||||
def create
|
||||
@automation_rule = Current.account.automation_rules.create(automation_rules_permit)
|
||||
@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?
|
||||
|
||||
@automation_rule.save!
|
||||
process_attachments
|
||||
@automation_rule
|
||||
end
|
||||
|
||||
def attach_file
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@automation_rule.update(automation_rules_permit)
|
||||
ActiveRecord::Base.transaction do
|
||||
automation_rule_update
|
||||
process_attachments
|
||||
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
@ -24,16 +49,34 @@ 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
|
||||
|
||||
def process_attachments
|
||||
actions = @automation_rule.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||
return if actions.blank?
|
||||
|
||||
actions.each do |action|
|
||||
blob_id = action['action_params']
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
@automation_rule.files.attach(blob)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -1,32 +1,6 @@
|
|||
class Api::V1::Accounts::BaseController < Api::BaseController
|
||||
include SwitchLocale
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :current_account
|
||||
around_action :switch_locale_using_account_locale
|
||||
|
||||
private
|
||||
|
||||
def current_account
|
||||
@current_account ||= ensure_current_account
|
||||
Current.account = @current_account
|
||||
end
|
||||
|
||||
def ensure_current_account
|
||||
account = Account.find(params[:account_id])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
@current_account_user = account.account_users.find_by(user_id: current_user.id)
|
||||
Current.account_user = @current_account_user
|
||||
render_unauthorized('You are not authorized to access this account') unless @current_account_user
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||
set_instagram_id(page_access_token, facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -60,7 +60,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||
set_instagram_id(access_token, fb_page)
|
||||
fb_page&.reauthorized!
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -77,7 +77,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Rails.logger.error e
|
||||
end
|
||||
|
||||
def mark_already_existing_facebook_pages(data)
|
||||
|
@ -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
|
||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@campaign.destroy
|
||||
@campaign.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
|
|||
end
|
||||
|
||||
def destroy
|
||||
@canned_response.destroy
|
||||
@canned_response.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
57
app/controllers/api/v1/accounts/categories_controller.rb
Normal file
57
app/controllers/api/v1/accounts/categories_controller.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
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.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
|
||||
@category.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_category
|
||||
@category = @portal.categories.find(params[:id])
|
||||
end
|
||||
|
||||
def portal
|
||||
@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, :slug, :locale, :parent_category_id, :associated_category_id
|
||||
)
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
end
|
|
@ -7,7 +7,6 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
|||
build_inbox
|
||||
setup_webhooks if @twilio_channel.sms?
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
end
|
||||
|
@ -28,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
|
||||
|
||||
|
@ -39,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
|
||||
)
|
||||
|
@ -50,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
|
||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
|
|||
end
|
||||
|
||||
def destroy
|
||||
@note.destroy
|
||||
@note.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :conversation
|
||||
|
||||
private
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
|
||||
@conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id])
|
||||
authorize @conversation.inbox, :show?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage::DirectUploadsController
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :current_account
|
||||
before_action :conversation
|
||||
|
||||
def create
|
||||
return if @conversation.nil? || @current_account.nil?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
|
||||
end
|
||||
end
|
|
@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
|||
|
||||
def destroy
|
||||
ActiveRecord::Base.transaction do
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), deleted: true)
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
|
||||
message.attachments.destroy_all
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
|||
RESULTS_PER_PAGE = 25
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_csat_survey_responses, only: [:index, :metrics]
|
||||
before_action :set_csat_survey_responses, only: [:index, :metrics, :download]
|
||||
before_action :set_current_page, only: [:index]
|
||||
before_action :set_current_page_surveys, only: [:index]
|
||||
before_action :set_total_sent_messages_count, only: [:metrics]
|
||||
|
@ -19,6 +19,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
|||
@ratings_count = @csat_survey_responses.group(:rating).count
|
||||
end
|
||||
|
||||
def download
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv'
|
||||
render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_total_sent_messages_count
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
|||
end
|
||||
|
||||
def destroy
|
||||
@custom_attribute_definition.destroy
|
||||
@custom_attribute_definition.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
|
|||
end
|
||||
|
||||
def destroy
|
||||
@custom_filter.destroy
|
||||
@custom_filter.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
|
|
44
app/controllers/api/v1/accounts/dashboard_apps_controller.rb
Normal file
44
app/controllers/api/v1/accounts/dashboard_apps_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
class Api::V1::Accounts::DashboardAppsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_dashboard_apps, except: [:create]
|
||||
before_action :fetch_dashboard_app, only: [:show, :update, :destroy]
|
||||
|
||||
def index; end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@dashboard_app = Current.account.dashboard_apps.create!(
|
||||
permitted_payload.merge(user_id: Current.user.id)
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
@dashboard_app.update!(permitted_payload)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@dashboard_app.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_dashboard_apps
|
||||
@dashboard_apps = Current.account.dashboard_apps
|
||||
end
|
||||
|
||||
def fetch_dashboard_app
|
||||
@dashboard_app = @dashboard_apps.find(permitted_params[:id])
|
||||
end
|
||||
|
||||
def permitted_payload
|
||||
params.require(:dashboard_app).permit(
|
||||
:title,
|
||||
content: [:url, :type]
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id)
|
||||
end
|
||||
end
|
|
@ -12,6 +12,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
|
||||
def show; end
|
||||
|
||||
# Deprecated: This API will be removed in 2.7.0
|
||||
def assignable_agents
|
||||
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
||||
end
|
||||
|
@ -41,18 +42,30 @@ 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?
|
||||
|
||||
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
|
||||
if @inbox.inbox_type == 'Email'
|
||||
begin
|
||||
validate_email_channel(channel_attributes)
|
||||
rescue StandardError => e
|
||||
render json: { message: e }, status: :unprocessable_entity and return
|
||||
end
|
||||
@inbox.channel.reauthorized!
|
||||
end
|
||||
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||
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
|
||||
|
@ -69,7 +82,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@inbox.destroy
|
||||
@inbox.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
@ -84,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])
|
||||
|
||||
|
@ -104,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
|
||||
|
@ -124,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
|
||||
|
@ -143,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')
|
||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
|||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy
|
||||
@hook.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
|||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy
|
||||
@hook.destroy!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseController
|
||||
before_action :portal
|
||||
|
||||
private
|
||||
|
||||
def portal
|
||||
@portal ||= Current.account.kbase_portals.find_by(id: params[:portal_id])
|
||||
end
|
||||
end
|
|
@ -1,32 +0,0 @@
|
|||
class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase::BaseController
|
||||
before_action :fetch_category, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@categories = @portal.categories
|
||||
end
|
||||
|
||||
def create
|
||||
@category = @portal.categories.create!(category_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@category.update!(category_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@category.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_category
|
||||
@category = @portal.categories.find(params[:id])
|
||||
end
|
||||
|
||||
def category_params
|
||||
params.require(:category).permit(
|
||||
:name, :description, :position
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,32 +0,0 @@
|
|||
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::BaseController
|
||||
before_action :fetch_portal, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@portals = Current.account.kbase_portals
|
||||
end
|
||||
|
||||
def create
|
||||
@portal = Current.account.kbase_portals.create!(portal_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@portal.update!(portal_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@portal.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_portal
|
||||
@portal = current_account.kbase_portals.find(params[:id])
|
||||
end
|
||||
|
||||
def portal_params
|
||||
params.require(:portal).permit(
|
||||
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug
|
||||
)
|
||||
end
|
||||
end
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@label.destroy
|
||||
@label.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
59
app/controllers/api/v1/accounts/macros_controller.rb
Normal file
59
app/controllers/api/v1/accounts/macros_controller.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
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
|
||||
head :not_found if @macro.nil?
|
||||
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
|
73
app/controllers/api/v1/accounts/portals_controller.rb
Normal file
73
app/controllers/api/v1/accounts/portals_controller.rb
Normal file
|
@ -0,0 +1,73 @@
|
|||
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.build(portal_params)
|
||||
@portal.save!
|
||||
process_attached_logo
|
||||
end
|
||||
|
||||
def update
|
||||
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
|
||||
@portal.destroy!
|
||||
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
|
||||
@portal = Current.account.portals.find_by(slug: permitted_params[:id])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id)
|
||||
end
|
||||
|
||||
def portal_params
|
||||
params.require(:portal).permit(
|
||||
: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
|
|
@ -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
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@team.destroy
|
||||
@team.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
|
@ -16,14 +16,14 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@webhook.destroy
|
||||
@webhook.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook).permit(:inbox_id, :url)
|
||||
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
|
||||
end
|
||||
|
||||
def fetch_webhook
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
|
|||
|
||||
def destroy
|
||||
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
|
||||
notification_subscription.destroy
|
||||
notification_subscription.destroy! if notification_subscription.present?
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::WebhooksController < ApplicationController
|
|||
twitter_consumer.consume
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class Api::V1::Widget::BaseController < ApplicationController
|
||||
include SwitchLocale
|
||||
include WebsiteTokenHelper
|
||||
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
|
@ -19,23 +20,6 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
@conversation ||= conversations.last
|
||||
end
|
||||
|
||||
def auth_token_params
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
@current_account = @web_widget.account
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
raise ActiveRecord::RecordNotFound unless @contact
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
::Conversation.create!(conversation_params)
|
||||
end
|
||||
|
@ -52,36 +36,25 @@ 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, name: contact_name, phone_number: contact_phone_number)
|
||||
end
|
||||
end
|
||||
|
||||
def contact_email
|
||||
permitted_params[:contact][:email].downcase
|
||||
permitted_params.dig(:contact, :email)&.downcase
|
||||
end
|
||||
|
||||
def contact_name
|
||||
params[:contact][:name] || contact_email.split('@')[0]
|
||||
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
|
||||
end
|
||||
|
||||
def contact_phone_number
|
||||
params[:contact][:phone_number]
|
||||
permitted_params.dig(:contact, :phone_number)
|
||||
end
|
||||
|
||||
def browser_params
|
||||
|
@ -98,10 +71,6 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
{ timestamp: permitted_params[:message][:timestamp] }
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -7,12 +7,20 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||
process_update_contact
|
||||
@conversation = create_conversation
|
||||
conversation.messages.create(message_params)
|
||||
conversation.messages.create!(message_params)
|
||||
end
|
||||
end
|
||||
|
||||
def process_update_contact
|
||||
@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
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
|
@ -44,6 +52,18 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
head :ok
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
return head :not_found if conversation.nil?
|
||||
|
||||
return head :forbidden unless @web_widget.end_conversation?
|
||||
|
||||
unless conversation.resolved?
|
||||
conversation.status = :resolved
|
||||
conversation.save!
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trigger_typing_event(event)
|
||||
|
@ -52,6 +72,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
|
||||
def permitted_params
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
||||
message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
message: [:content, :referer_url, :timestamp, :echo_id],
|
||||
custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
|
11
app/controllers/api/v1/widget/direct_uploads_controller.rb
Normal file
11
app/controllers/api/v1/widget/direct_uploads_controller.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController
|
||||
include WebsiteTokenHelper
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
|
||||
def create
|
||||
return if @contact.nil? || @current_account.nil?
|
||||
|
||||
super
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
include Api::V2::Accounts::ReportsHelper
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
|
@ -12,64 +13,80 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def agents
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=agents_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/agents.csv.erb', format: 'csv'
|
||||
@report_data = generate_agents_report
|
||||
generate_csv('agents_report', 'api/v2/accounts/reports/agents.csv.erb')
|
||||
end
|
||||
|
||||
def inboxes
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=inboxes_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/inboxes.csv.erb', format: 'csv'
|
||||
@report_data = generate_inboxes_report
|
||||
generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes.csv.erb')
|
||||
end
|
||||
|
||||
def labels
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=labels_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
|
||||
@report_data = generate_labels_report
|
||||
generate_csv('labels_report', 'api/v2/accounts/reports/labels.csv.erb')
|
||||
end
|
||||
|
||||
def teams
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=teams_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
||||
@report_data = generate_teams_report
|
||||
generate_csv('teams_report', 'api/v2/accounts/reports/teams.csv.erb')
|
||||
end
|
||||
|
||||
def conversations
|
||||
return head :unprocessable_entity if params[:type].blank?
|
||||
|
||||
render json: conversation_metrics
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_csv(filename, template)
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv"
|
||||
render layout: false, template: template, format: 'csv'
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def current_summary_params
|
||||
def common_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
id: params[:id],
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until],
|
||||
group_by: params[:group_by]
|
||||
group_by: params[:group_by],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||
}
|
||||
end
|
||||
|
||||
def current_summary_params
|
||||
common_params.merge({
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until]
|
||||
})
|
||||
end
|
||||
|
||||
def previous_summary_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
id: params[:id],
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until],
|
||||
group_by: params[:group_by]
|
||||
}
|
||||
common_params.merge({
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until]
|
||||
})
|
||||
end
|
||||
|
||||
def report_params
|
||||
common_params.merge({
|
||||
metric: params[:metric],
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
})
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
metric: params[:metric],
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id],
|
||||
group_by: params[:group_by],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
user_id: params[:user_id],
|
||||
page: params[:page].presence || 1
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -91,4 +108,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
|
||||
summary
|
||||
end
|
||||
|
||||
def conversation_metrics
|
||||
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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,
|
||||
|
|
34
app/controllers/concerns/ensure_current_account_helper.rb
Normal file
34
app/controllers/concerns/ensure_current_account_helper.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
module EnsureCurrentAccountHelper
|
||||
private
|
||||
|
||||
def current_account
|
||||
@current_account ||= ensure_current_account
|
||||
Current.account = @current_account
|
||||
end
|
||||
|
||||
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)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
@current_account_user = account.account_users.find_by(user_id: current_user.id)
|
||||
Current.account_user = @current_account_user
|
||||
render_unauthorized('You are not authorized to access this account') unless @current_account_user
|
||||
end
|
||||
|
||||
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
|
20
app/controllers/concerns/meta_token_verify_concern.rb
Normal file
20
app/controllers/concerns/meta_token_verify_concern.rb
Normal 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
|
|
@ -9,8 +9,7 @@ module RequestExceptionHandler
|
|||
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Sentry.capture_exception(e)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_not_found_error('Resource could not be found')
|
||||
rescue Pundit::NotAuthorizedError
|
||||
render_unauthorized('You are not authorized to do this action')
|
||||
|
|
26
app/controllers/concerns/website_token_helper.rb
Normal file
26
app/controllers/concerns/website_token_helper.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
module WebsiteTokenHelper
|
||||
def auth_token_params
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
@current_account = @web_widget.inbox.account
|
||||
|
||||
render json: { error: 'Account is suspended' }, status: :unauthorized unless @current_account.active?
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
raise ActiveRecord::RecordNotFound unless @contact
|
||||
|
||||
Current.contact = @contact
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
|
@ -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 :render_hc_if_custom_domain, only: [:index]
|
||||
|
||||
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,10 +38,25 @@ class DashboardController < ActionController::Base
|
|||
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||
end
|
||||
|
||||
def render_hc_if_custom_domain
|
||||
domain = request.host
|
||||
return if domain == URI.parse(ENV.fetch('FRONTEND_URL', '')).host
|
||||
|
||||
@portal = Portal.find_by(custom_domain: domain)
|
||||
return unless @portal
|
||||
|
||||
@locale = @portal.default_locale
|
||||
render 'public/api/v1/portals/show', layout: 'portal', portal: @portal and return
|
||||
end
|
||||
|
||||
def app_config
|
||||
{ APP_VERSION: Chatwoot.config[:version],
|
||||
{
|
||||
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', '') }
|
||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||
FACEBOOK_API_VERSION: 'v14.0',
|
||||
IS_ENTERPRISE: ChatwootApp.enterprise?
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,7 +13,7 @@ class Platform::Api::V1::AccountUsersController < PlatformController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy
|
||||
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
|
@ -27,6 +27,6 @@ class Platform::Api::V1::AccountsController < PlatformController
|
|||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:name)
|
||||
params.permit(:name, :locale)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,20 +7,24 @@ class Platform::Api::V1::UsersController < PlatformController
|
|||
|
||||
def create
|
||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||
@resource.skip_confirmation!
|
||||
@resource.save!
|
||||
@resource.confirm
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def update
|
||||
@resource.assign_attributes(user_update_params)
|
||||
|
||||
# We are using devise's reconfirmable flow for changing emails
|
||||
# But in case of platform APIs we don't want user to go through this extra step
|
||||
@resource.skip_reconfirmation! if user_update_params[:email].present?
|
||||
@resource.save!
|
||||
end
|
||||
|
||||
|
|
|
@ -43,6 +43,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
|||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
|
||||
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
|
39
app/controllers/public/api/v1/portals/articles_controller.rb
Normal file
39
app/controllers/public/api/v1/portals/articles_controller.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :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 portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:query)
|
||||
end
|
||||
|
||||
def render_article_content(content)
|
||||
# rubocop:disable Rails/OutputSafety
|
||||
CommonMarker.render_html(content).html_safe
|
||||
# rubocop:enable Rails/OutputSafety
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
class Public::Api::V1::Portals::CategoriesController < PublicController
|
||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :portal
|
||||
before_action :set_category, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
def index
|
||||
@categories = @portal.categories
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def set_category
|
||||
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
end
|
||||
end
|
21
app/controllers/public/api/v1/portals_controller.rb
Normal file
21
app/controllers/public/api/v1/portals_controller.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
class Public::Api::V1::PortalsController < PublicController
|
||||
before_action :ensure_custom_domain_request, only: [:show]
|
||||
before_action :portal
|
||||
before_action :redirect_to_portal_with_locale, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
@locale = params[:locale] || @portal.default_locale
|
||||
end
|
||||
|
||||
def redirect_to_portal_with_locale
|
||||
return if params[:locale].present?
|
||||
|
||||
redirect_to "/hc/#{@portal.slug}/#{@portal.default_locale}"
|
||||
end
|
||||
end
|
|
@ -3,4 +3,20 @@
|
|||
class PublicController < ActionController::Base
|
||||
include RequestExceptionHandler
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
||||
private
|
||||
|
||||
def ensure_custom_domain_request
|
||||
domain = request.host
|
||||
|
||||
return if [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain)
|
||||
|
||||
@portal = ::Portal.find_by(custom_domain: domain)
|
||||
return if @portal.present?
|
||||
|
||||
render json: {
|
||||
error: "Domain: #{domain} is not registered with us. \
|
||||
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
||||
}, status: :unauthorized and return
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,9 +36,15 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
|||
def resource_params
|
||||
permitted_params = super
|
||||
permitted_params[:limits] = permitted_params[:limits].to_h.compact
|
||||
permitted_params[:selected_feature_flags] = params[:enabled_features].keys.map(&:to_sym) if params[:enabled_features].present?
|
||||
permitted_params
|
||||
end
|
||||
|
||||
# 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,7 +14,7 @@ class Twitter::CallbacksController < Twitter::BaseController
|
|||
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Rails.logger.error e
|
||||
redirect_to twitter_app_redirect_url
|
||||
end
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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')
|
||||
|
@ -17,14 +7,14 @@ class Webhooks::InstagramController < ApplicationController
|
|||
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
||||
render json: :ok
|
||||
else
|
||||
Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
|
||||
Rails.logger.warn("Message is not received from the instagram webhook event: #{params['object']}")
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_instagram_token?(token)
|
||||
def valid_token?(token)
|
||||
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,6 +3,7 @@ class WidgetTestsController < ActionController::Base
|
|||
before_action :ensure_widget_position
|
||||
before_action :ensure_widget_type
|
||||
before_action :ensure_widget_style
|
||||
before_action :ensure_dark_mode
|
||||
|
||||
def index
|
||||
render
|
||||
|
@ -14,6 +15,10 @@ class WidgetTestsController < ActionController::Base
|
|||
@widget_style = params[:widget_style] || 'standard'
|
||||
end
|
||||
|
||||
def ensure_dark_mode
|
||||
@dark_mode = params[:dark_mode] || 'light'
|
||||
end
|
||||
|
||||
def ensure_widget_position
|
||||
@widget_position = params[:position] || 'left'
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,7 +8,15 @@ class AccountDashboard < Administrate::BaseDashboard
|
|||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
|
||||
enterprise_attribute_types = ChatwootApp.enterprise? ? { limits: Enterprise::AccountLimitsField } : {}
|
||||
enterprise_attribute_types = if ChatwootApp.enterprise?
|
||||
{
|
||||
limits: Enterprise::AccountLimitsField,
|
||||
all_features: Enterprise::AccountFeaturesField
|
||||
}
|
||||
else
|
||||
{}
|
||||
end
|
||||
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
|
@ -17,6 +25,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,17 +40,19 @@ class AccountDashboard < Administrate::BaseDashboard
|
|||
locale
|
||||
users
|
||||
conversations
|
||||
status
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
||||
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : []
|
||||
SHOW_PAGE_ATTRIBUTES = (%i[
|
||||
id
|
||||
name
|
||||
created_at
|
||||
updated_at
|
||||
locale
|
||||
status
|
||||
conversations
|
||||
account_users
|
||||
] + enterprise_show_page_attributes).freeze
|
||||
|
@ -49,10 +60,11 @@ class AccountDashboard < Administrate::BaseDashboard
|
|||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
||||
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : []
|
||||
FORM_ATTRIBUTES = (%i[
|
||||
name
|
||||
locale
|
||||
status
|
||||
] + enterprise_form_attributes).freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue