Compare commits
1 commit
develop
...
infra/hero
Author | SHA1 | Date | |
---|---|---|---|
|
74a512e3a1 |
3890 changed files with 27626 additions and 300693 deletions
|
@ -1,3 +0,0 @@
|
||||||
---
|
|
||||||
ignore:
|
|
||||||
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
|
|
|
@ -7,20 +7,16 @@ defaults: &defaults
|
||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
docker:
|
||||||
# specify the version you desire here
|
# specify the version you desire here
|
||||||
- image: cimg/ruby:3.0.4-browsers
|
- image: circleci/ruby:2.7.2-node-browsers
|
||||||
|
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||||
- image: cimg/postgres:14.1
|
- image: circleci/postgres:alpine
|
||||||
- image: cimg/redis:6.2.6
|
- image: circleci/redis:alpine
|
||||||
environment:
|
environment:
|
||||||
|
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
|
||||||
- RAILS_LOG_TO_STDOUT: false
|
- RAILS_LOG_TO_STDOUT: false
|
||||||
- COVERAGE: true
|
|
||||||
- LOG_LEVEL: warn
|
|
||||||
parallelism: 4
|
|
||||||
resource_class: large
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
|
@ -44,19 +40,20 @@ jobs:
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
- chatwoot-bundle-{{ checksum "Gemfile.lock" }}
|
||||||
|
- chatwoot-bundle
|
||||||
|
|
||||||
- run: bundle install --frozen --path ~/.bundle
|
- run: bundle install --frozen --path ~/.bundle
|
||||||
- save_cache:
|
- save_cache:
|
||||||
paths:
|
paths:
|
||||||
- ~/.bundle
|
- ~/.bundle
|
||||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
key: chatwoot-bundle-{{ checksum "Gemfile.lock" }}
|
||||||
|
|
||||||
|
|
||||||
# Only necessary if app uses webpacker or yarn in some other way
|
# Only necessary if app uses webpacker or yarn in some other way
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
- chatwoot-yarn-{{ checksum "yarn.lock" }}
|
||||||
- chatwoot-yarn-
|
- chatwoot-yarn-
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
|
@ -65,7 +62,7 @@ jobs:
|
||||||
|
|
||||||
# Store yarn / webpacker cache
|
# Store yarn / webpacker cache
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
key: chatwoot-yarn-{{ checksum "yarn.lock" }}
|
||||||
paths:
|
paths:
|
||||||
- ~/.cache/yarn
|
- ~/.cache/yarn
|
||||||
|
|
||||||
|
@ -80,19 +77,6 @@ jobs:
|
||||||
paths:
|
paths:
|
||||||
- cc-test-reporter
|
- cc-test-reporter
|
||||||
|
|
||||||
# verify swagger specification
|
|
||||||
- run:
|
|
||||||
name: Verify swagger API specification
|
|
||||||
command: |
|
|
||||||
bundle exec rake swagger:build
|
|
||||||
if [[ `git status swagger/swagger.json --porcelain` ]]
|
|
||||||
then
|
|
||||||
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
|
||||||
exit 1
|
|
||||||
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
|
# Database setup
|
||||||
- run: yarn install --check-files
|
- run: yarn install --check-files
|
||||||
- run: bundle exec rake db:create
|
- run: bundle exec rake db:create
|
||||||
|
@ -105,10 +89,6 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Rubocop
|
name: Rubocop
|
||||||
command: bundle exec rubocop
|
command: bundle exec rubocop
|
||||||
|
|
||||||
# - run:
|
|
||||||
# name: Brakeman
|
|
||||||
# command: bundle exec brakeman
|
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: eslint
|
name: eslint
|
||||||
|
@ -118,79 +98,34 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Run backend tests
|
name: Run backend tests
|
||||||
command: |
|
command: |
|
||||||
mkdir -p ~/tmp/test-results/rspec
|
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
|
||||||
mkdir -p ~/tmp/test-artifacts
|
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||||
mkdir -p coverage
|
- persist_to_workspace:
|
||||||
~/tmp/cc-test-reporter before-build
|
root: ~/tmp
|
||||||
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
paths:
|
||||||
bundle exec rspec --format progress \
|
- codeclimate.backend.json
|
||||||
--format RspecJunitFormatter \
|
|
||||||
--out ~/tmp/test-results/rspec.xml \
|
|
||||||
-- ${TESTFILES}
|
|
||||||
no_output_timeout: 30m
|
|
||||||
- run:
|
|
||||||
name: Code Climate Test Coverage
|
|
||||||
command: |
|
|
||||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
|
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Run frontend tests
|
name: Run frontend tests
|
||||||
command: |
|
command: |
|
||||||
mkdir -p ~/tmp/test-results/frontend_specs
|
yarn test:coverage
|
||||||
~/tmp/cc-test-reporter before-build
|
~/tmp/cc-test-reporter format-coverage -t lcov -o ~/tmp/codeclimate.frontend.json buildreports/lcov.info
|
||||||
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
|
|
||||||
yarn test:coverage --profile 10 \
|
|
||||||
--out ~/tmp/test-results/yarn.xml \
|
|
||||||
-- ${TESTFILES}
|
|
||||||
- run:
|
|
||||||
name: Code Climate Test Coverage
|
|
||||||
command: |
|
|
||||||
~/tmp/cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json buildreports/lcov.info
|
|
||||||
|
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: coverage
|
root: ~/tmp
|
||||||
paths:
|
paths:
|
||||||
- codeclimate.*.json
|
- codeclimate.frontend.json
|
||||||
|
|
||||||
# collect reports
|
# collect reports
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: ~/tmp/test-results
|
path: ~/tmp/test-results
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: ~/tmp/test-artifacts
|
path: ~/tmp/test-results
|
||||||
|
destination: test-results
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: log
|
path: log
|
||||||
|
|
||||||
upload-coverage:
|
|
||||||
working_directory: ~/build
|
|
||||||
docker:
|
|
||||||
# specify the version you desire here
|
|
||||||
- image: circleci/ruby:3.0.2-node-browsers
|
|
||||||
environment:
|
|
||||||
- CC_TEST_REPORTER_ID: caf26a895e937974a90860cfadfded20891cfd1373a5aaafb3f67406ab9d433f
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: ~/build
|
|
||||||
- run:
|
|
||||||
name: Download cc-test-reporter
|
|
||||||
command: |
|
|
||||||
mkdir -p ~/tmp
|
|
||||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
|
||||||
chmod +x ~/tmp/cc-test-reporter
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: ~/tmp
|
|
||||||
paths:
|
|
||||||
- cc-test-reporter
|
|
||||||
- run:
|
- run:
|
||||||
name: Upload coverage results to Code Climate
|
name: Upload coverage results to Code Climate
|
||||||
command: |
|
command: |
|
||||||
~/tmp/cc-test-reporter sum-coverage --output - codeclimate.*.json | ~/tmp/cc-test-reporter upload-coverage --debug --input -
|
~/tmp/cc-test-reporter sum-coverage ~/tmp/codeclimate.*.json -p 2 -o ~/tmp/codeclimate.total.json
|
||||||
|
~/tmp/cc-test-reporter upload-coverage -i ~/tmp/codeclimate.total.json
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
commit:
|
|
||||||
jobs:
|
|
||||||
- build
|
|
||||||
- upload-coverage:
|
|
||||||
requires:
|
|
||||||
- build
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
version: '2'
|
version: "2"
|
||||||
plugins:
|
plugins:
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -14,45 +14,19 @@ plugins:
|
||||||
checks:
|
checks:
|
||||||
similar-code:
|
similar-code:
|
||||||
enabled: false
|
enabled: false
|
||||||
method-count:
|
|
||||||
enabled: true
|
|
||||||
config:
|
|
||||||
threshold: 32
|
|
||||||
file-lines:
|
|
||||||
enabled: true
|
|
||||||
config:
|
|
||||||
threshold: 300
|
|
||||||
method-lines:
|
|
||||||
config:
|
|
||||||
threshold: 50
|
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
- 'spec/'
|
- "spec/"
|
||||||
- '**/specs/'
|
- "**/specs/"
|
||||||
- 'db/*'
|
- "db/*"
|
||||||
- 'bin/**/*'
|
- "bin/**/*"
|
||||||
- 'db/**/*'
|
- "db/**/*"
|
||||||
- 'config/**/*'
|
- "config/**/*"
|
||||||
- 'public/**/*'
|
- "public/**/*"
|
||||||
- 'vendor/**/*'
|
- "vendor/**/*"
|
||||||
- 'node_modules/**/*'
|
- "node_modules/**/*"
|
||||||
- 'lib/tasks/auto_annotate_models.rake'
|
- "lib/tasks/auto_annotate_models.rake"
|
||||||
- 'app/test-matchers.js'
|
- "app/test-matchers.js"
|
||||||
- 'docs/*'
|
- "docs/*"
|
||||||
- '**/*.md'
|
- "**/*.md"
|
||||||
- '**/*.yml'
|
- "**/*.yml"
|
||||||
- 'app/javascript/dashboard/i18n/locale'
|
- "app/javascript/dashboard/i18n/locale"
|
||||||
- '**/*.stories.js'
|
|
||||||
- 'stories/'
|
|
||||||
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js'
|
|
||||||
- 'app/javascript/shared/constants/countries.js'
|
|
||||||
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js'
|
|
||||||
- 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js'
|
|
||||||
- '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'
|
|
||||||
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
|
|
||||||
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'
|
|
||||||
|
|
|
@ -1,8 +1,51 @@
|
||||||
# The below image is created out of the Dockerfile.base
|
# pre-build stage
|
||||||
# It has the dependencies already installed so that codespace will boot up fast
|
ARG VARIANT=2.7
|
||||||
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
|
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||||
|
|
||||||
|
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||||
|
ARG USER_UID=1000
|
||||||
|
ARG USER_GID=$USER_UID
|
||||||
|
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
||||||
|
groupmod --gid $USER_GID vscode \
|
||||||
|
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
||||||
|
&& 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
|
||||||
|
|
||||||
|
|
||||||
|
# tmux is for overmind
|
||||||
|
# TODO : install foreman in future
|
||||||
|
# packages: postgresql-server-dev-all
|
||||||
|
# may be postgres in same machine
|
||||||
|
|
||||||
|
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get -y install --no-install-recommends \
|
||||||
|
libssl-dev \
|
||||||
|
tar \
|
||||||
|
tzdata \
|
||||||
|
postgresql-client \
|
||||||
|
yarn \
|
||||||
|
git \
|
||||||
|
imagemagick \
|
||||||
|
tmux \
|
||||||
|
zsh
|
||||||
|
|
||||||
|
# [Optional] Uncomment this line to install global node packages.
|
||||||
|
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||||
|
|
||||||
|
|
||||||
# Do the set up required for chatwoot app
|
# Do the set up required for chatwoot app
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
COPY . /workspace
|
COPY . /workspace
|
||||||
RUN yarn && gem install bundler && bundle install
|
|
||||||
|
# TODO: figure out installing rvm
|
||||||
|
# RUN rvm install
|
||||||
|
COPY Gemfile Gemfile.lock ./
|
||||||
|
RUN gem install bundler
|
||||||
|
RUN bundle install
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn install
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
|
|
||||||
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
|
|
||||||
ARG USER_GID=$USER_UID
|
|
||||||
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
|
||||||
groupmod --gid $USER_GID vscode \
|
|
||||||
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
|
||||||
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
|
|
||||||
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 \
|
|
||||||
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 \
|
|
||||||
&& gunzip overmind.gz \
|
|
||||||
&& 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
|
|
||||||
|
|
||||||
# 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
|
|
|
@ -12,29 +12,22 @@
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"rebornix.Ruby",
|
"rebornix.Ruby",
|
||||||
"misogi.ruby-rubocop",
|
"misogi.ruby-rubocop",
|
||||||
"wingrunr21.vscode-ruby",
|
"wingrunr21.vscode-ruby"
|
||||||
"davidpallinder.rails-test-runner",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"github.copilot",
|
|
||||||
"mrmlnc.vscode-duplicate"
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: figure whether we can get all this ports work properly
|
||||||
|
|
||||||
|
// 3000 rails
|
||||||
|
// 3035 webpacker
|
||||||
// 5432 postgres
|
// 5432 postgres
|
||||||
// 6379 redis
|
// 6379 redis
|
||||||
// 1025,8025 mailhog
|
// 1025,8025 mailhog
|
||||||
"forwardPorts": [8025, 3000, 3035],
|
"forwardPorts": [5432, 6379, 1025, 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],
|
||||||
|
|
||||||
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
"portsAttributes": {
|
// #TODO: can we move logic of copy env file into dockerfile ?
|
||||||
"3000": {
|
"postCreateCommand": "cp .env.example .env",
|
||||||
"label": "Rails Server"
|
|
||||||
},
|
|
||||||
"3035": {
|
|
||||||
"label": "Webpack Dev Server"
|
|
||||||
},
|
|
||||||
"8025": {
|
|
||||||
"label": "Mailhog UI"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ services:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: .devcontainer/Dockerfile
|
dockerfile: .devcontainer/Dockerfile
|
||||||
args:
|
args:
|
||||||
# Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
|
# Update 'VARIANT' to pick a Ruby version: 2, 2.7, 2.6, 2.5
|
||||||
VARIANT: 3
|
VARIANT: 2.7
|
||||||
# [Choice] Install Node.js
|
# [Choice] Install Node.js
|
||||||
INSTALL_NODE: "true"
|
INSTALL_NODE: "true"
|
||||||
NODE_VERSION: "lts/*"
|
NODE_VERSION: "lts/*"
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
cp .env.example .env
|
|
||||||
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
|
|
||||||
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
|
||||||
sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env
|
|
||||||
sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env
|
|
||||||
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
|
|
|
@ -7,8 +7,8 @@ end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_style = space
|
indent_style = spaces
|
||||||
tab_width = 2
|
tab_width = 2
|
||||||
|
|
||||||
[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
|
[{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
81
.env.example
81
.env.example
|
@ -3,8 +3,6 @@ SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||||
|
|
||||||
# Replace with the URL you are planning to use for your app
|
# Replace with the URL you are planning to use for your app
|
||||||
FRONTEND_URL=http://0.0.0.0:3000
|
FRONTEND_URL=http://0.0.0.0:3000
|
||||||
# To use a dedicated URL for help center pages
|
|
||||||
# HELPCENTER_URL=http://0.0.0.0:3000
|
|
||||||
|
|
||||||
# If the variable is set, all non-authenticated pages would fallback to the default locale.
|
# If the variable is set, all non-authenticated pages would fallback to the default locale.
|
||||||
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
|
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
|
||||||
|
@ -34,20 +32,7 @@ REDIS_SENTINELS=
|
||||||
# You can find list of master using "SENTINEL masters" command
|
# You can find list of master using "SENTINEL masters" command
|
||||||
REDIS_SENTINEL_MASTER_NAME=
|
REDIS_SENTINEL_MASTER_NAME=
|
||||||
|
|
||||||
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
|
|
||||||
# Use the following environment variable to customize passwords for sentinels.
|
|
||||||
# Use empty string if sentinels are configured with out passwords
|
|
||||||
# REDIS_SENTINEL_PASSWORD=
|
|
||||||
|
|
||||||
# Redis premium breakage in heroku fix
|
|
||||||
# enable the following configuration
|
|
||||||
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
|
||||||
# REDIS_OPENSSL_VERIFY_MODE=none
|
|
||||||
|
|
||||||
# Postgres Database config variables
|
# Postgres Database config variables
|
||||||
# You can leave POSTGRES_DATABASE blank. The default name of
|
|
||||||
# the database in the production environment is chatwoot_production
|
|
||||||
# POSTGRES_DATABASE=
|
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_HOST=postgres
|
||||||
POSTGRES_USERNAME=postgres
|
POSTGRES_USERNAME=postgres
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=
|
||||||
|
@ -58,12 +43,12 @@ RAILS_MAX_THREADS=5
|
||||||
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
||||||
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||||
|
|
||||||
|
|
||||||
#SMTP domain key is set up for HELO checking
|
#SMTP domain key is set up for HELO checking
|
||||||
SMTP_DOMAIN=chatwoot.com
|
SMTP_DOMAIN=chatwoot.com
|
||||||
# Set the value to "mailhog" if using docker-compose for development environments,
|
# the default value is set "mailhog" and is used by docker-compose for development environments,
|
||||||
# Set the value as "localhost" or your SMTP address in other environments
|
# Set the value as "localhost" or your SMTP address in other environments
|
||||||
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
|
SMTP_ADDRESS=mailhog
|
||||||
SMTP_ADDRESS=
|
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_USERNAME=
|
SMTP_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
@ -72,9 +57,6 @@ SMTP_AUTHENTICATION=
|
||||||
SMTP_ENABLE_STARTTLS_AUTO=true
|
SMTP_ENABLE_STARTTLS_AUTO=true
|
||||||
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
||||||
SMTP_OPENSSL_VERIFY_MODE=peer
|
SMTP_OPENSSL_VERIFY_MODE=peer
|
||||||
# Comment out the following environment variables if required by your SMTP server
|
|
||||||
# SMTP_TLS=
|
|
||||||
# SMTP_SSL=
|
|
||||||
|
|
||||||
# Mail Incoming
|
# Mail Incoming
|
||||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||||
|
@ -103,6 +85,9 @@ AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
AWS_REGION=
|
AWS_REGION=
|
||||||
|
|
||||||
|
# Sentry
|
||||||
|
SENTRY_DSN=
|
||||||
|
|
||||||
# Log settings
|
# Log settings
|
||||||
# Disable if you want to write logs to a file
|
# Disable if you want to write logs to a file
|
||||||
RAILS_LOG_TO_STDOUT=true
|
RAILS_LOG_TO_STDOUT=true
|
||||||
|
@ -117,9 +102,6 @@ FB_VERIFY_TOKEN=
|
||||||
FB_APP_SECRET=
|
FB_APP_SECRET=
|
||||||
FB_APP_ID=
|
FB_APP_ID=
|
||||||
|
|
||||||
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
|
||||||
IG_VERIFY_TOKEN=
|
|
||||||
|
|
||||||
# Twitter
|
# Twitter
|
||||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||||
TWITTER_APP_ID=
|
TWITTER_APP_ID=
|
||||||
|
@ -133,11 +115,8 @@ SLACK_CLIENT_SECRET=
|
||||||
|
|
||||||
### Change this env variable only if you are using a custom build mobile app
|
### Change this env variable only if you are using a custom build mobile app
|
||||||
## Mobile app env variables
|
## Mobile app env variables
|
||||||
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
|
||||||
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
|
### Smart App Banner
|
||||||
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
|
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
|
||||||
|
@ -155,31 +134,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||||
## Bot Customizations
|
## Bot Customizations
|
||||||
USE_INBOX_AVATAR_FOR_BOT=true
|
USE_INBOX_AVATAR_FOR_BOT=true
|
||||||
|
|
||||||
### APM and Error Monitoring configurations
|
|
||||||
## Elastic APM
|
|
||||||
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
|
|
||||||
# ELASTIC_APM_SERVER_URL=
|
|
||||||
# ELASTIC_APM_SECRET_TOKEN=
|
|
||||||
|
|
||||||
## Sentry
|
|
||||||
# SENTRY_DSN=
|
|
||||||
|
|
||||||
## Scout
|
|
||||||
## https://scoutapm.com/docs/ruby/configuration
|
|
||||||
# SCOUT_KEY=YOURKEY
|
|
||||||
# SCOUT_NAME=YOURAPPNAME (Production)
|
|
||||||
# SCOUT_MONITOR=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
|
## IP look up configuration
|
||||||
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
|
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
|
||||||
|
@ -188,28 +143,6 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||||
# maxmindb api key to use geoip2 service
|
# maxmindb api key to use geoip2 service
|
||||||
# IP_LOOKUP_API_KEY=
|
# IP_LOOKUP_API_KEY=
|
||||||
|
|
||||||
## Rack Attack configuration
|
|
||||||
## 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
|
|
||||||
|
|
||||||
## Development Only Config
|
## Development Only Config
|
||||||
# if you want to use letter_opener for local emails
|
# if you want to use letter_opener for local emails
|
||||||
# LETTER_OPENER=true
|
# LETTER_OPENER=true
|
||||||
# meant to be used in github codespaces
|
|
||||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
|
||||||
|
|
||||||
# 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=
|
|
||||||
|
|
46
.eslintrc.js
46
.eslintrc.js
|
@ -1,10 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
|
||||||
'airbnb-base/legacy',
|
|
||||||
'prettier',
|
|
||||||
'plugin:vue/recommended',
|
|
||||||
'plugin:storybook/recommended',
|
|
||||||
],
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: 'babel-eslint',
|
parser: 'babel-eslint',
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
|
@ -24,32 +19,17 @@ module.exports = {
|
||||||
'jsx-a11y/label-has-for': 'off',
|
'jsx-a11y/label-has-for': 'off',
|
||||||
'jsx-a11y/anchor-is-valid': 'off',
|
'jsx-a11y/anchor-is-valid': 'off',
|
||||||
'import/no-unresolved': 'off',
|
'import/no-unresolved': 'off',
|
||||||
'vue/max-attributes-per-line': [
|
'vue/max-attributes-per-line': ['error', {
|
||||||
'error',
|
'singleline': 20,
|
||||||
{
|
'multiline': {
|
||||||
singleline: 20,
|
'max': 1,
|
||||||
multiline: {
|
'allowFirstLine': false
|
||||||
max: 1,
|
|
||||||
allowFirstLine: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
}],
|
||||||
'vue/html-self-closing': [
|
'vue/html-self-closing': 'off',
|
||||||
'error',
|
"vue/no-v-html": 'off',
|
||||||
{
|
'import/extensions': ['off']
|
||||||
html: {
|
|
||||||
void: 'always',
|
|
||||||
normal: 'always',
|
|
||||||
component: 'always',
|
|
||||||
},
|
|
||||||
svg: 'always',
|
|
||||||
math: 'always',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'vue/no-v-html': 'off',
|
|
||||||
'vue/singleline-html-element-content-newline': 'off',
|
|
||||||
'import/extensions': ['off'],
|
|
||||||
'no-console': 'error',
|
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
|
@ -60,10 +40,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
jest: true,
|
|
||||||
node: true,
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
jasmine: true
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
|
__WEBPACK_ENV__: true,
|
||||||
bus: true,
|
bus: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -6,7 +6,6 @@ labels: 'Bug'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
@ -17,11 +16,11 @@ Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See the error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
|
|
||||||
Share a clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
|
|
||||||
|
@ -29,50 +28,27 @@ If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Browser logs**
|
**Browser logs**
|
||||||
|
|
||||||
Share the browser logs to debug the issue further.
|
Share the browser logs to debug the issue further
|
||||||
|
|
||||||
**Server logs**
|
**Server logs**
|
||||||
|
|
||||||
Share the server logs to debug the issue further.
|
Share the server logs to debug the issue further
|
||||||
|
|
||||||
**Environment**
|
**Environment**
|
||||||
|
|
||||||
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self hosted installation of Chatwoot. If you are using a self hosted installation of Chatwoot describe the type of deployment (Docker/Linux VM installation/Heroku)
|
||||||
|
|
||||||
- [ ] app.chatwoot.com (Chatwoot Cloud)
|
**Desktop (please complete the following information):**
|
||||||
- [ ] Self-hosted
|
- OS: [e.g. iOS]
|
||||||
- - [ ] Linux VM
|
- Browser [e.g. chrome, safari]
|
||||||
- - [ ] Docker
|
|
||||||
- - [ ] Kubernetes
|
|
||||||
- - [ ] Heroku
|
|
||||||
- - [ ] Other (Please specify)
|
|
||||||
|
|
||||||
|
|
||||||
**Desktop (please complete the following information)** (If applicable)
|
|
||||||
- OS: [e.g. Linux, Windows, MacOS]
|
|
||||||
- Browser [e.g. chrome, firefox, safari]
|
|
||||||
- Version [e.g. 22]
|
- Version [e.g. 22]
|
||||||
|
|
||||||
**Smartphone (please complete the following information)** (If applicable)
|
**Smartphone (please complete the following information):**
|
||||||
- Device: [e.g. iPhone6, Pixel7]
|
- Device: [e.g. iPhone6]
|
||||||
- OS: [e.g. iOS8.1]
|
- OS: [e.g. iOS8.1]
|
||||||
- Browser [e.g. stock browser, firefox, safari]
|
- Browser [e.g. stock browser, safari]
|
||||||
- Version [e.g. 22]
|
- Version [e.g. 22]
|
||||||
|
|
||||||
**Docker** (If applicable)
|
|
||||||
|
|
||||||
Please share the output of the following.
|
|
||||||
- `docker version`
|
|
||||||
- `docker info`
|
|
||||||
- `docker-compose version`
|
|
||||||
|
|
||||||
**Cloud Provider** (If applicable)
|
|
||||||
- [ ] AWS
|
|
||||||
- [ ] GCP
|
|
||||||
- [ ] Azure
|
|
||||||
- [ ] DigitalOcean
|
|
||||||
- [ ] Others
|
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires.
|
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||||
|
|
||||||
Fixes # (issue)
|
Fixes # (issue)
|
||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
@ -11,18 +12,18 @@ Please delete options that are not relevant.
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- [ ] This change requires a documentation update
|
- [ ] This change requires a documentation update
|
||||||
|
|
||||||
## How Has This Been Tested?
|
## How Has This Been Tested?
|
||||||
|
|
||||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
|
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
||||||
|
|
||||||
|
|
||||||
## Checklist:
|
## Checklist:
|
||||||
|
|
||||||
- [ ] My code follows the style guidelines of this project
|
- [ ] My code follows the style guidelines of this project
|
||||||
- [ ] I have performed a self-review of my code
|
- [ ] I have performed a self-review of my own code
|
||||||
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||||
- [ ] I have made corresponding changes to the documentation
|
- [ ] I have made corresponding changes to the documentation
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
|
|
36
.github/workflows/lock.yml
vendored
36
.github/workflows/lock.yml
vendored
|
@ -1,36 +0,0 @@
|
||||||
# We often have cases where users would comment over stale closed Github Issues.
|
|
||||||
# This creates unnecessary noise for the original reporter and makes it harder for triaging.
|
|
||||||
# This action locks the closed threads once it is inactive for over a month.
|
|
||||||
|
|
||||||
name: 'Lock Threads'
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 * * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: lock
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
action:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/lock-threads@v3
|
|
||||||
with:
|
|
||||||
issue-inactive-days: '30'
|
|
||||||
issue-lock-reason: 'resolved'
|
|
||||||
issue-comment: >
|
|
||||||
This issue has been automatically locked since there
|
|
||||||
has not been any recent activity after it was closed.
|
|
||||||
Please open a new issue for related bugs.
|
|
||||||
pr-inactive-days: '30'
|
|
||||||
pr-lock-reason: 'resolved'
|
|
||||||
pr-comment: >
|
|
||||||
This pull request has been automatically locked since there
|
|
||||||
has not been any recent activity after it was closed.
|
|
||||||
Please open a new issue for related bugs.
|
|
46
.github/workflows/nightly_installer.yml
vendored
46
.github/workflows/nightly_installer.yml
vendored
|
@ -1,46 +0,0 @@
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# # Linux nightly installer action
|
|
||||||
# # This action will try to install and setup
|
|
||||||
# # chatwoot on an Ubuntu 20.04 machine using
|
|
||||||
# # the linux installer script.
|
|
||||||
# #
|
|
||||||
# # This is set to run daily at midnight.
|
|
||||||
# #
|
|
||||||
|
|
||||||
name: Run Linux nightly installer
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nightly:
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: get installer
|
|
||||||
run: |
|
|
||||||
wget https://get.chatwoot.app/linux/install.sh
|
|
||||||
chmod +x install.sh
|
|
||||||
#fix for postgtres not starting automatically in gh action env
|
|
||||||
sed -i '/function configure_db() {/a sudo service postgresql start' install.sh
|
|
||||||
|
|
||||||
- name: create input file
|
|
||||||
run: |
|
|
||||||
echo "no" > input
|
|
||||||
echo "yes" >> input
|
|
||||||
|
|
||||||
- name: Run the installer
|
|
||||||
run: |
|
|
||||||
sudo ./install.sh --install < input
|
|
||||||
|
|
||||||
# disabling http verify for now as http
|
|
||||||
# access to port 3000 fails in gh action env
|
|
||||||
# - name: Verify
|
|
||||||
# if: always()
|
|
||||||
# run: |
|
|
||||||
# sudo netstat -ntlp | grep 3000
|
|
||||||
# sudo systemctl restart chatwoot.target
|
|
||||||
# curl http://localhost:3000/api
|
|
||||||
|
|
23
.github/workflows/publish_codespace_image.yml
vendored
23
.github/workflows/publish_codespace_image.yml
vendored
|
@ -1,23 +0,0 @@
|
||||||
name: Publish Codespace Base Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-code-space-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build the Codespace Base Image
|
|
||||||
run: |
|
|
||||||
docker build . -t ghcr.io/chatwoot/chatwoot_codespace:latest -f .devcontainer/Dockerfile.base
|
|
||||||
docker push ghcr.io/chatwoot/chatwoot_codespace:latest
|
|
63
.github/workflows/publish_foss_docker.yml
vendored
63
.github/workflows/publish_foss_docker.yml
vendored
|
@ -1,63 +0,0 @@
|
||||||
# #
|
|
||||||
# # 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
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.DOCKER_TAG }}
|
|
73
.github/workflows/run_foss_spec.yml
vendored
73
.github/workflows/run_foss_spec.yml
vendored
|
@ -1,73 +0,0 @@
|
||||||
# #
|
|
||||||
# # 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
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -39,6 +39,9 @@ public/packs*
|
||||||
*.un~
|
*.un~
|
||||||
.jest-cache
|
.jest-cache
|
||||||
|
|
||||||
|
#VS Code files
|
||||||
|
.vscode
|
||||||
|
|
||||||
# ignore jetbrains IDE files
|
# ignore jetbrains IDE files
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
@ -59,6 +62,4 @@ package-lock.json
|
||||||
test/cypress/videos/*
|
test/cypress/videos/*
|
||||||
|
|
||||||
/config/master.key
|
/config/master.key
|
||||||
/config/*.enc
|
/config/*.enc
|
||||||
|
|
||||||
.vscode/settings.json
|
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
npm run eslint
|
|
||||||
bundle exec rubocop -a
|
|
||||||
git add
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
sh bin/validate_push
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
14.17.4
|
12.16.1
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5"
|
||||||
"arrowParens": "avoid"
|
|
||||||
}
|
}
|
||||||
|
|
66
.rubocop.yml
66
.rubocop.yml
|
@ -11,13 +11,8 @@ Metrics/ClassLength:
|
||||||
Max: 125
|
Max: 125
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/conversation.rb'
|
- 'app/models/conversation.rb'
|
||||||
- 'app/models/contact.rb'
|
|
||||||
- 'app/mailers/conversation_reply_mailer.rb'
|
- 'app/mailers/conversation_reply_mailer.rb'
|
||||||
- 'app/models/message.rb'
|
- '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:
|
RSpec/ExampleLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
|
@ -28,16 +23,13 @@ Style/FrozenStringLiteralComment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Style/SymbolArray:
|
Style/SymbolArray:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Style/OpenStructUse:
|
|
||||||
Enabled: false
|
|
||||||
Style/OptionalBooleanParameter:
|
Style/OptionalBooleanParameter:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/services/email_templates/db_resolver_service.rb'
|
- 'app/services/email_templates/db_resolver_service.rb'
|
||||||
- 'app/dispatchers/dispatcher.rb'
|
- 'app/dispatchers/dispatcher.rb'
|
||||||
Style/GlobalVars:
|
Style/GlobalVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/01_redis.rb'
|
- 'config/initializers/redis.rb'
|
||||||
- 'config/initializers/rack_attack.rb'
|
|
||||||
- 'lib/redis/alfred.rb'
|
- 'lib/redis/alfred.rb'
|
||||||
- 'lib/global_config.rb'
|
- 'lib/global_config.rb'
|
||||||
Style/ClassVars:
|
Style/ClassVars:
|
||||||
|
@ -46,23 +38,12 @@ Style/ClassVars:
|
||||||
Lint/MissingSuper:
|
Lint/MissingSuper:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/drops/base_drop.rb'
|
- 'app/drops/base_drop.rb'
|
||||||
Lint/SymbolConversion:
|
|
||||||
Enabled: false
|
|
||||||
Lint/EmptyBlock:
|
|
||||||
Exclude:
|
|
||||||
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
|
|
||||||
Lint/OrAssignmentToConstant:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/redis/config.rb'
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- spec/**/*
|
- spec/**/*
|
||||||
- '**/routes.rb'
|
- '**/routes.rb'
|
||||||
- 'config/environments/*'
|
- 'config/environments/*'
|
||||||
- db/schema.rb
|
- db/schema.rb
|
||||||
Metrics/ModuleLength:
|
|
||||||
Exclude:
|
|
||||||
- lib/seeders/message_seeder.rb
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||||
|
@ -70,37 +51,18 @@ Rails/ApplicationController:
|
||||||
- 'app/controllers/widget_tests_controller.rb'
|
- 'app/controllers/widget_tests_controller.rb'
|
||||||
- 'app/controllers/widgets_controller.rb'
|
- 'app/controllers/widgets_controller.rb'
|
||||||
- 'app/controllers/platform_controller.rb'
|
- 'app/controllers/platform_controller.rb'
|
||||||
- 'app/controllers/public_controller.rb'
|
|
||||||
- 'app/controllers/survey/responses_controller.rb'
|
|
||||||
Rails/CompactBlank:
|
|
||||||
Enabled: false
|
|
||||||
Rails/EnvironmentVariableAccess:
|
|
||||||
Enabled: false
|
|
||||||
Rails/TimeZoneAssignment:
|
|
||||||
Enabled: false
|
|
||||||
Rails/RedundantPresenceValidationOnBelongsTo:
|
|
||||||
Enabled: false
|
|
||||||
Style/ClassAndModuleChildren:
|
Style/ClassAndModuleChildren:
|
||||||
EnforcedStyle: compact
|
EnforcedStyle: compact
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/application.rb'
|
- 'config/application.rb'
|
||||||
Style/MapToHash:
|
|
||||||
Enabled: false
|
|
||||||
RSpec/NestedGroups:
|
RSpec/NestedGroups:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Max: 4
|
Max: 4
|
||||||
RSpec/MessageSpies:
|
RSpec/MessageSpies:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
RSpec/StubbedMock:
|
|
||||||
Enabled: false
|
|
||||||
RSpec/FactoryBot/SyntaxMethods:
|
|
||||||
Enabled: false
|
|
||||||
Naming/VariableNumber:
|
|
||||||
Enabled: false
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
|
||||||
Rails/CreateTableWithTimestamps:
|
Rails/CreateTableWithTimestamps:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
|
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
|
||||||
|
@ -111,12 +73,10 @@ Style/GuardClause:
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/concerns/auth_helper.rb'
|
- 'app/controllers/concerns/auth_helper.rb'
|
||||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||||
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
|
|
||||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 7
|
Max: 7
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -131,7 +91,6 @@ Rails/ReversibleMigration:
|
||||||
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
||||||
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
||||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||||
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
|
|
||||||
Rails/BulkChangeTable:
|
Rails/BulkChangeTable:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||||
|
@ -142,25 +101,19 @@ Rails/BulkChangeTable:
|
||||||
- 'db/migrate/20170511134418_latlong.rb'
|
- 'db/migrate/20170511134418_latlong.rb'
|
||||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
||||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||||
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
|
Rails/UniqueValidationWithoutIndex:
|
||||||
Rails/UniqueValidationWithoutIndex:
|
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/channel/twitter_profile.rb'
|
- 'app/models/channel/twitter_profile.rb'
|
||||||
- 'app/models/webhook.rb'
|
- 'app/models/webhook.rb'
|
||||||
- 'app/models/contact.rb'
|
- 'app/models/contact.rb'
|
||||||
- 'app/models/integrations/hook.rb'
|
|
||||||
Rails/RenderInline:
|
Rails/RenderInline:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/swagger_controller.rb'
|
- 'app/controllers/swagger_controller.rb'
|
||||||
Performance/CollectionLiteralInLoop:
|
|
||||||
Exclude:
|
|
||||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
|
||||||
RSpec/NamedSubject:
|
RSpec/NamedSubject:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
# we should bring this down
|
# we should bring this down
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 12
|
Max: 12
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
NewCops: enable
|
NewCops: enable
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -175,13 +128,4 @@ AllCops:
|
||||||
- 'tmp/**/*'
|
- 'tmp/**/*'
|
||||||
- 'storage/**/*'
|
- 'storage/**/*'
|
||||||
- 'db/migrate/20200225162150_init_schema.rb'
|
- 'db/migrate/20200225162150_init_schema.rb'
|
||||||
- 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.rb'
|
- 'config/initializers/azure_storage_service_patch.rb'
|
||||||
- 'db/migrate/20210611180221_add_service_name_to_active_storage_blobs.active_storage.rb'
|
|
||||||
- db/migrate/20200309213132_add_account_id_to_agent_bot_inboxes.rb
|
|
||||||
- db/migrate/20200331095710_add_identifier_to_contact.rb
|
|
||||||
- db/migrate/20200429082655_add_medium_to_twilio_sms.rb
|
|
||||||
- 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.4
|
2.7.2
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
const resolve = require('../config/webpack/resolve');
|
|
||||||
|
|
||||||
// Chatwoot's webpack.config.js
|
|
||||||
process.env.NODE_ENV = 'development';
|
|
||||||
const custom = require('../config/webpack/environment');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
stories: [
|
|
||||||
'../stories/**/*.stories.mdx',
|
|
||||||
'../app/javascript/**/*.stories.@(js|jsx|ts|tsx)',
|
|
||||||
],
|
|
||||||
addons: [
|
|
||||||
{
|
|
||||||
name: '@storybook/addon-docs',
|
|
||||||
options: {
|
|
||||||
vueDocgenOptions: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, '../'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'@storybook/addon-links',
|
|
||||||
'@storybook/addon-essentials',
|
|
||||||
],
|
|
||||||
webpackFinal: config => {
|
|
||||||
const newConfig = {
|
|
||||||
...config,
|
|
||||||
resolve: {
|
|
||||||
...config.resolve,
|
|
||||||
modules: custom.resolvedModules.map(i => i.value),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
newConfig.module.rules.push({
|
|
||||||
test: /\.scss$/,
|
|
||||||
use: ['style-loader', 'css-loader', 'sass-loader'],
|
|
||||||
include: path.resolve(__dirname, '../app/javascript'),
|
|
||||||
});
|
|
||||||
|
|
||||||
return newConfig;
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { addDecorator } from '@storybook/vue';
|
|
||||||
import Vue from 'vue';
|
|
||||||
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';
|
|
||||||
|
|
||||||
import '../app/javascript/dashboard/assets/scss/storybook.scss';
|
|
||||||
|
|
||||||
Vue.use(VueI18n);
|
|
||||||
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({
|
|
||||||
locale: 'en',
|
|
||||||
messages: i18n,
|
|
||||||
});
|
|
||||||
|
|
||||||
addDecorator(() => ({
|
|
||||||
template: '<story/>',
|
|
||||||
i18n: i18nConfig,
|
|
||||||
store,
|
|
||||||
beforeCreate: function() {
|
|
||||||
this.$root._i18n = this.$i18n;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const parameters = {
|
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
32
.vscode/extensions.json
vendored
32
.vscode/extensions.json
vendored
|
@ -1,32 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
// Spell check
|
|
||||||
"streetsidesoftware.code-spell-checker",
|
|
||||||
// Better Comments
|
|
||||||
"aaron-bond.better-comments",
|
|
||||||
// Rails Test Runner
|
|
||||||
"davidpallinder.rails-test-runner",
|
|
||||||
// Eslint
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
// Auto Close Tag
|
|
||||||
"formulahendry.auto-close-tag",
|
|
||||||
// Auto Rename Tag
|
|
||||||
"formulahendry.auto-rename-tag",
|
|
||||||
// Hight light colors
|
|
||||||
"naumovs.color-highlight",
|
|
||||||
// GitLens
|
|
||||||
"eamodio.gitlens",
|
|
||||||
// Ruby
|
|
||||||
"rebornix.ruby",
|
|
||||||
// Vue
|
|
||||||
"octref.vetur",
|
|
||||||
// Prettier
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
// Dot Env
|
|
||||||
"mikestead.dotenv",
|
|
||||||
// HTML CSS Support
|
|
||||||
"ecmel.vscode-html-css",
|
|
||||||
// Tailwind CSS Intellisense
|
|
||||||
"bradlc.vscode-tailwindcss",
|
|
||||||
]
|
|
||||||
}
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -1 +0,0 @@
|
||||||
{}
|
|
|
@ -1,128 +0,0 @@
|
||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
|
||||||
and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
||||||
diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
|
||||||
community include:
|
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
* Giving and gracefully accepting constructive feedback
|
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
||||||
and learning from the experience
|
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our standards of
|
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
||||||
or harmful.
|
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit, or reject
|
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
||||||
decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces and also applies when
|
|
||||||
an individual is officially representing the community in public spaces.
|
|
||||||
Examples of representing our community include using an official e-mail address,
|
|
||||||
posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported to the community leaders responsible for enforcement at
|
|
||||||
hello@chatwoot.com.
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
|
||||||
reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in determining
|
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
||||||
unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders, providing
|
|
||||||
clarity around the nature of the violation and an explanation of why the
|
|
||||||
behavior was inappropriate. A public apology may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
|
||||||
interaction with the people involved, including unsolicited interaction with
|
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
|
||||||
like social media. Violating these terms may lead to a temporary or
|
|
||||||
permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
|
||||||
sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
|
||||||
communication with the community for a specified period of time. No public or
|
|
||||||
private interaction with the people involved, including unsolicited interaction
|
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
|
||||||
Violating these terms may lead to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
|
||||||
the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
||||||
version 2.0, available at
|
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
|
||||||
https://www.contributor-covenant.org/translations.
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Contributing to Chatwoot
|
|
||||||
|
|
||||||
Thanks for taking the time to contribute! :tada::+1:
|
|
||||||
|
|
||||||
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions on how to contribute.
|
|
82
Gemfile
82
Gemfile
|
@ -1,10 +1,10 @@
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.0.4'
|
ruby '2.7.2'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
gem 'rails', '~> 6.1', '>= 6.1.6.1'
|
gem 'rails'
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
|
||||||
|
@ -31,24 +31,19 @@ gem 'haikunator'
|
||||||
gem 'liquid'
|
gem 'liquid'
|
||||||
# Parse Markdown to HTML
|
# Parse Markdown to HTML
|
||||||
gem 'commonmarker'
|
gem 'commonmarker'
|
||||||
# Validate Data against JSON Schema
|
|
||||||
gem 'json_schemer'
|
|
||||||
# Rack middleware for blocking & throttling abusive requests
|
|
||||||
gem 'rack-attack'
|
|
||||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
|
||||||
gem 'down', '~> 5.0'
|
|
||||||
|
|
||||||
##-- for active storage --##
|
##-- for active storage --##
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
gem 'azure-storage-blob', require: false
|
||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'image_processing', '~> 1.12.2'
|
gem 'mini_magick'
|
||||||
|
|
||||||
##-- gems for database --#
|
##-- gems for database --#
|
||||||
gem 'groupdate'
|
gem 'groupdate'
|
||||||
gem 'pg'
|
gem 'pg'
|
||||||
gem 'redis'
|
gem 'redis'
|
||||||
gem 'redis-namespace'
|
gem 'redis-namespace'
|
||||||
|
gem 'redis-rack-cache'
|
||||||
# super fast record imports in bulk
|
# super fast record imports in bulk
|
||||||
gem 'activerecord-import'
|
gem 'activerecord-import'
|
||||||
|
|
||||||
|
@ -56,13 +51,12 @@ gem 'activerecord-import'
|
||||||
gem 'dotenv-rails'
|
gem 'dotenv-rails'
|
||||||
gem 'foreman'
|
gem 'foreman'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
gem 'webpacker', '~> 5.4', '>= 5.4.3'
|
gem 'webpacker', '~> 5.x'
|
||||||
# metrics on heroku
|
# metrics on heroku
|
||||||
gem 'barnes'
|
gem 'barnes'
|
||||||
|
|
||||||
##--- gems for authentication & authorization ---##
|
##--- gems for authentication & authorization ---##
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-secure_password', '~> 2.0', git: 'https://github.com/chatwoot/devise-secure_password'
|
|
||||||
gem 'devise_token_auth'
|
gem 'devise_token_auth'
|
||||||
# authorization
|
# authorization
|
||||||
gem 'jwt'
|
gem 'jwt'
|
||||||
|
@ -76,9 +70,9 @@ gem 'wisper', '2.0.0'
|
||||||
|
|
||||||
##--- gems for channels ---##
|
##--- gems for channels ---##
|
||||||
# TODO: bump up gem to 2.0
|
# TODO: bump up gem to 2.0
|
||||||
gem 'facebook-messenger'
|
gem 'facebook-messenger', '1.5.0'
|
||||||
gem 'line-bot-api'
|
gem 'telegram-bot-ruby'
|
||||||
gem 'twilio-ruby', '~> 5.66'
|
gem 'twilio-ruby', '~> 5.32.0'
|
||||||
# twitty will handle subscription of twitter account events
|
# twitty will handle subscription of twitter account events
|
||||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||||
gem 'twitty'
|
gem 'twitty'
|
||||||
|
@ -86,22 +80,17 @@ gem 'twitty'
|
||||||
gem 'koala'
|
gem 'koala'
|
||||||
# slack client
|
# slack client
|
||||||
gem 'slack-ruby-client'
|
gem 'slack-ruby-client'
|
||||||
# for dialogflow integrations
|
|
||||||
gem 'google-cloud-dialogflow'
|
|
||||||
|
|
||||||
##-- apm and error monitoring ---#
|
##--- gems for debugging and error reporting ---##
|
||||||
gem 'ddtrace'
|
# static analysis
|
||||||
gem 'elastic-apm'
|
gem 'brakeman'
|
||||||
gem 'newrelic_rpm'
|
|
||||||
gem 'scout_apm'
|
gem 'scout_apm'
|
||||||
gem 'sentry-rails', '~> 5.3', '>= 5.3.1'
|
gem 'sentry-raven'
|
||||||
gem 'sentry-ruby', '~> 5.3'
|
|
||||||
gem 'sentry-sidekiq', '~> 5.3'
|
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '~> 6.4.0'
|
gem 'sidekiq'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron', '~> 1.3'
|
gem 'sidekiq-cron'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
|
@ -116,30 +105,6 @@ gem 'maxminddb'
|
||||||
# to create db triggers
|
# to create db triggers
|
||||||
gem 'hairtrigger'
|
gem 'hairtrigger'
|
||||||
|
|
||||||
gem 'procore-sift'
|
|
||||||
|
|
||||||
# parse email
|
|
||||||
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'
|
|
||||||
end
|
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'annotate'
|
gem 'annotate'
|
||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
|
@ -147,7 +112,7 @@ group :development do
|
||||||
gem 'web-console'
|
gem 'web-console'
|
||||||
|
|
||||||
# used in swagger build
|
# used in swagger build
|
||||||
gem 'json_refs'
|
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0'
|
||||||
|
|
||||||
# When we want to squash migrations
|
# When we want to squash migrations
|
||||||
gem 'squasher'
|
gem 'squasher'
|
||||||
|
@ -158,31 +123,28 @@ group :test do
|
||||||
gem 'cypress-on-rails', '~> 1.0'
|
gem 'cypress-on-rails', '~> 1.0'
|
||||||
# fast cleaning of database
|
# fast cleaning of database
|
||||||
gem 'database_cleaner'
|
gem 'database_cleaner'
|
||||||
# mock http calls
|
|
||||||
gem 'webmock'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'active_record_query_trace'
|
# locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved
|
||||||
##--- gems for debugging and error reporting ---##
|
gem 'action-cable-testing'
|
||||||
# static analysis
|
|
||||||
gem 'brakeman'
|
|
||||||
gem 'bundle-audit', require: false
|
gem 'bundle-audit', require: false
|
||||||
gem 'byebug', platform: :mri
|
gem 'byebug', platform: :mri
|
||||||
gem 'climate_control'
|
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails'
|
||||||
|
gem 'faker'
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
gem 'mock_redis'
|
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'rspec_junit_formatter'
|
gem 'rspec-rails', '~> 4.0.0.beta2'
|
||||||
gem 'rspec-rails', '~> 5.0.3'
|
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
gem 'rubocop-rspec', require: false
|
gem 'rubocop-rspec', require: false
|
||||||
|
gem 'scss_lint', require: false
|
||||||
gem 'seed_dump'
|
gem 'seed_dump'
|
||||||
gem 'shoulda-matchers'
|
gem 'shoulda-matchers'
|
||||||
gem 'simplecov', '0.17.1', require: false
|
gem 'simplecov', '0.17.1', require: false
|
||||||
gem 'spring'
|
gem 'spring'
|
||||||
gem 'spring-watcher-listen'
|
gem 'spring-watcher-listen'
|
||||||
|
gem 'webmock'
|
||||||
end
|
end
|
||||||
|
|
864
Gemfile.lock
864
Gemfile.lock
File diff suppressed because it is too large
Load diff
8
LICENSE
8
LICENSE
|
@ -1,11 +1,7 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2017-2021 Chatwoot Inc.
|
Copyright (c) 2017-2021 Chatwoot Inc.
|
||||||
|
|
||||||
Portions of this software are licensed as follows:
|
|
||||||
|
|
||||||
* All content that resides under the "enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "enterprise/LICENSE".
|
|
||||||
* All third party components incorporated into the Chatwoot Software are licensed under the original license provided by the owner of the applicable component.
|
|
||||||
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
|
|
88
README.md
88
README.md
|
@ -6,82 +6,79 @@
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
<img alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||||
</a>
|
|
||||||
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
|
||||||
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/80f9e1a7c72d186289ad/maintainability" alt="Maintainability"></a>
|
||||||
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
||||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
||||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a>
|
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a>
|
||||||
|
<img src="https://img.shields.io/github/license/chatwoot/chatwoot" alt="License">
|
||||||
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
|
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
|
||||||
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
|
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
|
||||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
|
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
|
||||||
<a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a>
|
<a href="https://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>
|
</p>
|
||||||
|
|
||||||
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
|
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png" width="100%" alt="Chat dashboard"/>
|
||||||
|
|
||||||
|
Chatwoot is an open-source omnichannel customer support software. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it open-source, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
|
||||||
|
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
### Features
|
||||||
|
|
||||||
## Features
|
Chatwoot gives an integrated view of conversations happening in different communication channels.
|
||||||
|
|
||||||
Chatwoot supports the following conversation channels:
|
It supports the following conversation channels:
|
||||||
|
|
||||||
- **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
|
- **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
|
||||||
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
|
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
|
||||||
- **Instagram**: Connect your Instagram profile and start replying to the direct messages.
|
|
||||||
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
|
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
|
||||||
- **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard.
|
- **Whatsapp**: Connect your Whatsapp business account and manage the conversation in Chatwoot
|
||||||
- **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot.
|
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot
|
||||||
- **Line**: Connect your Line account and manage the conversations in Chatwoot.
|
|
||||||
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot.
|
|
||||||
- **API Channel**: Build custom communication channels using our API channel.
|
- **API Channel**: Build custom communication channels using our API channel.
|
||||||
- **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
|
- **Email (beta)**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
|
||||||
|
|
||||||
And more.
|
|
||||||
|
|
||||||
Other features include:
|
Other features include:
|
||||||
|
|
||||||
- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes.
|
- **Multi-brand inboxes**: Manage multiple brands or pages using a single dashboard.
|
||||||
- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow.
|
- **Private notes**: Inter team communication is possible using private notes in a conversation.
|
||||||
- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox.
|
|
||||||
- **Private notes**: Use @mentions and private notes to communicate internally about a conversation.
|
|
||||||
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
|
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
|
||||||
- **Conversation Labels**: Use conversation labels to create custom workflows.
|
- **Conversation Labels**: Use conversation labelling to create custom workflows.
|
||||||
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
|
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
|
||||||
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email.
|
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot would send an email to the customer under the agent name so that the user can continue the conversation over the email.
|
||||||
- **Multi-lingual support**: Chatwoot supports 10+ languages.
|
- **Multi-lingual support**: Chatwoot supports 10+ languages.
|
||||||
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
||||||
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
||||||
|
|
||||||
## Documentation
|
---
|
||||||
|
|
||||||
Detailed documentation is available at [chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
### Documentation
|
||||||
|
|
||||||
## Translation process
|
Detailed documentation is available at [www.chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
||||||
|
|
||||||
|
### Translation process
|
||||||
|
|
||||||
The translation process for Chatwoot web and mobile app is managed at [https://translate.chatwoot.com](https://translate.chatwoot.com) using Crowdin. Please read the [translation guide](https://www.chatwoot.com/docs/contributing/translating-chatwoot-to-your-language) for contributing to Chatwoot.
|
The translation process for Chatwoot web and mobile app is managed at [https://translate.chatwoot.com](https://translate.chatwoot.com) using Crowdin. Please read the [translation guide](https://www.chatwoot.com/docs/contributing/translating-chatwoot-to-your-language) for contributing to Chatwoot.
|
||||||
|
|
||||||
## Branching model
|
---
|
||||||
|
|
||||||
|
### Branching model
|
||||||
|
|
||||||
We use the [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`.
|
We use the [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`.
|
||||||
If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`.
|
If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`.
|
||||||
|
|
||||||
## Deployment
|
---
|
||||||
|
|
||||||
### Heroku one-click deploy
|
### Deployment
|
||||||
|
|
||||||
|
#### Heroku one-click deploy
|
||||||
|
|
||||||
Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button:
|
Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button:
|
||||||
|
|
||||||
|
@ -89,34 +86,17 @@ Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button
|
||||||
|
|
||||||
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
||||||
|
|
||||||
|
#### Other deployment options
|
||||||
|
|
||||||
### DigitalOcean 1-Click Kubernetes deployment
|
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
|
||||||
|
|
||||||
Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app.
|
---
|
||||||
|
|
||||||
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
### Contributors ✨
|
||||||
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
### Other deployment options
|
|
||||||
|
|
||||||
For other supported options, checkout our [deployment page](https://chatwoot.com/deploy).
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
|
|
||||||
|
|
||||||
|
|
||||||
## Community? Questions? Support ?
|
|
||||||
|
|
||||||
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
|
|
||||||
|
|
||||||
|
|
||||||
## Contributors ✨
|
|
||||||
|
|
||||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||||
|
|
||||||
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
|
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
|
||||||
|
|
||||||
|
|
||||||
*Chatwoot* © 2017-2022, Chatwoot Inc - Released under the MIT License.
|
*Chatwoot* © 2017-2021, Chatwoot Inc - Released under the MIT License.
|
||||||
|
|
56
SECURITY.md
56
SECURITY.md
|
@ -1,56 +1,8 @@
|
||||||
Chatwoot is looking forward to working with security researchers worldwide to keep Chatwoot and our users safe. If you have found an issue in our systems/applications, please reach out to us.
|
# Security Policy
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose). This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
|
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose).
|
||||||
|
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 hello@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 | ❌ |
|
|
||||||
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
You can learn more about our triaging process [here](https://www.chatwoot.com/docs/contributing-guide/security-reports).
|
|
||||||
|
|
||||||
## Non-Qualifying Vulnerabilities
|
|
||||||
|
|
||||||
We consider the following out of scope, though there may be exceptions.
|
|
||||||
|
|
||||||
- Missing HTTP security headers
|
|
||||||
- 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
|
|
||||||
|
|
||||||
If you are unsure about the scope, please create a [report](https://huntr.dev/repos/chatwoot/chatwoot/).
|
|
||||||
|
|
||||||
|
|
||||||
## Thanks
|
|
||||||
|
|
||||||
Thank you for keeping Chatwoot and our users safe. 🙇
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
2.2.0
|
|
|
@ -1 +0,0 @@
|
||||||
2.1.0
|
|
25
app.json
25
app.json
|
@ -28,37 +28,18 @@
|
||||||
"FRONTEND_URL": {
|
"FRONTEND_URL": {
|
||||||
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
||||||
"value": "https://CHANGE.herokuapp.com"
|
"value": "https://CHANGE.herokuapp.com"
|
||||||
},
|
|
||||||
"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": {
|
"formation": {
|
||||||
"web": {
|
"web": {
|
||||||
"quantity": 1,
|
"quantity": 1
|
||||||
"size": "basic"
|
|
||||||
},
|
},
|
||||||
"worker": {
|
"worker": {
|
||||||
"quantity": 1,
|
"quantity": 1
|
||||||
"size": "basic"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stack": "heroku-20",
|
|
||||||
"image": "heroku/ruby",
|
"image": "heroku/ruby",
|
||||||
"addons": [
|
"addons": [ "heroku-redis", "heroku-postgresql"],
|
||||||
{
|
|
||||||
"plan": "heroku-redis:mini"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"plan": "heroku-postgresql:mini"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stack": "heroku-20",
|
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
{
|
{
|
||||||
"url": "heroku/ruby"
|
"url": "heroku/ruby"
|
||||||
|
|
|
@ -1,20 +1,10 @@
|
||||||
# retain_original_contact_name: false / true
|
|
||||||
# In case of setUser we want to update the name of the identified contact,
|
|
||||||
# which is the default behaviour
|
|
||||||
#
|
|
||||||
# But, In case of contact merge during prechat form contact update.
|
|
||||||
# We don't want to update the name of the identified original contact.
|
|
||||||
|
|
||||||
class ContactIdentifyAction
|
class ContactIdentifyAction
|
||||||
pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }]
|
pattr_initialize [:contact!, :params!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@attributes_to_update = [:identifier, :name, :email, :phone_number]
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
merge_if_existing_identified_contact
|
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||||
merge_if_existing_email_contact
|
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||||
merge_if_existing_phone_number_contact
|
|
||||||
update_contact
|
update_contact
|
||||||
end
|
end
|
||||||
@contact
|
@contact
|
||||||
|
@ -26,106 +16,35 @@ class ContactIdentifyAction
|
||||||
@account ||= @contact.account
|
@account ||= @contact.account
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_if_existing_identified_contact
|
|
||||||
return unless merge_contacts?(existing_identified_contact, :identifier)
|
|
||||||
|
|
||||||
process_contact_merge(existing_identified_contact)
|
|
||||||
end
|
|
||||||
|
|
||||||
def merge_if_existing_email_contact
|
|
||||||
return unless merge_contacts?(existing_email_contact, :email)
|
|
||||||
|
|
||||||
process_contact_merge(existing_email_contact)
|
|
||||||
end
|
|
||||||
|
|
||||||
def merge_if_existing_phone_number_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
|
def existing_identified_contact
|
||||||
return if params[:identifier].blank?
|
return if params[:identifier].blank?
|
||||||
|
|
||||||
@existing_identified_contact ||= account.contacts.find_by(identifier: params[:identifier])
|
@existing_identified_contact ||= Contact.where(account_id: account.id).find_by(identifier: params[:identifier])
|
||||||
end
|
end
|
||||||
|
|
||||||
def existing_email_contact
|
def existing_email_contact
|
||||||
return if params[:email].blank?
|
return if params[:email].blank?
|
||||||
|
|
||||||
@existing_email_contact ||= account.contacts.find_by(email: params[:email])
|
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
||||||
end
|
end
|
||||||
|
|
||||||
def existing_phone_number_contact
|
def merge_contacts?(existing_contact, _contact)
|
||||||
return if params[:phone_number].blank?
|
existing_contact && existing_contact.id != @contact.id
|
||||||
|
|
||||||
@existing_phone_number_contact ||= account.contacts.find_by(phone_number: params[:phone_number])
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
def update_contact
|
def update_contact
|
||||||
@contact.attributes = params.slice(*@attributes_to_update).reject do |_k, v|
|
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||||
v.blank?
|
|
||||||
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes })
|
|
||||||
# blank identifier or email will throw unique index error
|
# blank identifier or email will throw unique index error
|
||||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||||
@contact.discard_invalid_attrs if discard_invalid_attrs
|
@contact.update!(params.slice(:name, :email, :identifier).reject { |_k, v| v.blank? }.merge({ custom_attributes: custom_attributes }))
|
||||||
@contact.save!
|
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_contact(base_contact, merge_contact)
|
def merge_contact(base_contact, merge_contact)
|
||||||
return base_contact if base_contact.id == merge_contact.id
|
|
||||||
|
|
||||||
ContactMergeAction.new(
|
ContactMergeAction.new(
|
||||||
account: account,
|
account: account,
|
||||||
base_contact: base_contact,
|
base_contact: base_contact,
|
||||||
mergee_contact: merge_contact
|
mergee_contact: merge_contact
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def 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
|
|
||||||
return @contact.additional_attributes if params[:additional_attributes].blank?
|
|
||||||
|
|
||||||
(@contact.additional_attributes || {}).deep_merge(params[:additional_attributes].stringify_keys)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
class ContactMergeAction
|
class ContactMergeAction
|
||||||
include Events::Types
|
|
||||||
pattr_initialize [:account!, :base_contact!, :mergee_contact!]
|
pattr_initialize [:account!, :base_contact!, :mergee_contact!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
# This case happens when an agent updates a contact email in dashboard,
|
|
||||||
# while the contact also update his email via email collect box
|
|
||||||
return @base_contact if base_contact.id == mergee_contact.id
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
validate_contacts
|
validate_contacts
|
||||||
merge_conversations
|
merge_conversations
|
||||||
merge_messages
|
merge_messages
|
||||||
merge_contact_inboxes
|
merge_contact_inboxes
|
||||||
merge_and_remove_mergee_contact
|
remove_mergee_contact
|
||||||
end
|
end
|
||||||
@base_contact
|
@base_contact
|
||||||
end
|
end
|
||||||
|
@ -41,17 +36,7 @@ class ContactMergeAction
|
||||||
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_and_remove_mergee_contact
|
def remove_mergee_contact
|
||||||
mergable_attribute_keys = %w[identifier name email phone_number additional_attributes custom_attributes]
|
|
||||||
base_contact_attributes = base_contact.attributes.slice(*mergable_attribute_keys).compact_blank
|
|
||||||
mergee_contact_attributes = mergee_contact.attributes.slice(*mergable_attribute_keys).compact_blank
|
|
||||||
|
|
||||||
# attributes in base contact are given preference
|
|
||||||
merged_attributes = mergee_contact_attributes.deep_merge(base_contact_attributes)
|
|
||||||
|
|
||||||
@mergee_contact.destroy!
|
@mergee_contact.destroy!
|
||||||
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact,
|
|
||||||
tokens: [@base_contact.contact_inboxes.filter_map(&:pubsub_token)])
|
|
||||||
@base_contact.update!(merged_attributes)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,7 +43,7 @@ $woot-logo-padding: $space-large $space-two;
|
||||||
// Colors
|
// Colors
|
||||||
$color-woot: #1f93ff;
|
$color-woot: #1f93ff;
|
||||||
$color-gray: #6e6f73;
|
$color-gray: #6e6f73;
|
||||||
$color-light-gray: #747677;
|
$color-light-gray: #999a9b;
|
||||||
$color-border: #e0e6ed;
|
$color-border: #e0e6ed;
|
||||||
$color-border-light: #f0f4f5;
|
$color-border-light: #f0f4f5;
|
||||||
$color-background: #f4f6fb;
|
$color-background: #f4f6fb;
|
||||||
|
|
28
app/bot/facebook_bot.rb
Normal file
28
app/bot/facebook_bot.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
require 'facebook/messenger'
|
||||||
|
|
||||||
|
class FacebookBot
|
||||||
|
include Facebook::Messenger
|
||||||
|
|
||||||
|
Bot.on :message do |message|
|
||||||
|
Rails.logger.info "MESSAGE_RECIEVED #{message}"
|
||||||
|
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||||
|
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
Bot.on :delivery do |delivery|
|
||||||
|
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
|
||||||
|
# delivery.sender # => { 'id' => '1008372609250235' }
|
||||||
|
# delivery.recipient # => { 'id' => '2015573629214912' }
|
||||||
|
# delivery.at # => 2016-04-22 21:30:36 +0200
|
||||||
|
# delivery.seq # => 37
|
||||||
|
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
|
||||||
|
updater.perform
|
||||||
|
Rails.logger.info "Human was online at #{delivery.at}"
|
||||||
|
end
|
||||||
|
|
||||||
|
Bot.on :message_echo do |message|
|
||||||
|
Rails.logger.info "MESSAGE_ECHO #{message}"
|
||||||
|
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||||
|
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class AccountBuilder
|
class AccountBuilder
|
||||||
include CustomExceptions::Account
|
include CustomExceptions::Account
|
||||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
|
pattr_initialize [:account_name!, :email!, :confirmed!, :user, :user_full_name, :user_password]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
if @user.nil?
|
if @user.nil?
|
||||||
|
@ -61,11 +61,12 @@ class AccountBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_user
|
def create_user
|
||||||
|
password = user_password || SecureRandom.alphanumeric(12)
|
||||||
|
|
||||||
@user = User.new(email: @email,
|
@user = User.new(email: @email,
|
||||||
password: user_password,
|
password: password,
|
||||||
password_confirmation: user_password,
|
password_confirmation: password,
|
||||||
name: @user_full_name)
|
name: @user_full_name)
|
||||||
@user.type = 'SuperAdmin' if @super_admin
|
|
||||||
@user.confirm if @confirmed
|
@user.confirm if @confirmed
|
||||||
@user.save!
|
@user.save!
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
class Campaigns::CampaignConversationBuilder
|
|
||||||
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes, :custom_attributes]
|
|
||||||
|
|
||||||
def perform
|
|
||||||
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
|
||||||
@campaign = @contact_inbox.inbox.campaigns.find_by!(display_id: campaign_display_id)
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
@contact_inbox.lock!
|
|
||||||
|
|
||||||
# We won't send campaigns if a conversation is already 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,
|
|
||||||
campaign_id: @campaign.id
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation_params
|
|
||||||
{
|
|
||||||
account_id: @campaign.account_id,
|
|
||||||
inbox_id: @contact_inbox.inbox_id,
|
|
||||||
contact_id: @contact_inbox.contact_id,
|
|
||||||
contact_inbox_id: @contact_inbox.id,
|
|
||||||
campaign_id: @campaign.id,
|
|
||||||
additional_attributes: conversation_additional_attributes,
|
|
||||||
custom_attributes: custom_attributes || {}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
61
app/builders/contact_builder.rb
Normal file
61
app/builders/contact_builder.rb
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
class ContactBuilder
|
||||||
|
pattr_initialize [:source_id!, :inbox!, :contact_attributes!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
|
||||||
|
return contact_inbox if contact_inbox
|
||||||
|
|
||||||
|
build_contact_inbox
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= inbox.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_contact_inbox(contact)
|
||||||
|
::ContactInbox.create!(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: inbox.id,
|
||||||
|
source_id: source_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_contact_avatar(contact)
|
||||||
|
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_contact
|
||||||
|
account.contacts.create!(
|
||||||
|
name: contact_attributes[:name],
|
||||||
|
phone_number: contact_attributes[:phone_number],
|
||||||
|
email: contact_attributes[:email],
|
||||||
|
identifier: contact_attributes[:identifier],
|
||||||
|
additional_attributes: contact_attributes[:additional_attributes]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_contact
|
||||||
|
contact = nil
|
||||||
|
|
||||||
|
contact = account.contacts.find_by(identifier: contact_attributes[:identifier]) if contact_attributes[:identifier].present?
|
||||||
|
|
||||||
|
contact ||= account.contacts.find_by(email: contact_attributes[:email]) if contact_attributes[:email].present?
|
||||||
|
|
||||||
|
contact ||= account.contacts.find_by(phone_number: contact_attributes[:phone_number]) if contact_attributes[:phone_number].present?
|
||||||
|
|
||||||
|
contact
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_contact_inbox
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
contact = find_contact || create_contact
|
||||||
|
contact_inbox = create_contact_inbox(contact)
|
||||||
|
update_contact_avatar(contact)
|
||||||
|
contact_inbox
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.info e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,54 +1,27 @@
|
||||||
# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned.
|
|
||||||
# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided.
|
|
||||||
|
|
||||||
class ContactInboxBuilder
|
class ContactInboxBuilder
|
||||||
pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
|
pattr_initialize [:contact_id!, :inbox_id!, :source_id]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@source_id ||= generate_source_id
|
@contact = Contact.find(contact_id)
|
||||||
create_contact_inbox if source_id.present?
|
@inbox = @contact.account.inboxes.find(inbox_id)
|
||||||
|
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api'].include? @inbox.channel_type
|
||||||
|
|
||||||
|
source_id = @source_id || generate_source_id
|
||||||
|
create_contact_inbox(source_id) if source_id.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_source_id
|
def generate_source_id
|
||||||
case @inbox.channel_type
|
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
|
||||||
when 'Channel::TwilioSms'
|
return @contact.email if @inbox.channel_type == 'Channel::Email'
|
||||||
twilio_source_id
|
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
|
||||||
when 'Channel::Whatsapp'
|
|
||||||
wa_source_id
|
|
||||||
when 'Channel::Email'
|
|
||||||
email_source_id
|
|
||||||
when 'Channel::Sms'
|
|
||||||
phone_source_id
|
|
||||||
when 'Channel::Api', 'Channel::WebWidget'
|
|
||||||
SecureRandom.uuid
|
|
||||||
else
|
|
||||||
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def email_source_id
|
nil
|
||||||
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
|
|
||||||
|
|
||||||
@contact.email
|
|
||||||
end
|
|
||||||
|
|
||||||
def phone_source_id
|
|
||||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
|
||||||
|
|
||||||
@contact.phone_number
|
|
||||||
end
|
|
||||||
|
|
||||||
def wa_source_id
|
|
||||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
|
||||||
|
|
||||||
# whatsapp doesn't want the + in e164 format
|
|
||||||
@contact.phone_number.delete('+').to_s
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def twilio_source_id
|
def twilio_source_id
|
||||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
return unless @contact.phone_number
|
||||||
|
|
||||||
case @inbox.channel.medium
|
case @inbox.channel.medium
|
||||||
when 'sms'
|
when 'sms'
|
||||||
|
@ -58,11 +31,11 @@ class ContactInboxBuilder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox
|
def create_contact_inbox(source_id)
|
||||||
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
::ContactInbox.find_or_create_by!(
|
||||||
contact_id: @contact.id,
|
contact_id: @contact.id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
source_id: @source_id
|
source_id: source_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
# This Builder will create a contact and contact inbox with specified attributes.
|
|
||||||
# If an existing identified contact exisits, it will be returned.
|
|
||||||
# for contact inbox logic it uses the contact inbox builder
|
|
||||||
|
|
||||||
class ContactInboxWithContactBuilder
|
|
||||||
pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified]
|
|
||||||
|
|
||||||
def perform
|
|
||||||
find_or_create_contact_and_contact_inbox
|
|
||||||
# in case of race conditions where contact is created by another thread
|
|
||||||
# we will try to find the contact and create a contact inbox
|
|
||||||
rescue ActiveRecord::RecordNotUnique
|
|
||||||
find_or_create_contact_and_contact_inbox
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_or_create_contact_and_contact_inbox
|
|
||||||
@contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present?
|
|
||||||
return @contact_inbox if @contact_inbox
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction(requires_new: true) do
|
|
||||||
build_contact_with_contact_inbox
|
|
||||||
update_contact_avatar(@contact) unless @contact.avatar.attached?
|
|
||||||
@contact_inbox
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def build_contact_with_contact_inbox
|
|
||||||
@contact = find_contact || create_contact
|
|
||||||
@contact_inbox = create_contact_inbox
|
|
||||||
end
|
|
||||||
|
|
||||||
def account
|
|
||||||
@account ||= inbox.account
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_contact_inbox
|
|
||||||
ContactInboxBuilder.new(
|
|
||||||
contact: @contact,
|
|
||||||
inbox: @inbox,
|
|
||||||
source_id: @source_id,
|
|
||||||
hmac_verified: hmac_verified
|
|
||||||
).perform
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_contact_avatar(contact)
|
|
||||||
::Avatar::AvatarFromUrlJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_contact
|
|
||||||
account.contacts.create!(
|
|
||||||
name: contact_attributes[:name] || ::Haikunator.haikunate(1000),
|
|
||||||
phone_number: contact_attributes[:phone_number],
|
|
||||||
email: contact_attributes[:email],
|
|
||||||
identifier: contact_attributes[:identifier],
|
|
||||||
additional_attributes: contact_attributes[:additional_attributes],
|
|
||||||
custom_attributes: contact_attributes[:custom_attributes]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_contact
|
|
||||||
contact = find_contact_by_identifier(contact_attributes[:identifier])
|
|
||||||
contact ||= find_contact_by_email(contact_attributes[:email])
|
|
||||||
contact ||= find_contact_by_phone_number(contact_attributes[:phone_number])
|
|
||||||
contact
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_contact_by_identifier(identifier)
|
|
||||||
return if identifier.blank?
|
|
||||||
|
|
||||||
account.contacts.find_by(identifier: identifier)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_contact_by_email(email)
|
|
||||||
return if email.blank?
|
|
||||||
|
|
||||||
account.contacts.find_by(email: email.downcase)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_contact_by_phone_number(phone_number)
|
|
||||||
return if phone_number.blank?
|
|
||||||
|
|
||||||
account.contacts.find_by(phone_number: phone_number)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,40 +0,0 @@
|
||||||
class ConversationBuilder
|
|
||||||
pattr_initialize [:params!, :contact_inbox!]
|
|
||||||
|
|
||||||
def perform
|
|
||||||
look_up_exising_conversation || create_new_conversation
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def look_up_exising_conversation
|
|
||||||
return unless @contact_inbox.inbox.lock_to_single_conversation?
|
|
||||||
|
|
||||||
@contact_inbox.conversations.last
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_new_conversation
|
|
||||||
::Conversation.create!(conversation_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation_params
|
|
||||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
|
||||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
|
||||||
status = params[:status].present? ? { status: params[:status] } : {}
|
|
||||||
|
|
||||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
|
||||||
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
|
||||||
# status = { status: 'pending' } if status[:status] == 'bot'
|
|
||||||
{
|
|
||||||
account_id: @contact_inbox.inbox.account_id,
|
|
||||||
inbox_id: @contact_inbox.inbox_id,
|
|
||||||
contact_id: @contact_inbox.contact_id,
|
|
||||||
contact_inbox_id: @contact_inbox.id,
|
|
||||||
additional_attributes: additional_attributes,
|
|
||||||
custom_attributes: custom_attributes,
|
|
||||||
snoozed_until: params[:snoozed_until],
|
|
||||||
assignee_id: params[:assignee_id],
|
|
||||||
team_id: params[:team_id]
|
|
||||||
}.merge(status)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,28 +0,0 @@
|
||||||
class CsatSurveys::ResponseBuilder
|
|
||||||
pattr_initialize [:message]
|
|
||||||
|
|
||||||
def perform
|
|
||||||
raise 'Invalid Message' unless message.input_csat?
|
|
||||||
|
|
||||||
conversation = message.conversation
|
|
||||||
rating = message.content_attributes.dig('submitted_values', 'csat_survey_response', 'rating')
|
|
||||||
feedback_message = message.content_attributes.dig('submitted_values', 'csat_survey_response', 'feedback_message')
|
|
||||||
|
|
||||||
return if rating.blank?
|
|
||||||
|
|
||||||
process_csat_response(conversation, rating, feedback_message)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def process_csat_response(conversation, rating, feedback_message)
|
|
||||||
csat_survey_response = message.csat_survey_response || CsatSurveyResponse.new(
|
|
||||||
message_id: message.id, account_id: message.account_id, conversation_id: message.conversation_id,
|
|
||||||
contact_id: conversation.contact_id, assigned_agent: conversation.assignee
|
|
||||||
)
|
|
||||||
csat_survey_response.rating = rating
|
|
||||||
csat_survey_response.feedback_message = feedback_message
|
|
||||||
csat_survey_response.save!
|
|
||||||
csat_survey_response
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -4,62 +4,90 @@
|
||||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||||
|
|
||||||
class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
class Messages::Facebook::MessageBuilder
|
||||||
attr_reader :response
|
attr_reader :response
|
||||||
|
|
||||||
def initialize(response, inbox, outgoing_echo: false)
|
def initialize(response, inbox, outgoing_echo: false)
|
||||||
super()
|
|
||||||
@response = response
|
@response = response
|
||||||
@inbox = inbox
|
@inbox = inbox
|
||||||
@outgoing_echo = outgoing_echo
|
@outgoing_echo = outgoing_echo
|
||||||
@sender_id = (@outgoing_echo ? @response.recipient_id : @response.sender_id)
|
@sender_id = (@outgoing_echo ? @response.recipient_id : @response.sender_id)
|
||||||
@message_type = (@outgoing_echo ? :outgoing : :incoming)
|
@message_type = (@outgoing_echo ? :outgoing : :incoming)
|
||||||
@attachments = (@response.attachments || [])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
# This channel might require reauthorization, may be owner might have changed the fb password
|
|
||||||
return if @inbox.channel.reauthorization_required?
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
build_contact_inbox
|
build_contact
|
||||||
build_message
|
build_message
|
||||||
end
|
end
|
||||||
rescue Koala::Facebook::AuthenticationError
|
|
||||||
@inbox.channel.authorization_error!
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
Raven.capture_exception(e)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_contact_inbox
|
def contact
|
||||||
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
|
||||||
source_id: @sender_id,
|
end
|
||||||
inbox: @inbox,
|
|
||||||
contact_attributes: contact_params
|
def build_contact
|
||||||
).perform
|
return if contact.present?
|
||||||
|
|
||||||
|
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||||
|
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
|
||||||
|
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_message
|
def build_message
|
||||||
@message = conversation.messages.create!(message_params)
|
@message = conversation.messages.create!(message_params)
|
||||||
|
(response.attachments || []).each do |attachment|
|
||||||
@attachments.each do |attachment|
|
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||||
process_attachment(attachment)
|
attachment_obj.save!
|
||||||
|
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attach_file(attachment, file_url)
|
||||||
|
file_resource = LocalResource.new(file_url)
|
||||||
|
attachment.file.attach(io: file_resource.file, filename: file_resource.filename, content_type: file_resource.encoding)
|
||||||
|
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||||
|
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_conversation
|
def build_conversation
|
||||||
|
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
|
||||||
Conversation.create!(conversation_params.merge(
|
Conversation.create!(conversation_params.merge(
|
||||||
contact_inbox_id: @contact_inbox.id
|
contact_inbox_id: @contact_inbox.id
|
||||||
))
|
))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attachment_params(attachment)
|
||||||
|
file_type = attachment['type'].to_sym
|
||||||
|
params = { file_type: file_type, account_id: @message.account_id }
|
||||||
|
|
||||||
|
if [:image, :file, :audio, :video].include? file_type
|
||||||
|
params.merge!(file_type_params(attachment))
|
||||||
|
elsif file_type == :location
|
||||||
|
params.merge!(location_params(attachment))
|
||||||
|
elsif file_type == :fallback
|
||||||
|
params.merge!(fallback_params(attachment))
|
||||||
|
end
|
||||||
|
|
||||||
|
params
|
||||||
|
end
|
||||||
|
|
||||||
|
def file_type_params(attachment)
|
||||||
|
{
|
||||||
|
external_url: attachment['payload']['url'],
|
||||||
|
remote_file_url: attachment['payload']['url']
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def location_params(attachment)
|
def location_params(attachment)
|
||||||
lat = attachment['payload']['coordinates']['lat']
|
lat = attachment['payload']['coordinates']['lat']
|
||||||
long = attachment['payload']['coordinates']['long']
|
long = attachment['payload']['coordinates']['long']
|
||||||
|
@ -82,7 +110,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
{
|
{
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
contact_id: @contact_inbox.contact_id
|
contact_id: contact.id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -93,15 +121,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
message_type: @message_type,
|
message_type: @message_type,
|
||||||
content: response.content,
|
content: response.content,
|
||||||
source_id: response.identifier,
|
source_id: response.identifier,
|
||||||
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
sender: @outgoing_echo ? nil : contact
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_contact_params_result(result)
|
|
||||||
{
|
|
||||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
|
||||||
account_id: @inbox.account_id,
|
|
||||||
avatar_url: result['profile_pic']
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -109,18 +129,14 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
begin
|
begin
|
||||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||||
result = k.get_object(@sender_id) || {}
|
result = k.get_object(@sender_id) || {}
|
||||||
rescue Koala::Facebook::AuthenticationError
|
|
||||||
@inbox.channel.authorization_error!
|
|
||||||
raise
|
|
||||||
rescue Koala::Facebook::ClientError => e
|
|
||||||
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
|
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
result = {}
|
result = {}
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
Raven.capture_exception(e)
|
||||||
end
|
end
|
||||||
process_contact_params_result(result)
|
{
|
||||||
|
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||||
|
account_id: @inbox.account_id,
|
||||||
|
remote_avatar_url: result['profile_pic'] || ''
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
|
||||||
# Assumptions
|
|
||||||
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
|
||||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
|
||||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
|
||||||
|
|
||||||
class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|
||||||
attr_reader :messaging
|
|
||||||
|
|
||||||
def initialize(messaging, inbox, outgoing_echo: false)
|
|
||||||
super()
|
|
||||||
@messaging = messaging
|
|
||||||
@inbox = inbox
|
|
||||||
@outgoing_echo = outgoing_echo
|
|
||||||
end
|
|
||||||
|
|
||||||
def perform
|
|
||||||
return if @inbox.channel.reauthorization_required?
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
build_message
|
|
||||||
end
|
|
||||||
rescue Koala::Facebook::AuthenticationError
|
|
||||||
@inbox.channel.authorization_error!
|
|
||||||
raise
|
|
||||||
rescue StandardError => e
|
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def attachments
|
|
||||||
@messaging[:message][:attachments] || {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_type
|
|
||||||
@outgoing_echo ? :outgoing : :incoming
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_identifier
|
|
||||||
message[:mid]
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_source_id
|
|
||||||
@outgoing_echo ? recipient_id : sender_id
|
|
||||||
end
|
|
||||||
|
|
||||||
def sender_id
|
|
||||||
@messaging[:sender][:id]
|
|
||||||
end
|
|
||||||
|
|
||||||
def recipient_id
|
|
||||||
@messaging[:recipient][:id]
|
|
||||||
end
|
|
||||||
|
|
||||||
def message
|
|
||||||
@messaging[:message]
|
|
||||||
end
|
|
||||||
|
|
||||||
def contact
|
|
||||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: message_source_id)&.contact
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation
|
|
||||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_content
|
|
||||||
@messaging[:message][:text]
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_message
|
|
||||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
|
||||||
return if message_content.blank? && all_unsupported_files?
|
|
||||||
|
|
||||||
@message = conversation.messages.create!(message_params)
|
|
||||||
|
|
||||||
attachments.each do |attachment|
|
|
||||||
process_attachment(attachment)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_conversation
|
|
||||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
|
|
||||||
Conversation.create!(conversation_params.merge(
|
|
||||||
contact_inbox_id: @contact_inbox.id
|
|
||||||
))
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation_params
|
|
||||||
{
|
|
||||||
account_id: @inbox.account_id,
|
|
||||||
inbox_id: @inbox.id,
|
|
||||||
contact_id: contact.id,
|
|
||||||
additional_attributes: {
|
|
||||||
type: 'instagram_direct_message'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_params
|
|
||||||
{
|
|
||||||
account_id: conversation.account_id,
|
|
||||||
inbox_id: conversation.inbox_id,
|
|
||||||
message_type: message_type,
|
|
||||||
source_id: message_identifier,
|
|
||||||
content: message_content,
|
|
||||||
sender: @outgoing_echo ? nil : contact
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def already_sent_from_chatwoot?
|
|
||||||
cw_message = conversation.messages.where(
|
|
||||||
source_id: @messaging[:message][:mid]
|
|
||||||
).first
|
|
||||||
|
|
||||||
cw_message.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def all_unsupported_files?
|
|
||||||
return if attachments.empty?
|
|
||||||
|
|
||||||
attachments_type = attachments.pluck(:type).uniq.first
|
|
||||||
unsupported_file_type?(attachments_type)
|
|
||||||
end
|
|
||||||
|
|
||||||
### Sample response
|
|
||||||
# {
|
|
||||||
# "object": "instagram",
|
|
||||||
# "entry": [
|
|
||||||
# {
|
|
||||||
# "id": "<IGID>",// ig id of the business
|
|
||||||
# "time": 1569262486134,
|
|
||||||
# "messaging": [
|
|
||||||
# {
|
|
||||||
# "sender": {
|
|
||||||
# "id": "<IGSID>"
|
|
||||||
# },
|
|
||||||
# "recipient": {
|
|
||||||
# "id": "<IGID>"
|
|
||||||
# },
|
|
||||||
# "timestamp": 1569262485349,
|
|
||||||
# "message": {
|
|
||||||
# "mid": "<MESSAGE_ID>",
|
|
||||||
# "text": "<MESSAGE_CONTENT>"
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
# ],
|
|
||||||
# }
|
|
||||||
end
|
|
|
@ -8,53 +8,28 @@ class Messages::MessageBuilder
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
@user = user
|
@user = user
|
||||||
@message_type = params[:message_type] || 'outgoing'
|
@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)
|
|
||||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||||
|
@attachments = params[:attachments]
|
||||||
|
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@message = @conversation.messages.build(message_params)
|
@message = @conversation.messages.build(message_params)
|
||||||
process_attachments
|
if @attachments.present?
|
||||||
process_emails
|
@attachments.each do |uploaded_attachment|
|
||||||
@message.save!
|
attachment = @message.attachments.new(
|
||||||
|
account_id: @message.account_id,
|
||||||
|
file_type: file_type(uploaded_attachment&.content_type)
|
||||||
|
)
|
||||||
|
attachment.file.attach(uploaded_attachment)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@message.save
|
||||||
@message
|
@message
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def process_attachments
|
|
||||||
return if @attachments.blank?
|
|
||||||
|
|
||||||
@attachments.each do |uploaded_attachment|
|
|
||||||
attachment = @message.attachments.build(
|
|
||||||
account_id: @message.account_id,
|
|
||||||
file: uploaded_attachment
|
|
||||||
)
|
|
||||||
|
|
||||||
attachment.file_type = if uploaded_attachment.is_a?(String)
|
|
||||||
file_type_by_signed_id(
|
|
||||||
uploaded_attachment
|
|
||||||
)
|
|
||||||
else
|
|
||||||
file_type(uploaded_attachment&.content_type)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_emails
|
|
||||||
return unless @conversation.inbox&.inbox_type == 'Email'
|
|
||||||
|
|
||||||
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails]
|
|
||||||
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails]
|
|
||||||
|
|
||||||
@message.content_attributes[:cc_emails] = cc_emails
|
|
||||||
@message.content_attributes[:bcc_emails] = bcc_emails
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_type
|
def message_type
|
||||||
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
||||||
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
||||||
|
@ -64,29 +39,7 @@ class Messages::MessageBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def sender
|
def sender
|
||||||
message_type == 'outgoing' ? (message_sender || @user) : @conversation.contact
|
message_type == 'outgoing' ? @user : @conversation.contact
|
||||||
end
|
|
||||||
|
|
||||||
def external_created_at
|
|
||||||
@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'
|
|
||||||
|
|
||||||
AgentBot.where(account_id: [nil, @conversation.account.id]).find_by(id: @params[:sender_id])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_params
|
def message_params
|
||||||
|
@ -101,6 +54,6 @@ class Messages::MessageBuilder
|
||||||
items: @items,
|
items: @items,
|
||||||
in_reply_to: @in_reply_to,
|
in_reply_to: @in_reply_to,
|
||||||
echo_id: @params[:echo_id]
|
echo_id: @params[:echo_id]
|
||||||
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
class Messages::Messenger::MessageBuilder
|
|
||||||
include ::FileTypeHelper
|
|
||||||
|
|
||||||
def process_attachment(attachment)
|
|
||||||
# This check handles very rare case if there are multiple files to attach with only one usupported file
|
|
||||||
return if unsupported_file_type?(attachment['type'])
|
|
||||||
|
|
||||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
|
||||||
attachment_obj.save!
|
|
||||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
|
||||||
fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention'
|
|
||||||
update_attachment_file_type(attachment_obj)
|
|
||||||
end
|
|
||||||
|
|
||||||
def attach_file(attachment, file_url)
|
|
||||||
attachment_file = Down.download(
|
|
||||||
file_url
|
|
||||||
)
|
|
||||||
attachment.file.attach(
|
|
||||||
io: attachment_file,
|
|
||||||
filename: attachment_file.original_filename,
|
|
||||||
content_type: attachment_file.content_type
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def attachment_params(attachment)
|
|
||||||
file_type = attachment['type'].to_sym
|
|
||||||
params = { file_type: file_type, account_id: @message.account_id }
|
|
||||||
|
|
||||||
if [:image, :file, :audio, :video, :share, :story_mention].include? file_type
|
|
||||||
params.merge!(file_type_params(attachment))
|
|
||||||
elsif file_type == :location
|
|
||||||
params.merge!(location_params(attachment))
|
|
||||||
elsif file_type == :fallback
|
|
||||||
params.merge!(fallback_params(attachment))
|
|
||||||
end
|
|
||||||
|
|
||||||
params
|
|
||||||
end
|
|
||||||
|
|
||||||
def file_type_params(attachment)
|
|
||||||
{
|
|
||||||
external_url: attachment['payload']['url'],
|
|
||||||
remote_file_url: attachment['payload']['url']
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_attachment_file_type(attachment)
|
|
||||||
return if @message.reload.attachments.blank?
|
|
||||||
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
|
||||||
|
|
||||||
attachment.file_type = file_type(attachment.file&.content_type)
|
|
||||||
attachment.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_story_link(attachment)
|
|
||||||
message = attachment.message
|
|
||||||
result = get_story_object_from_source_id(message.source_id)
|
|
||||||
|
|
||||||
return if result.blank?
|
|
||||||
|
|
||||||
story_id = result['story']['mention']['id']
|
|
||||||
story_sender = result['from']['username']
|
|
||||||
message.content_attributes[:story_sender] = story_sender
|
|
||||||
message.content_attributes[:story_id] = story_id
|
|
||||||
message.content_attributes[:image_type] = 'story_mention'
|
|
||||||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
|
||||||
message.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.attachments.destroy_all
|
|
||||||
@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
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def unsupported_file_type?(attachment_type)
|
|
||||||
[:template, :unsupported_type].include? attachment_type.to_sym
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -15,9 +15,6 @@ class NotificationBuilder
|
||||||
|
|
||||||
def user_subscribed_to_notification?
|
def user_subscribed_to_notification?
|
||||||
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
||||||
# added for the case where an assignee might be removed from the account but remains in conversation
|
|
||||||
return if notification_setting.blank?
|
|
||||||
|
|
||||||
return true if notification_setting.public_send("email_#{notification_type}?")
|
return true if notification_setting.public_send("email_#{notification_type}?")
|
||||||
return true if notification_setting.public_send("push_#{notification_type}?")
|
return true if notification_setting.public_send("push_#{notification_type}?")
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ class NotificationSubscriptionBuilder
|
||||||
def perform
|
def perform
|
||||||
# if multiple accounts were used to login in same browser
|
# if multiple accounts were used to login in same browser
|
||||||
move_subscription_to_user if identifier_subscription && identifier_subscription.user_id != user.id
|
move_subscription_to_user if identifier_subscription && identifier_subscription.user_id != user.id
|
||||||
identifier_subscription.blank? ? build_identifier_subscription : update_identifier_subscription
|
build_identifier_subscription if identifier_subscription.blank?
|
||||||
identifier_subscription
|
identifier_subscription
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -25,10 +25,6 @@ class NotificationSubscriptionBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_identifier_subscription
|
def build_identifier_subscription
|
||||||
@identifier_subscription = user.notification_subscriptions.create!(params.merge(identifier: identifier))
|
user.notification_subscriptions.create(params.merge(identifier: identifier))
|
||||||
end
|
|
||||||
|
|
||||||
def update_identifier_subscription
|
|
||||||
identifier_subscription.update(params.merge(identifier: identifier))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
class V2::ReportBuilder
|
class V2::ReportBuilder
|
||||||
include DateRangeHelper
|
|
||||||
include ReportHelper
|
|
||||||
attr_reader :account, :params
|
attr_reader :account, :params
|
||||||
|
|
||||||
DEFAULT_GROUP_BY = 'day'.freeze
|
|
||||||
AGENT_RESULTS_PER_PAGE = 25
|
|
||||||
|
|
||||||
def initialize(account, params)
|
def initialize(account, params)
|
||||||
@account = account
|
@account = account
|
||||||
@params = params
|
@params = params
|
||||||
|
|
||||||
timezone_offset = (params[:timezone_offset] || 0).to_f
|
|
||||||
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def timeseries
|
def timeseries
|
||||||
|
@ -20,14 +12,8 @@ class V2::ReportBuilder
|
||||||
|
|
||||||
# For backward compatible with old report
|
# For backward compatible with old report
|
||||||
def build
|
def build
|
||||||
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
|
timeseries.each_with_object([]) do |p, arr|
|
||||||
timeseries.each_with_object([]) do |p, arr|
|
arr << { value: p[1], timestamp: p[0].to_time.to_i }
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,66 +28,84 @@ class V2::ReportBuilder
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_metrics
|
|
||||||
if params[:type].equal?(:account)
|
|
||||||
conversations
|
|
||||||
else
|
|
||||||
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def scope
|
||||||
|
return account if params[:type].match?('account')
|
||||||
|
return inbox if params[:type].match?('inbox')
|
||||||
|
return user if params[:type].match?('agent')
|
||||||
|
end
|
||||||
|
|
||||||
def inbox
|
def inbox
|
||||||
@inbox ||= account.inboxes.find(params[:id])
|
@inbox ||= account.inboxes.where(id: params[:id]).first
|
||||||
end
|
end
|
||||||
|
|
||||||
def user
|
def user
|
||||||
@user ||= account.users.find(params[:id])
|
@user ||= account.users.where(id: params[:id]).first
|
||||||
end
|
end
|
||||||
|
|
||||||
def label
|
def conversations_count
|
||||||
@label ||= account.labels.find(params[:id])
|
scope.conversations
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def team
|
# unscoped removes all scopes added to a model previously
|
||||||
@team ||= account.teams.find(params[:id])
|
def incoming_messages_count
|
||||||
|
scope.messages.unscoped.where(account_id: account.id).incoming
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_grouped_values(object_scope)
|
def outgoing_messages_count
|
||||||
@grouped_values = object_scope.group_by_period(
|
scope.messages.unscoped.where(account_id: account.id).outgoing
|
||||||
params[:group_by] || DEFAULT_GROUP_BY,
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
:created_at,
|
.count
|
||||||
default_value: 0,
|
|
||||||
range: range,
|
|
||||||
permit: %w[day week month year],
|
|
||||||
time_zone: @timezone
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def agent_metrics
|
def resolutions_count
|
||||||
account_users = @account.account_users.page(params[:page]).per(AGENT_RESULTS_PER_PAGE)
|
scope.conversations
|
||||||
account_users.each_with_object([]) do |account_user, arr|
|
.resolved
|
||||||
@user = account_user.user
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
arr << {
|
.count
|
||||||
id: @user.id,
|
|
||||||
name: @user.name,
|
|
||||||
email: @user.email,
|
|
||||||
thumbnail: @user.avatar_url,
|
|
||||||
availability: account_user.availability_status,
|
|
||||||
metric: conversations
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations
|
def avg_first_response_time
|
||||||
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
scope.events
|
||||||
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
.where(name: 'first_response')
|
||||||
metric = {
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
open: @open_conversations.count,
|
.average(:value)
|
||||||
unattended: @open_conversations.count - first_response_count
|
end
|
||||||
}
|
|
||||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
def avg_resolution_time
|
||||||
metric
|
scope.events.where(name: 'conversation_resolved')
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def range
|
||||||
|
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Taking average of average is not too accurate
|
||||||
|
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||||
|
# TODO: Will optimize this later
|
||||||
|
def avg_resolution_time_summary
|
||||||
|
return 0 if avg_resolution_time.values.empty?
|
||||||
|
|
||||||
|
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_first_response_time_summary
|
||||||
|
return 0 if avg_first_response_time.values.empty?
|
||||||
|
|
||||||
|
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_date_time(datetime)
|
||||||
|
return datetime if datetime.is_a?(DateTime)
|
||||||
|
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||||
|
|
||||||
|
DateTime.strptime(datetime, '%s')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
class RoomChannel < ApplicationCable::Channel
|
class RoomChannel < ApplicationCable::Channel
|
||||||
def subscribed
|
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
|
ensure_stream
|
||||||
current_user
|
current_user
|
||||||
current_account
|
current_account
|
||||||
|
@ -18,8 +15,6 @@ class RoomChannel < ApplicationCable::Channel
|
||||||
private
|
private
|
||||||
|
|
||||||
def broadcast_presence
|
def broadcast_presence
|
||||||
return if @current_account.blank?
|
|
||||||
|
|
||||||
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
|
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
|
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 })
|
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
|
||||||
|
@ -31,22 +26,18 @@ class RoomChannel < ApplicationCable::Channel
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_subscription
|
def update_subscription
|
||||||
return if @current_account.blank?
|
|
||||||
|
|
||||||
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_user
|
def current_user
|
||||||
@current_user ||= if params[:user_id].blank?
|
@current_user ||= if params[:user_id].blank?
|
||||||
ContactInbox.find_by!(pubsub_token: @pubsub_token).contact
|
Contact.find_by!(pubsub_token: @pubsub_token)
|
||||||
else
|
else
|
||||||
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
return if current_user.blank?
|
|
||||||
|
|
||||||
@current_account ||= if @current_user.is_a? Contact
|
@current_account ||= if @current_user.is_a? Contact
|
||||||
@current_user.account
|
@current_user.account
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
class AndroidAppController < ApplicationController
|
|
||||||
def assetlinks
|
|
||||||
render layout: false
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -16,8 +16,4 @@ class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
authorize(model)
|
authorize(model)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_admin_authorization?
|
|
||||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :current_account
|
|
||||||
before_action :check_authorization
|
|
||||||
before_action :agent_bot, except: [:index, :create]
|
|
||||||
|
|
||||||
def index
|
|
||||||
@agent_bots = AgentBot.where(account_id: [nil, Current.account.id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@agent_bot = Current.account.agent_bots.create!(permitted_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@agent_bot.update!(permitted_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@agent_bot.destroy!
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def agent_bot
|
|
||||||
@agent_bot = AgentBot.where(account_id: [nil, Current.account.id]).find(params[:id]) if params[:action] == 'show'
|
|
||||||
@agent_bot ||= Current.account.agent_bots.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -2,7 +2,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_agent, except: [:create, :index]
|
before_action :fetch_agent, except: [:create, :index]
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :find_user, only: [:create]
|
before_action :find_user, only: [:create]
|
||||||
before_action :validate_limit, only: [:create]
|
|
||||||
before_action :create_user, only: [:create]
|
before_action :create_user, only: [:create]
|
||||||
before_action :save_account_user, only: [:create]
|
before_action :save_account_user, only: [:create]
|
||||||
|
|
||||||
|
@ -10,16 +9,19 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||||
@agents = agents
|
@agents = agents
|
||||||
end
|
end
|
||||||
|
|
||||||
def create; end
|
def destroy
|
||||||
|
@agent.current_account_user.destroy
|
||||||
def update
|
head :ok
|
||||||
@agent.update!(agent_params.slice(:name).compact)
|
|
||||||
@agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def update
|
||||||
@agent.current_account_user.destroy!
|
@agent.update!(agent_params.except(:role))
|
||||||
head :ok
|
@agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role]
|
||||||
|
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -36,42 +38,32 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||||
@user = User.find_by(email: new_agent_params[:email])
|
@user = User.find_by(email: new_agent_params[:email])
|
||||||
end
|
end
|
||||||
|
|
||||||
# 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
|
def create_user
|
||||||
return @user.send_confirmation_instructions if @user
|
return if @user
|
||||||
|
|
||||||
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
|
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_account_user
|
def save_account_user
|
||||||
AccountUser.create!({
|
AccountUser.create!(
|
||||||
account_id: Current.account.id,
|
account_id: Current.account.id,
|
||||||
user_id: @user.id,
|
user_id: @user.id,
|
||||||
inviter_id: current_user.id
|
|
||||||
}.merge({
|
|
||||||
role: new_agent_params[:role],
|
role: new_agent_params[:role],
|
||||||
availability: new_agent_params[:availability],
|
inviter_id: current_user.id
|
||||||
auto_offline: new_agent_params[:auto_offline]
|
)
|
||||||
}.compact))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def agent_params
|
def agent_params
|
||||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
params.require(:agent).permit(:email, :name, :role)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_agent_params
|
def new_agent_params
|
||||||
# intial string ensures the password requirements are met
|
time = Time.now.to_i
|
||||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
params.require(:agent).permit(:email, :name, :role)
|
||||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
.merge!(password: time, password_confirmation: time, inviter: current_user)
|
||||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def agents
|
def agents
|
||||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
||||||
end
|
|
||||||
|
|
||||||
def validate_limit
|
|
||||||
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
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
|
|
||||||
@portal_articles = @portal.articles
|
|
||||||
@all_articles = @portal_articles.search(list_params)
|
|
||||||
@articles_count = @all_articles.count
|
|
||||||
@articles = @all_articles.page(@current_page)
|
|
||||||
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
|
|
|
@ -1,24 +0,0 @@
|
||||||
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
|
|
|
@ -1,87 +0,0 @@
|
||||||
class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :check_authorization
|
|
||||||
before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone]
|
|
||||||
|
|
||||||
def index
|
|
||||||
@automation_rules = Current.account.automation_rules
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
|
||||||
@automation_rule.actions = params[:actions]
|
|
||||||
@automation_rule.conditions = params[:conditions]
|
|
||||||
|
|
||||||
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
|
||||||
|
|
||||||
@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
|
|
||||||
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
|
|
||||||
@automation_rule.destroy!
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def clone
|
|
||||||
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
|
||||||
new_rule = automation_rule.dup
|
|
||||||
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, :custom_attribute_type, { values: [] }],
|
|
||||||
actions: [:action_name, { action_params: [] }]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_automation_rule
|
|
||||||
@automation_rule = Current.account.automation_rules.find_by(id: params[:id])
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +1,32 @@
|
||||||
class Api::V1::Accounts::BaseController < Api::BaseController
|
class Api::V1::Accounts::BaseController < Api::BaseController
|
||||||
include SwitchLocale
|
include SwitchLocale
|
||||||
include EnsureCurrentAccountHelper
|
|
||||||
before_action :current_account
|
before_action :current_account
|
||||||
around_action :switch_locale_using_account_locale
|
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
|
end
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :type_matches?
|
|
||||||
|
|
||||||
def create
|
|
||||||
if type_matches?
|
|
||||||
::BulkActionsJob.perform_later(
|
|
||||||
account: @current_account,
|
|
||||||
user: current_user,
|
|
||||||
params: permitted_params
|
|
||||||
)
|
|
||||||
head :ok
|
|
||||||
else
|
|
||||||
render json: { success: false }, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def type_matches?
|
|
||||||
['Conversation'].include?(params[:type])
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []])
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -12,10 +12,9 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||||
page_access_token: page_access_token
|
page_access_token: page_access_token
|
||||||
)
|
)
|
||||||
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||||
set_instagram_id(page_access_token, facebook_channel)
|
|
||||||
set_avatar(@facebook_inbox, page_id)
|
set_avatar(@facebook_inbox, page_id)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e).capture_exception
|
Rails.logger.info e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,15 +22,6 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_instagram_id(page_access_token, facebook_channel)
|
|
||||||
fb_object = Koala::Facebook::API.new(page_access_token)
|
|
||||||
response = fb_object.get_connections('me', '', { fields: 'instagram_business_account' })
|
|
||||||
return if response['instagram_business_account'].blank?
|
|
||||||
|
|
||||||
instagram_id = response['instagram_business_account']['id']
|
|
||||||
facebook_channel.update(instagram_id: instagram_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# get params[:inbox_id], current_account. params[:omniauth_token]
|
# get params[:inbox_id], current_account. params[:omniauth_token]
|
||||||
def reauthorize_page
|
def reauthorize_page
|
||||||
if @inbox&.facebook?
|
if @inbox&.facebook?
|
||||||
|
@ -55,13 +45,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def update_fb_page(fb_page_id, access_token)
|
def update_fb_page(fb_page_id, access_token)
|
||||||
fb_page = get_fb_page(fb_page_id)
|
fb_page = get_fb_page(fb_page_id)
|
||||||
ActiveRecord::Base.transaction do
|
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
||||||
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
fb_page&.reauthorized!
|
||||||
set_instagram_id(access_token, fb_page)
|
|
||||||
fb_page&.reauthorized!
|
|
||||||
rescue StandardError => e
|
|
||||||
ChatwootExceptionTracker.new(e).capture_exception
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_fb_page(fb_page_id)
|
def get_fb_page(fb_page_id)
|
||||||
|
@ -74,23 +59,49 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def long_lived_token(omniauth_token)
|
def long_lived_token(omniauth_token)
|
||||||
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
|
||||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.info e
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_already_existing_facebook_pages(data)
|
def mark_already_existing_facebook_pages(data)
|
||||||
return [] if data.empty?
|
return [] if data.empty?
|
||||||
|
|
||||||
data.inject([]) do |result, page_detail|
|
data.inject([]) do |result, page_detail|
|
||||||
page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id'])
|
page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
|
||||||
result << page_detail
|
result << page_detail
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_avatar(facebook_inbox, page_id)
|
def set_avatar(facebook_inbox, page_id)
|
||||||
avatar_url = "https://graph.facebook.com/#{page_id}/picture?type=large"
|
uri = get_avatar_url(page_id)
|
||||||
Avatar::AvatarFromUrlJob.perform_later(facebook_inbox, avatar_url)
|
|
||||||
|
return unless uri
|
||||||
|
|
||||||
|
avatar_resource = LocalResource.new(uri)
|
||||||
|
facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
||||||
|
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||||
|
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_avatar_url(page_id)
|
||||||
|
begin
|
||||||
|
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
|
||||||
|
uri = URI.parse(url)
|
||||||
|
tries = 3
|
||||||
|
begin
|
||||||
|
response = uri.open(redirect: false)
|
||||||
|
rescue OpenURI::HTTPRedirect => e
|
||||||
|
uri = e.uri # assigned from the "Location" response header
|
||||||
|
retry if (tries -= 1).positive?
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
pic_url = response.base_uri.to_s
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||||
|
pic_url = nil
|
||||||
|
end
|
||||||
|
pic_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :campaign, except: [:index, :create]
|
|
||||||
before_action :check_authorization
|
|
||||||
|
|
||||||
def index
|
|
||||||
@campaigns = Current.account.campaigns
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@campaign = Current.account.campaigns.create!(campaign_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@campaign.destroy!
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@campaign.update!(campaign_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def campaign
|
|
||||||
@campaign ||= Current.account.campaigns.find_by(display_id: params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def campaign_params
|
|
||||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
|
|
||||||
:scheduled_at, audience: [:type, :id], trigger_rules: {})
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -17,7 +17,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@canned_response.destroy!
|
@canned_response.destroy
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
|
||||||
|
|
||||||
def canned_responses
|
def canned_responses
|
||||||
if params[:search]
|
if params[:search]
|
||||||
Current.account.canned_responses.where('short_code ILIKE :search OR content ILIKE :search', search: "%#{params[:search]}%")
|
Current.account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
|
||||||
else
|
else
|
||||||
Current.account.canned_responses
|
Current.account.canned_responses
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
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
|
|
||||||
@current_locale = params[:locale]
|
|
||||||
@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
|
|
|
@ -6,6 +6,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||||
authenticate_twilio
|
authenticate_twilio
|
||||||
build_inbox
|
build_inbox
|
||||||
setup_webhooks if @twilio_channel.sms?
|
setup_webhooks if @twilio_channel.sms?
|
||||||
|
rescue Twilio::REST::TwilioError => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
render_could_not_create_error(e.message)
|
render_could_not_create_error(e.message)
|
||||||
end
|
end
|
||||||
|
@ -27,8 +29,6 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||||
end
|
end
|
||||||
|
|
||||||
def phone_number
|
def phone_number
|
||||||
return if permitted_params[:phone_number].blank?
|
|
||||||
|
|
||||||
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
|
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -40,11 +40,10 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||||
@twilio_channel = Current.account.twilio_sms.create!(
|
@twilio_channel = Current.account.twilio_sms.create!(
|
||||||
account_sid: permitted_params[:account_sid],
|
account_sid: permitted_params[:account_sid],
|
||||||
auth_token: permitted_params[:auth_token],
|
auth_token: permitted_params[:auth_token],
|
||||||
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
|
|
||||||
phone_number: phone_number,
|
phone_number: phone_number,
|
||||||
medium: medium
|
medium: medium
|
||||||
)
|
)
|
||||||
@inbox = Current.account.inboxes.create!(
|
@inbox = Current.account.inboxes.create(
|
||||||
name: permitted_params[:name],
|
name: permitted_params[:name],
|
||||||
channel: @twilio_channel
|
channel: @twilio_channel
|
||||||
)
|
)
|
||||||
|
@ -52,7 +51,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.require(:twilio_channel).permit(
|
params.require(:twilio_channel).permit(
|
||||||
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium
|
:account_id, :phone_number, :account_sid, :auth_token, :name, :medium
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
class Api::V1::Accounts::Contacts::BaseController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :ensure_contact
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def ensure_contact
|
|
||||||
@contact = Current.account.contacts.find(params[:contact_id])
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,18 +1,19 @@
|
||||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
|
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :ensure_contact
|
||||||
before_action :ensure_inbox, only: [:create]
|
before_action :ensure_inbox, only: [:create]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@contact_inbox = ContactInboxBuilder.new(
|
source_id = params[:source_id] || SecureRandom.uuid
|
||||||
contact: @contact,
|
@contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id)
|
||||||
inbox: @inbox,
|
|
||||||
source_id: params[:source_id]
|
|
||||||
).perform
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def ensure_inbox
|
def ensure_inbox
|
||||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
authorize @inbox, :show?
|
end
|
||||||
|
|
||||||
|
def ensure_contact
|
||||||
|
@contact = Current.account.contacts.find(params[:contact_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::Contacts::BaseController
|
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::BaseController
|
||||||
def index
|
def index
|
||||||
@conversations = Current.account.conversations.includes(
|
@conversations = Current.account.conversations.includes(
|
||||||
:assignee, :contact, :inbox, :taggings
|
:assignee, :contact, :inbox, :taggings
|
||||||
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20)
|
).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def inbox_ids
|
def inbox_ids
|
||||||
if Current.user.administrator? || Current.user.agent?
|
if Current.user.administrator?
|
||||||
|
Current.account.inboxes.pluck(:id)
|
||||||
|
elsif Current.user.agent?
|
||||||
Current.user.assigned_inboxes.pluck(:id)
|
Current.user.assigned_inboxes.pluck(:id)
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:contact_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
class Api::V1::Accounts::Contacts::LabelsController < Api::V1::Accounts::Contacts::BaseController
|
class Api::V1::Accounts::Contacts::LabelsController < Api::V1::Accounts::BaseController
|
||||||
include LabelConcern
|
include LabelConcern
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def model
|
def model
|
||||||
@model ||= @contact
|
@model ||= Current.account.contacts.find(permitted_params[:contact_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(labels: [])
|
params.permit(:contact_id, labels: [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts::BaseController
|
|
||||||
before_action :note, except: [:index, :create]
|
|
||||||
|
|
||||||
def index
|
|
||||||
@notes = @contact.notes.latest.includes(:user)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@note = @contact.notes.create!(note_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@note.destroy!
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@note.update(note_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def note
|
|
||||||
@note ||= @contact.notes.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def note_params
|
|
||||||
params.require(:note).permit(:content).merge({ contact_id: @contact.id, user_id: Current.user.id })
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,44 +1,32 @@
|
||||||
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
include Sift
|
|
||||||
sort_on :email, type: :string
|
|
||||||
sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction]
|
|
||||||
sort_on :phone_number, type: :string
|
|
||||||
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
|
|
||||||
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
|
|
||||||
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
|
|
||||||
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
|
|
||||||
|
|
||||||
RESULTS_PER_PAGE = 15
|
RESULTS_PER_PAGE = 15
|
||||||
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index, :active, :search, :filter]
|
before_action :set_current_page, only: [:index, :active, :search]
|
||||||
before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes]
|
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
||||||
before_action :set_include_contact_inboxes, only: [:index, :search, :filter]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts_count = resolved_contacts.count
|
@contacts_count = resolved_contacts.count
|
||||||
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
|
@contacts = fetch_contact_last_seen_at(resolved_contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||||
|
|
||||||
contacts = resolved_contacts.where(
|
contacts = resolved_contacts.where(
|
||||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
|
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search',
|
||||||
search: "%#{params[:q].strip}%"
|
search: "%#{params[:q]}%"
|
||||||
)
|
)
|
||||||
@contacts_count = contacts.count
|
@contacts_count = contacts.count
|
||||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
@contacts = fetch_contact_last_seen_at(contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def import
|
def import
|
||||||
render json: { error: I18n.t('errors.contacts.import.failed') }, status: :unprocessable_entity and return if params[:import_file].blank?
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
import = Current.account.data_imports.create!(data_type: 'contacts')
|
import = Current.account.data_imports.create!(data_type: 'contacts')
|
||||||
import.import_file.attach(params[:import_file])
|
import.import_file.attach(params[:import_file])
|
||||||
end
|
end
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -52,127 +40,73 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def filter
|
|
||||||
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
|
|
||||||
contacts = result[:contacts]
|
|
||||||
@contacts_count = result[:count]
|
|
||||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
|
||||||
end
|
|
||||||
|
|
||||||
def contactable_inboxes
|
def contactable_inboxes
|
||||||
@all_contactable_inboxes = Contacts::ContactableInboxesService.new(contact: @contact).get
|
@contactable_inboxes = Contacts::ContactableInboxesService.new(contact: @contact).get
|
||||||
@contactable_inboxes = @all_contactable_inboxes.select { |contactable_inbox| policy(contactable_inbox[:inbox]).show? }
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO : refactor this method into dedicated contacts/custom_attributes controller class and routes
|
|
||||||
def destroy_custom_attributes
|
|
||||||
@contact.custom_attributes = @contact.custom_attributes.excluding(params[:custom_attributes])
|
|
||||||
@contact.save!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
|
@contact = Current.account.contacts.new(contact_params)
|
||||||
@contact.save!
|
@contact.save!
|
||||||
@contact_inbox = build_contact_inbox
|
@contact_inbox = build_contact_inbox
|
||||||
process_avatar
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@contact.assign_attributes(contact_update_params)
|
@contact.assign_attributes(contact_update_params)
|
||||||
@contact.save!
|
@contact.save!
|
||||||
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
end
|
render json: {
|
||||||
|
message: e.record.errors.full_messages.join(', '),
|
||||||
def destroy
|
contact: Current.account.contacts.find_by(email: contact_params[:email])
|
||||||
if ::OnlineStatusTracker.get_presence(
|
}, status: :unprocessable_entity
|
||||||
@contact.account.id, 'Contact', @contact.id
|
|
||||||
)
|
|
||||||
return render_error({ message: I18n.t('contacts.online.delete', contact_name: @contact.name.capitalize) },
|
|
||||||
:unprocessable_entity)
|
|
||||||
end
|
|
||||||
|
|
||||||
@contact.destroy!
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def avatar
|
|
||||||
@contact.avatar.purge if @contact.avatar.attached?
|
|
||||||
@contact
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# TODO: Move this to a finder class
|
|
||||||
def resolved_contacts
|
def resolved_contacts
|
||||||
return @resolved_contacts if @resolved_contacts
|
@resolved_contacts ||= Current.account.contacts
|
||||||
|
.where.not(email: [nil, ''])
|
||||||
@resolved_contacts = Current.account.contacts.resolved_contacts
|
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||||
|
.order('LOWER(name)')
|
||||||
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
|
|
||||||
@resolved_contacts
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_current_page
|
def set_current_page
|
||||||
@current_page = params[:page] || 1
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contacts_with_conversation_count(contacts)
|
def fetch_contact_last_seen_at(contacts)
|
||||||
contacts_with_conversation_count = filtrate(contacts).left_outer_joins(:conversations)
|
contacts.left_outer_joins(:conversations)
|
||||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
.select('contacts.*, COUNT(conversations.id) as conversations_count, MAX(conversations.contact_last_seen_at) as last_seen_at')
|
||||||
.group('contacts.id')
|
.group('contacts.id')
|
||||||
.includes([{ avatar_attachment: [:blob] }])
|
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
||||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||||
|
|
||||||
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
|
||||||
|
|
||||||
contacts_with_conversation_count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
return if params[:inbox_id].blank?
|
return if params[:inbox_id].blank?
|
||||||
|
|
||||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
ContactInboxBuilder.new(
|
source_id = params[:source_id] || SecureRandom.uuid
|
||||||
contact: @contact,
|
ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id)
|
||||||
inbox: inbox,
|
|
||||||
source_id: params[:source_id]
|
|
||||||
).perform
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def contact_params
|
||||||
params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
|
params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_custom_attributes
|
def contact_custom_attributes
|
||||||
return @contact.custom_attributes.merge(permitted_params[:custom_attributes]) if permitted_params[:custom_attributes]
|
return @contact.custom_attributes.merge(contact_params[:custom_attributes]) if contact_params[:custom_attributes]
|
||||||
|
|
||||||
@contact.custom_attributes
|
@contact.custom_attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_update_params
|
def contact_update_params
|
||||||
# we want the merged custom attributes not the original one
|
# we want the merged custom attributes not the original one
|
||||||
permitted_params.except(:custom_attributes, :avatar_url).merge({ custom_attributes: contact_custom_attributes })
|
contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes })
|
||||||
end
|
|
||||||
|
|
||||||
def set_include_contact_inboxes
|
|
||||||
@include_contact_inboxes = if params[:include_contact_inboxes].present?
|
|
||||||
params[:include_contact_inboxes] == 'true'
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contact
|
def fetch_contact
|
||||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_avatar
|
|
||||||
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_error(error, error_status)
|
|
||||||
render json: error, status: error_status
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,34 +1,20 @@
|
||||||
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
|
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
|
||||||
# assigns agent/team to a conversation
|
# assigns agent/team to a conversation
|
||||||
def create
|
def create
|
||||||
if params.key?(:assignee_id)
|
set_assignee
|
||||||
set_agent
|
render json: @assignee
|
||||||
elsif params.key?(:team_id)
|
|
||||||
set_team
|
|
||||||
else
|
|
||||||
render json: nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_agent
|
def set_assignee
|
||||||
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
|
||||||
@conversation.update_assignee(@agent)
|
if params.key?(:assignee_id)
|
||||||
render_agent
|
@assignee = Current.account.users.find_by(id: params[:assignee_id])
|
||||||
end
|
@conversation.update_assignee(@assignee)
|
||||||
|
elsif params.key?(:team_id)
|
||||||
def render_agent
|
@assignee = Current.account.teams.find_by(id: params[:team_id])
|
||||||
if @agent.nil?
|
@conversation.update!(team: @assignee)
|
||||||
render json: nil
|
|
||||||
else
|
|
||||||
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @agent }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_team
|
|
||||||
@team = Current.account.teams.find_by(id: params[:team_id])
|
|
||||||
@conversation.update!(team: @team)
|
|
||||||
render json: @team
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
|
||||||
include EnsureCurrentAccountHelper
|
|
||||||
before_action :conversation
|
before_action :conversation
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def conversation
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
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
|
def destroy
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
|
message.update!(content: I18n.t('conversations.messages.deleted'), deleted: true)
|
||||||
message.attachments.destroy_all
|
message.attachments.destroy_all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
include DateRangeHelper
|
|
||||||
|
|
||||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
before_action :conversation, except: [:index]
|
||||||
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
before_action :contact_inbox, only: [:create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
result = conversation_finder.perform
|
result = conversation_finder.perform
|
||||||
|
@ -24,19 +23,13 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
@conversation = ::Conversation.create!(conversation_params)
|
||||||
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def filter
|
|
||||||
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
|
|
||||||
@conversations = result[:conversations]
|
|
||||||
@conversations_count = result[:count]
|
|
||||||
end
|
|
||||||
|
|
||||||
def mute
|
def mute
|
||||||
@conversation.mute!
|
@conversation.mute!
|
||||||
head :ok
|
head :ok
|
||||||
|
@ -48,117 +41,73 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
end
|
end
|
||||||
|
|
||||||
def transcript
|
def transcript
|
||||||
render json: { error: 'email param missing' }, status: :unprocessable_entity and return if params[:email].blank?
|
ConversationReplyMailer.conversation_transcript(@conversation, params[:email])&.deliver_later if params[:email].present?
|
||||||
|
|
||||||
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, params[:email])&.deliver_later
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
if params[:status]
|
if params[:status]
|
||||||
set_conversation_status
|
@conversation.status = params[:status]
|
||||||
@status = @conversation.save!
|
@status = @conversation.save
|
||||||
else
|
else
|
||||||
@status = @conversation.toggle_status
|
@status = @conversation.toggle_status
|
||||||
end
|
end
|
||||||
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def toggle_typing_status
|
def toggle_typing_status
|
||||||
case params[:typing_status]
|
case params[:typing_status]
|
||||||
when 'on'
|
when 'on'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
|
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||||
when 'off'
|
when 'off'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
|
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||||
end
|
end
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_last_seen
|
def update_last_seen
|
||||||
update_last_seen_on_conversation(DateTime.now.utc, assignee?)
|
@conversation.agent_last_seen_at = DateTime.now.utc
|
||||||
end
|
|
||||||
|
|
||||||
def unread
|
|
||||||
last_incoming_message = @conversation.messages.incoming.last
|
|
||||||
last_seen_at = last_incoming_message.created_at - 1.second if last_incoming_message.present?
|
|
||||||
update_last_seen_on_conversation(last_seen_at, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def custom_attributes
|
|
||||||
@conversation.custom_attributes = params.permit(custom_attributes: {})[:custom_attributes]
|
|
||||||
@conversation.save!
|
@conversation.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def update_last_seen_on_conversation(last_seen_at, update_assignee)
|
def trigger_typing_event(event)
|
||||||
# rubocop:disable Rails/SkipsModelValidations
|
|
||||||
@conversation.update_column(:agent_last_seen_at, last_seen_at)
|
|
||||||
@conversation.update_column(:assignee_last_seen_at, last_seen_at) if update_assignee.present?
|
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_conversation_status
|
|
||||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
|
||||||
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
|
||||||
# status = params[:status] == 'bot' ? 'pending' : params[:status]
|
|
||||||
@conversation.status = params[:status]
|
|
||||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
|
||||||
end
|
|
||||||
|
|
||||||
def assign_conversation
|
|
||||||
@agent = Current.account.users.find(current_user.id)
|
|
||||||
@conversation.update_assignee(@agent)
|
|
||||||
end
|
|
||||||
|
|
||||||
def trigger_typing_event(event, is_private)
|
|
||||||
user = current_user.presence || @resource
|
user = current_user.presence || @resource
|
||||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
|
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
|
@conversation ||= Current.account.conversations.find_by(display_id: params[:id])
|
||||||
authorize @conversation.inbox, :show?
|
|
||||||
end
|
|
||||||
|
|
||||||
def inbox
|
|
||||||
return if params[:inbox_id].blank?
|
|
||||||
|
|
||||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
|
||||||
authorize @inbox, :show?
|
|
||||||
end
|
|
||||||
|
|
||||||
def contact
|
|
||||||
return if params[:contact_id].blank?
|
|
||||||
|
|
||||||
@contact = Current.account.contacts.find(params[:contact_id])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_inbox
|
def contact_inbox
|
||||||
@contact_inbox = build_contact_inbox
|
@contact_inbox = build_contact_inbox
|
||||||
|
|
||||||
# fallback for the old case where we do look up only using source id
|
|
||||||
# In future we need to change this and make sure we do look up on combination of inbox_id and source_id
|
|
||||||
# and deprecate the support of passing only source_id as the param
|
|
||||||
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||||
authorize @contact_inbox.inbox, :show?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
return if @inbox.blank? || @contact.blank?
|
return if params[:contact_id].blank? || params[:inbox_id].blank?
|
||||||
|
|
||||||
ContactInboxBuilder.new(
|
ContactInboxBuilder.new(
|
||||||
contact: @contact,
|
contact_id: params[:contact_id],
|
||||||
inbox: @inbox,
|
inbox_id: params[:inbox_id],
|
||||||
source_id: params[:source_id]
|
source_id: params[:source_id]
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_finder
|
def conversation_params
|
||||||
@conversation_finder ||= ConversationFinder.new(Current.user, params)
|
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||||
|
{
|
||||||
|
account_id: Current.account.id,
|
||||||
|
inbox_id: @contact_inbox.inbox_id,
|
||||||
|
contact_id: @contact_inbox.contact_id,
|
||||||
|
contact_inbox_id: @contact_inbox.id,
|
||||||
|
additional_attributes: additional_attributes
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def assignee?
|
def conversation_finder
|
||||||
@conversation.assignee_id? && Current.user == @conversation.assignee
|
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::BaseController
|
|
||||||
include Sift
|
|
||||||
include DateRangeHelper
|
|
||||||
|
|
||||||
RESULTS_PER_PAGE = 25
|
|
||||||
|
|
||||||
before_action :check_authorization
|
|
||||||
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]
|
|
||||||
|
|
||||||
sort_on :created_at, type: :datetime
|
|
||||||
|
|
||||||
def index; end
|
|
||||||
|
|
||||||
def metrics
|
|
||||||
@total_count = @csat_survey_responses.count
|
|
||||||
@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', formats: [:csv]
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_total_sent_messages_count
|
|
||||||
@csat_messages = Current.account.messages.input_csat
|
|
||||||
@csat_messages = @csat_messages.where(created_at: range) if range.present?
|
|
||||||
@total_sent_messages_count = @csat_messages.count
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_csat_survey_responses
|
|
||||||
@csat_survey_responses = filtrate(
|
|
||||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
|
||||||
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_current_page_surveys
|
|
||||||
@csat_survey_responses = @csat_survey_responses.page(@current_page).per(RESULTS_PER_PAGE)
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_current_page
|
|
||||||
@current_page = params[:page] || 1
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,49 +0,0 @@
|
||||||
class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_custom_attributes_definitions, except: [:create]
|
|
||||||
before_action :fetch_custom_attribute_definition, only: [:show, :update, :destroy]
|
|
||||||
DEFAULT_ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
|
|
||||||
|
|
||||||
def index; end
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@custom_attribute_definition = Current.account.custom_attribute_definitions.create!(
|
|
||||||
permitted_payload
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@custom_attribute_definition.update!(permitted_payload)
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@custom_attribute_definition.destroy!
|
|
||||||
head :no_content
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def fetch_custom_attributes_definitions
|
|
||||||
@custom_attribute_definitions = Current.account.custom_attribute_definitions.with_attribute_model(permitted_params[:attribute_model])
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_custom_attribute_definition
|
|
||||||
@custom_attribute_definition = Current.account.custom_attribute_definitions.find(permitted_params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_payload
|
|
||||||
params.require(:custom_attribute_definition).permit(
|
|
||||||
:attribute_display_name,
|
|
||||||
:attribute_description,
|
|
||||||
:attribute_display_type,
|
|
||||||
:attribute_key,
|
|
||||||
:attribute_model,
|
|
||||||
attribute_values: []
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(:id, :filter_type, :attribute_model)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,49 +0,0 @@
|
||||||
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_custom_filters, except: [:create]
|
|
||||||
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
|
|
||||||
DEFAULT_FILTER_TYPE = 'conversation'.freeze
|
|
||||||
|
|
||||||
def index; end
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@custom_filter = current_user.custom_filters.create!(
|
|
||||||
permitted_payload.merge(account_id: Current.account.id)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@custom_filter.update!(permitted_payload)
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@custom_filter.destroy!
|
|
||||||
head :no_content
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def fetch_custom_filters
|
|
||||||
@custom_filters = current_user.custom_filters.where(
|
|
||||||
account_id: Current.account.id,
|
|
||||||
filter_type: permitted_params[:filter_type] || DEFAULT_FILTER_TYPE
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_custom_filter
|
|
||||||
@custom_filter = @custom_filters.find(permitted_params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_payload
|
|
||||||
params.require(:custom_filter).permit(
|
|
||||||
:name,
|
|
||||||
:filter_type,
|
|
||||||
query: {}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(:id, :filter_type)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,44 +0,0 @@
|
||||||
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
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
class Api::V1::Accounts::FacebookIndicatorsController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :set_access_token
|
||||||
|
around_action :handle_with_exception
|
||||||
|
|
||||||
|
def mark_seen
|
||||||
|
fb_bot.deliver(payload('mark_seen'), access_token: @access_token)
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def typing_on
|
||||||
|
fb_bot.deliver(payload('typing_on'), access_token: @access_token)
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def typing_off
|
||||||
|
fb_bot.deliver(payload('typing_off'), access_token: @access_token)
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fb_bot
|
||||||
|
::Facebook::Messenger::Bot
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_with_exception
|
||||||
|
yield
|
||||||
|
rescue Facebook::Messenger::Error => e
|
||||||
|
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload(action)
|
||||||
|
{
|
||||||
|
recipient: { id: contact.source_id },
|
||||||
|
sender_action: action
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
@inbox ||= Current.account.inboxes.find(permitted_params[:inbox_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_access_token
|
||||||
|
@access_token = inbox.channel.page_access_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact
|
||||||
|
@contact ||= inbox.contact_inboxes.find_by!(contact_id: permitted_params[:contact_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:inbox_id, :contact_id)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,40 +1,22 @@
|
||||||
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_inbox
|
before_action :fetch_inbox, only: [:create, :show]
|
||||||
before_action :current_agents_ids, only: [:create, :update]
|
before_action :current_agents_ids, only: [:create]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize @inbox, :create?
|
# update also done via same action
|
||||||
ActiveRecord::Base.transaction do
|
update_agents_list
|
||||||
agents_to_be_added_ids.map { |user_id| @inbox.add_member(user_id) }
|
head :ok
|
||||||
end
|
rescue StandardError => e
|
||||||
fetch_updated_agents
|
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||||
|
render_could_not_create_error('Could not add agents to inbox')
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize @inbox, :show?
|
@agents = Current.account.users.where(id: @inbox.members.select(:user_id))
|
||||||
fetch_updated_agents
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
authorize @inbox, :update?
|
|
||||||
update_agents_list
|
|
||||||
fetch_updated_agents
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
authorize @inbox, :destroy?
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
params[:user_ids].map { |user_id| @inbox.remove_member(user_id) }
|
|
||||||
end
|
|
||||||
head :ok
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_updated_agents
|
|
||||||
@agents = Current.account.users.where(id: @inbox.members.select(:user_id))
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_agents_list
|
def update_agents_list
|
||||||
# get all the user_ids which the inbox currently has as members.
|
# get all the user_ids which the inbox currently has as members.
|
||||||
# get the list of user_ids from params
|
# get the list of user_ids from params
|
||||||
|
|
|
@ -1,73 +1,37 @@
|
||||||
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
include Api::V1::InboxesHelper
|
|
||||||
before_action :fetch_inbox, except: [:index, :create]
|
before_action :fetch_inbox, except: [:index, :create]
|
||||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||||
before_action :validate_limit, only: [:create]
|
before_action :check_authorization
|
||||||
# we are already handling the authorization in fetch inbox
|
|
||||||
before_action :check_authorization, except: [:show]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
# Deprecated: This API will be removed in 2.7.0
|
|
||||||
def assignable_agents
|
def assignable_agents
|
||||||
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def campaigns
|
|
||||||
@campaigns = @inbox.campaigns
|
|
||||||
end
|
|
||||||
|
|
||||||
def avatar
|
|
||||||
@inbox.avatar.attachment.destroy! if @inbox.avatar.attached?
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
channel = create_channel
|
channel = create_channel
|
||||||
@inbox = Current.account.inboxes.build(
|
@inbox = Current.account.inboxes.build(
|
||||||
{
|
name: permitted_params[:name],
|
||||||
name: inbox_name(channel),
|
greeting_message: permitted_params[:greeting_message],
|
||||||
channel: channel
|
greeting_enabled: permitted_params[:greeting_enabled],
|
||||||
}.merge(
|
channel: channel
|
||||||
permitted_params.except(:channel)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
@inbox.avatar.attach(permitted_params[:avatar])
|
||||||
@inbox.save!
|
@inbox.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@inbox.update!(permitted_params.except(:channel))
|
@inbox.update(inbox_update_params.except(:channel))
|
||||||
update_inbox_working_hours
|
|
||||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
|
||||||
|
|
||||||
# Inbox update doesn't necessarily need channel attributes
|
|
||||||
return if permitted_params(channel_attributes)[:channel].blank?
|
|
||||||
|
|
||||||
if @inbox.inbox_type == 'Email'
|
|
||||||
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]
|
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||||
end
|
return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
|
||||||
|
|
||||||
def agent_bot
|
@inbox.channel.update!(inbox_update_params[:channel])
|
||||||
@agent_bot = @inbox.agent_bot
|
update_channel_feature_flags
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_agent_bot
|
def set_agent_bot
|
||||||
|
@ -82,7 +46,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@inbox.destroy!
|
@inbox.destroy
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -90,7 +54,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def fetch_inbox
|
def fetch_inbox
|
||||||
@inbox = Current.account.inboxes.find(params[:id])
|
@inbox = Current.account.inboxes.find(params[:id])
|
||||||
authorize @inbox, :show?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_agent_bot
|
def fetch_agent_bot
|
||||||
|
@ -98,51 +61,42 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_channel
|
def create_channel
|
||||||
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
case permitted_params[:channel][:type]
|
||||||
|
when 'web_widget'
|
||||||
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
|
Current.account.web_widgets.create!(permitted_params[:channel].except(:type))
|
||||||
|
when 'api'
|
||||||
|
Current.account.api_channels.create!(permitted_params[:channel].except(:type))
|
||||||
|
when 'email'
|
||||||
|
Current.account.email_channels.create!(permitted_params[:channel].except(:type))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_channel_feature_flags
|
def update_channel_feature_flags
|
||||||
return unless @inbox.web_widget?
|
return unless inbox_update_params[:channel].key? :selected_feature_flags
|
||||||
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
|
||||||
|
|
||||||
@inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags]
|
@inbox.channel.selected_feature_flags = inbox_update_params[:channel][:selected_feature_flags]
|
||||||
@inbox.channel.save!
|
@inbox.channel.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def inbox_attributes
|
def permitted_params
|
||||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel:
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time])
|
||||||
:lock_to_single_conversation]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params(channel_attributes = [])
|
def inbox_update_params
|
||||||
params.permit(
|
params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
|
||||||
*inbox_attributes,
|
:working_hours_enabled, :out_of_office_message, :timezone,
|
||||||
channel: [:type, *channel_attributes]
|
channel: [
|
||||||
)
|
:website_url,
|
||||||
end
|
:widget_color,
|
||||||
|
:welcome_title,
|
||||||
def channel_type_from_params
|
:welcome_tagline,
|
||||||
{
|
:webhook_url,
|
||||||
'web_widget' => Channel::WebWidget,
|
:email,
|
||||||
'api' => Channel::Api,
|
:reply_time,
|
||||||
'email' => Channel::Email,
|
:pre_chat_form_enabled,
|
||||||
'line' => Channel::Line,
|
{ pre_chat_form_options: [:pre_chat_message, :require_email] },
|
||||||
'telegram' => Channel::Telegram,
|
{ selected_feature_flags: [] }
|
||||||
'whatsapp' => Channel::Whatsapp,
|
])
|
||||||
'sms' => Channel::Sms
|
|
||||||
}[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
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_admin_authorization?
|
|
||||||
before_action :fetch_apps, only: [:index]
|
before_action :fetch_apps, only: [:index]
|
||||||
before_action :fetch_app, only: [:show]
|
before_action :fetch_app, only: [:show]
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_hook, only: [:update, :destroy]
|
|
||||||
before_action :check_authorization
|
|
||||||
|
|
||||||
def create
|
|
||||||
@hook = Current.account.hooks.create!(permitted_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@hook.update!(permitted_params.slice(:status, :settings))
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@hook.destroy!
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def fetch_hook
|
|
||||||
@hook = Current.account.hooks.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
authorize(:hook)
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.require(:hook).permit(:app_id, :inbox_id, :status, settings: {})
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,5 +1,4 @@
|
||||||
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_admin_authorization?
|
|
||||||
before_action :fetch_hook, only: [:update, :destroy]
|
before_action :fetch_hook, only: [:update, :destroy]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -20,7 +19,7 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@hook.destroy!
|
@hook.destroy
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
9
app/controllers/api/v1/accounts/kbase/base_controller.rb
Normal file
9
app/controllers/api/v1/accounts/kbase/base_controller.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
32
app/controllers/api/v1/accounts/kbase/portals_controller.rb
Normal file
32
app/controllers/api/v1/accounts/kbase/portals_controller.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@label.destroy!
|
@label.destroy
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
|
|
||||||
before_action :check_authorization, only: [:show, :update, :destroy, :execute]
|
|
||||||
|
|
||||||
def index
|
|
||||||
@macros = Macro.with_visibility(current_user, params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
|
|
||||||
@macro.set_visibility(current_user, permitted_params)
|
|
||||||
@macro.actions = params[:actions]
|
|
||||||
|
|
||||||
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
|
||||||
|
|
||||||
@macro.save!
|
|
||||||
process_attachments
|
|
||||||
@macro
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
|
||||||
head :not_found if @macro.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@macro.destroy!
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def attach_file
|
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
|
||||||
key: nil,
|
|
||||||
io: params[:attachment].tempfile,
|
|
||||||
filename: params[:attachment].original_filename,
|
|
||||||
content_type: params[:attachment].content_type
|
|
||||||
)
|
|
||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
@macro.update!(macros_with_user)
|
|
||||||
@macro.set_visibility(current_user, permitted_params)
|
|
||||||
process_attachments
|
|
||||||
@macro.save!
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error e
|
|
||||||
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute
|
|
||||||
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
|
|
||||||
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def process_attachments
|
|
||||||
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
|
||||||
return if actions.blank?
|
|
||||||
|
|
||||||
actions.each do |action|
|
|
||||||
blob_id = action['action_params']
|
|
||||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
|
||||||
@macro.files.attach(blob)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(
|
|
||||||
:name, :account_id, :visibility,
|
|
||||||
actions: [:action_name, { action_params: [] }]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def macros_with_user
|
|
||||||
permitted_params.merge(updated_by_id: current_user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_macro
|
|
||||||
@macro = Current.account.macros.find_by(id: params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
authorize(@macro) if @macro.present?
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +1,7 @@
|
||||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||||
RESULTS_PER_PAGE = 15
|
RESULTS_PER_PAGE = 15
|
||||||
|
|
||||||
|
protect_from_forgery with: :null_session
|
||||||
before_action :fetch_notification, only: [:update]
|
before_action :fetch_notification, only: [:update]
|
||||||
before_action :set_primary_actor, only: [:read_all]
|
before_action :set_primary_actor, only: [:read_all]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
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
|
|
||||||
@all_articles = @portal.articles
|
|
||||||
@articles = @all_articles.search(locale: params[:locale])
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@portal = Current.account.portals.build(portal_params)
|
|
||||||
@portal.custom_domain = parsed_custom_domain
|
|
||||||
@portal.save!
|
|
||||||
process_attached_logo
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
@portal.update!(portal_params) if params[:portal].present?
|
|
||||||
# @portal.custom_domain = parsed_custom_domain
|
|
||||||
process_attached_logo
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error e
|
|
||||||
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def parsed_custom_domain
|
|
||||||
domain = URI.parse(@portal.custom_domain)
|
|
||||||
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
|
|
||||||
end
|
|
||||||
end
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue