Compare commits

..
Author SHA1 Message Date
iamsivin 8fb043de54 chore: spec fixes 2023-05-19 18:55:09 +05:30
iamsivin 6d07a7d92e fix: Problem when using Brazilian Portuguese 2023-05-19 15:51:41 +05:30
3843 changed files with 54941 additions and 162062 deletions
+27 -26
View File
@@ -12,12 +12,12 @@ defaults: &defaults
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
- image: cimg/postgres:15.3
- image: cimg/postgres:14.1
- image: cimg/redis:6.2.6
environment:
- RAILS_LOG_TO_STDOUT: false
- COVERAGE: true
- LOG_LEVEL: warn
- RAILS_LOG_TO_STDOUT: false
- COVERAGE: true
- LOG_LEVEL: warn
parallelism: 4
resource_class: large
@@ -46,7 +46,7 @@ jobs:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
nvm install v20
nvm install v16
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
@@ -64,6 +64,7 @@ jobs:
- ~/.bundle
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
# Only necessary if app uses webpacker or yarn in some other way
- restore_cache:
keys:
@@ -72,7 +73,7 @@ jobs:
- run:
name: yarn
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
command: yarn install --cache-folder ~/.cache/yarn
# Store yarn / webpacker cache
- save_cache:
@@ -81,7 +82,7 @@ jobs:
- ~/.cache/yarn
- run:
name: Download cc-test-reporter
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
@@ -103,8 +104,9 @@ jobs:
fi
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
# Database setup
- run: yarn install --check-files
- run: bundle exec rake db:create
- run: bundle exec rake db:schema:load
@@ -115,7 +117,7 @@ jobs:
- run:
name: Rubocop
command: bundle exec rubocop
# - run:
# name: Brakeman
# command: bundle exec brakeman
@@ -124,21 +126,6 @@ jobs:
name: eslint
command: yarn run eslint
# Run frontend tests
- run:
name: Run frontend tests
command: |
mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
--out ~/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"
# Run rails tests
- run:
name: Run backend tests
@@ -158,6 +145,20 @@ jobs:
command: |
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
- run:
name: Run frontend tests
command: |
mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
--out ~/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:
root: coverage
paths:
@@ -181,7 +182,7 @@ jobs:
- attach_workspace:
at: ~/build
- run:
name: Download cc-test-reporter
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
@@ -203,4 +204,4 @@ workflows:
- build
- upload-coverage:
requires:
- build
- build
+1 -1
View File
@@ -2,7 +2,7 @@
# It has the dependencies already installed so that codespace will boot up fast
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
# Do the set up required for chatwoot app
# Do the set up required for chatwoot app
WORKDIR /workspace
COPY . /workspace
RUN yarn && gem install bundler && bundle install
+11 -14
View File
@@ -1,14 +1,10 @@
ARG VARIANT
FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
ARG NODE_VERSION
ARG RUBY_VERSION
ARG USER_UID
ARG USER_GID
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 \
@@ -31,10 +27,10 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
tmux \
zsh \
git-flow \
npm \
libyaml-dev
npm
# Install rbenv and ruby
ARG RUBY_VERSION="3.2.2"
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
@@ -50,7 +46,7 @@ RUN rbenv install $RUBY_VERSION && \
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
&& chmod +x /usr/local/bin/overmind
# Install gh
@@ -60,7 +56,7 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | s
&& sudo apt install gh
# Do the set up required for chatwoot app
# Do the set up required for chatwoot app
WORKDIR /workspace
COPY . /workspace
@@ -69,7 +65,8 @@ COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install
# set up node js
RUN npm install n -g && \
n $NODE_VERSION
RUN npm install npm@latest -g && \
npm install n -g && \
n latest
RUN npm install --global yarn
RUN yarn
+13 -23
View File
@@ -1,41 +1,30 @@
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/python-3-postgres/.devcontainer/docker-compose.yml
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/ruby-rails/.devcontainer/devcontainer.json
#
#
version: '3'
services:
base:
build:
context: ..
dockerfile: .devcontainer/Dockerfile.base
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.2.2"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
image: base:latest
app:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.2.2"
# Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
VARIANT: 3
# [Choice] Install Node.js
INSTALL_NODE: "true"
NODE_VERSION: "lts/*"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
USER_UID: 1000
USER_GID: 1000
volumes:
- ..:/workspace:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
@@ -60,7 +49,8 @@ services:
restart: unless-stopped
image: mailhog/mailhog
network_mode: service:db
volumes:
postgres-data:
redis-data:
+3 -31
View File
@@ -1,9 +1,4 @@
# Learn about the various environment variables at
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
# Used to verify the integrity of signed cookies. so ensure a secure value is set
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
# Use `rake secret` to generate this variable
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
# Replace with the URL you are planning to use for your app
@@ -28,9 +23,6 @@ FORCE_SSL=false
ENABLE_ACCOUNT_SIGNUP=false
# Redis config
# specify the configs via single URL or individual variables
# ref: https://www.iana.org/assignments/uri-schemes/prov/redis
# You can also use the following format for the URL: redis://:password@host:port/db_number
REDIS_URL=redis://redis:6379
# If you are using docker-compose, set this variable's value to be any string,
# which will be the password for the redis service running inside the docker-compose
@@ -89,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
# Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled
MAILER_INBOUND_EMAIL_DOMAIN=
# Set this to the appropriate ingress channel with regards to incoming emails
# Set this to appropriate ingress channel with regards to incoming emails
# Possible values are :
# relay for Exim, Postfix, Qmail
# mailgun for Mailgun
@@ -99,20 +91,10 @@ MAILER_INBOUND_EMAIL_DOMAIN=
RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
# Set this to a password of your choice and use it in the Inbound webhook
RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY=
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
# Inbound webhook URL format:
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
# Note: Replace the values inside the brackets; do not include the brackets themselves.
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
# For Postmark
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
# Storage
ACTIVE_STORAGE_SERVICE=local
@@ -188,6 +170,8 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Sentry
# SENTRY_DSN=
## LogRocket
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
## Scout
## https://scoutapm.com/docs/ruby/configuration
@@ -212,8 +196,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
## Rack Attack configuration
## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=true
# RACK_ATTACK_LIMIT=300
# ENABLE_RACK_ATTACK_WIDGET_API=true
## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints
@@ -245,13 +227,3 @@ AZURE_APP_SECRET=
## Change these values to fine tune performance
# control the concurrency setting of sidekiq
# SIDEKIQ_CONCURRENCY=10
# AI powered features
## OpenAI key
# OPENAI_API_KEY=
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
+3 -8
View File
@@ -4,10 +4,9 @@ module.exports = {
'prettier',
'plugin:vue/recommended',
'plugin:storybook/recommended',
'plugin:cypress/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',
parser: 'babel-eslint',
ecmaVersion: 2020,
sourceType: 'module',
},
@@ -25,16 +24,13 @@ module.exports = {
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'import/no-unresolved': 'off',
'vue/html-indent': 'off',
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': [
'error',
{
singleline: {
max: 20,
},
singleline: 20,
multiline: {
max: 1,
allowFirstLine: false,
},
},
],
@@ -51,7 +47,6 @@ module.exports = {
},
],
'vue/no-v-html': 'off',
'vue/component-definition-name-casing': 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off'],
'no-console': 'error',
-2
View File
@@ -1,2 +0,0 @@
## All enterprise related files should be reviewed by sojan before merging
/enterprise/* @sojan-official
-45
View File
@@ -1,45 +0,0 @@
## github action to check deployment success
## curl the deployment url and check for 200 status
## deployment url will be of the form chatwoot-pr-<pr_number>.herokuapp.com
name: Deploy Check
on:
pull_request:
jobs:
deployment_check:
name: Check Deployment
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get install -y jq
- name: Print Deployment URL
run: echo "https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com"
- name: Check Deployment Status
run: |
max_attempts=10
attempt=1
status_code=0
echo "Waiting for review app to be deployed/redeployed, trying in 10 minutes..."
sleep 600
while [ $attempt -le $max_attempts ]; do
response=$(curl -s -o /dev/null -w "%{http_code}" https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
status_code=$(echo $response | head -n 1)
if [ $status_code -eq 200 ]; then
body=$(curl -s https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
if echo "$body" | jq -e '.version and .timestamp and .queue_services == "ok" and .data_services == "ok"' > /dev/null; then
echo "Deployment successful"
exit 0
else
echo "Deployment status unknown, retrying in 3 minutes..."
sleep 180
fi
else
echo "Waiting for review app to be ready, retrying in 3 minutes..."
sleep 180
attempt=$((attempt + 1))
fi
done
echo "Deployment failed after $max_attempts attempts"
exit 1
fi
-23
View File
@@ -1,23 +0,0 @@
# ref: https://github.com/amannn/action-semantic-pull-request
# ensure PR title is in semantic format
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,55 +0,0 @@
name: Log Lines Percentage Check
on:
pull_request:
branches:
- develop
jobs:
log_lines_check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check for log lines and calculate percentage
run: |
# Define the log line pattern
LOG_LINE_PATTERN="Rails\.logger"
# Get the list of changed files in the pull request
CHANGED_FILES=$(git diff --name-only)
# Initialize a flag to track if any files have insufficient log lines
INSUFFICIENT_LOGS=0
for file in $CHANGED_FILES; do
if [[ $file =~ \.rb$ && ! $file =~ _spec\.rb$ ]]; then
# Count the total number of lines in the file
total_lines=$(wc -l < "$file")
# Count the number of log lines in the file
log_lines=$(grep -c "$LOG_LINE_PATTERN" "$file")
# Calculate the percentage of log lines
if [ "$total_lines" -gt 0 ]; then
percentage=$(awk "BEGIN { pc=100*${log_lines}/${total_lines}; i=int(pc); print (pc-i<0.5)?i:i+1 }")
else
percentage=0
fi
# Check if the percentage is less than 5%
if [ "$percentage" -lt 5 ]; then
echo "Error: Log lines percentage is less than 5% ($percentage%) in $file. Please add more log lines using Rails.logger statements."
INSUFFICIENT_LOGS=1
else
echo "Log lines percentage is $percentage% in $file. Code looks good!"
fi
fi
done
# If any files have insufficient log lines, fail the action
if [ "$INSUFFICIENT_LOGS" -eq 1 ]; then
exit 1
fi
+3 -3
View File
@@ -4,7 +4,7 @@
# # 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.
# #
@@ -35,7 +35,7 @@ jobs:
run: |
sudo ./install.sh --install < input
# disabling http verify for now as http
# disabling http verify for now as http
# access to port 3000 fails in gh action env
# - name: Verify
# if: always()
@@ -45,7 +45,7 @@ jobs:
# curl http://localhost:3000/api
- name: Upload chatwoot setup log file as an artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
if: always()
with:
name: chatwoot-setup-log-file
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
@@ -19,6 +19,5 @@ jobs:
- name: Build the Codespace Base Image
run: |
docker-compose -f .devcontainer/docker-compose.yml build base
docker tag base:latest ghcr.io/chatwoot/chatwoot_codespace:latest
docker build . -t ghcr.io/chatwoot/chatwoot_codespace:latest -f .devcontainer/Dockerfile.base
docker push ghcr.io/chatwoot/chatwoot_codespace:latest
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
+5 -9
View File
@@ -18,12 +18,11 @@ jobs:
runs-on: ubuntu-20.04
services:
postgres:
image: postgres:15.3
image: postgres:10.8
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ""
POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
# needed because the postgres container does not provide a healthcheck
@@ -41,7 +40,7 @@ jobs:
options: --entrypoint redis-server
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -50,10 +49,9 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 20
cache: yarn
node-version: 16
- name: yarn
run: yarn install
@@ -76,11 +74,9 @@ jobs:
- name: Run backend tests
run: |
bundle exec rspec --profile=10 --format documentation
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
if: always()
with:
name: rails-log-folder
@@ -1,85 +0,0 @@
# #
# # This workflow will run specs related to response bot
# # This can only be activated in installations Where vector extension is available.
# #
name: Run Response Bot spec
on:
push:
branches:
- develop
- master
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-20.04
services:
postgres:
image: ankane/pgvector
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ""
POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: trust
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@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
- name: yarn
run: yarn install
- name: Create database
run: bundle exec rake db:create
- name: Seed database
run: bundle exec rake db:schema:load
- name: Enable ResponseBotService in installation
run: RAILS_ENV=test bundle exec rails runner "Features::ResponseBotService.new.enable_in_installation"
# Run Response Bot specs
- name: Run backend tests
run: |
bundle exec rspec \
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \
--profile=10 \
--format documentation
- name: Upload rails log folder
uses: actions/upload-artifact@v4
if: always()
with:
name: rails-log-folder
path: log
-41
View File
@@ -1,41 +0,0 @@
name: Run Size Limit Check
on:
pull_request:
branches:
- develop
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
- name: yarn
run: yarn install
- name: Strip enterprise code
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: Run asset compile
run: bundle exec rake assets:precompile
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Size Check
run: yarn run size
-2
View File
@@ -74,5 +74,3 @@ yalc.lock
/yarn-error.log
yarn-debug.log*
.yarn-integrity
/storybook-static
+1 -1
View File
@@ -1 +1 @@
20.5.1
14.17.4
+78 -28
View File
@@ -2,19 +2,21 @@ require:
- rubocop-performance
- rubocop-rails
- rubocop-rspec
- ./rubocop/use_from_email.rb
- ./rubocop/custom_cop_location.rb
inherit_from: .rubocop_todo.yml
Layout/LineLength:
Max: 150
Metrics/ClassLength:
Max: 175
Max: 125
Exclude:
- 'app/models/message.rb'
- 'app/models/conversation.rb'
Metrics/MethodLength:
Max: 19
- 'app/models/contact.rb'
- 'app/mailers/conversation_reply_mailer.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'
RSpec/ExampleLength:
Max: 25
Style/Documentation:
@@ -52,7 +54,6 @@ Lint/OrAssignmentToConstant:
Exclude:
- 'lib/redis/config.rb'
Metrics/BlockLength:
Max: 30
Exclude:
- spec/**/*
- '**/routes.rb'
@@ -61,7 +62,6 @@ Metrics/BlockLength:
Metrics/ModuleLength:
Exclude:
- lib/seeders/message_seeder.rb
- spec/support/slack_stubs.rb
Rails/ApplicationController:
Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb'
@@ -73,7 +73,7 @@ Rails/ApplicationController:
- 'app/controllers/survey/responses_controller.rb'
Rails/FindEach:
Enabled: true
Include:
Include:
- 'app/**/*.rb'
Rails/CompactBlank:
Enabled: false
@@ -87,7 +87,6 @@ Style/ClassAndModuleChildren:
EnforcedStyle: compact
Exclude:
- 'config/application.rb'
- 'config/initializers/monkey_patches/*'
Style/MapToHash:
Enabled: false
Style/HashSyntax:
@@ -105,53 +104,95 @@ RSpec/FactoryBot/SyntaxMethods:
Enabled: false
Naming/VariableNumber:
Enabled: false
Naming/MemoizedInstanceVariableName:
Metrics/MethodLength:
Exclude:
- 'app/models/message.rb'
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
Rails/CreateTableWithTimestamps:
Exclude:
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
Style/GuardClause:
Exclude:
- 'app/builders/account_builder.rb'
- 'app/models/attachment.rb'
- 'app/models/message.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Metrics/AbcSize:
Max: 26
Exclude:
- 'app/controllers/concerns/auth_helper.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.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:
Max: 7
Exclude:
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Rails/ReversibleMigration:
Exclude:
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
- 'db/migrate/20161025070645_remchannel.rb'
- 'db/migrate/20161025070645_remchannel.rb'
- 'db/migrate/20161110102609_removeinboxid.rb'
- 'db/migrate/20170519091539_add_avatar_to_fb.rb'
- 'db/migrate/20191020085608_rename_old_tables.rb'
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
Rails/BulkChangeTable:
Exclude:
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
- 'db/migrate/20200121190901_create_account_users.rb'
- 'db/migrate/20170211092540_notnullableusers.rb'
- 'db/migrate/20170403095203_contactadder.rb'
- 'db/migrate/20170406104018_add_default_status_conv.rb'
- 'db/migrate/20170511134418_latlong.rb'
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
Rails/UniqueValidationWithoutIndex:
Exclude:
- 'app/models/channel/twitter_profile.rb'
- 'app/models/webhook.rb'
- 'app/models/contact.rb'
- 'app/models/integrations/hook.rb'
- 'app/models/canned_response.rb'
- 'app/models/telegram_bot.rb'
Rails/RenderInline:
Exclude:
- 'app/controllers/swagger_controller.rb'
Performance/CollectionLiteralInLoop:
Exclude:
- 'db/migrate/20210315101919_enable_email_channel.rb'
Rails/ThreeStateBooleanColumn:
Exclude:
- 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb'
- 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb'
- 'db/migrate/20200606132552_create_labels.rb'
- 'db/migrate/20201027135006_create_working_hours.rb'
- 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb'
- 'db/migrate/20210114202310_create_teams.rb'
- 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb'
- 'db/migrate/20210428135041_add_campaigns.rb'
- 'db/migrate/20210602182058_add_hmac_to_api_channel.rb'
- 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb'
- 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb'
- 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb'
- 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb'
- 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb'
- 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb'
- 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb'
- 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb'
- 'db/migrate/20220511072655_add_archive_column_to_portal.rb'
- 'db/migrate/20230503101201_create_sla_policies.rb'
RSpec/IndexedLet:
Enabled: false
RSpec/NamedSubject:
Enabled: false
# we should bring this down
RSpec/MultipleExpectations:
Max: 7
RSpec/MultipleMemoizedHelpers:
Max: 14
# custom rules
UseFromEmail:
Enabled: true
Exclude:
- 'app/models/user.rb'
- 'app/models/contact.rb'
CustomCopLocation:
Enabled: true
AllCops:
NewCops: enable
Exclude:
@@ -165,4 +206,13 @@ AllCops:
- 'config/environments/**/*'
- 'tmp/**/*'
- 'storage/**/*'
- 'db/migrate/20230426130150_init_schema.rb'
- 'db/migrate/20200225162150_init_schema.rb'
- 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.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
+287
View File
@@ -0,0 +1,287 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-10-23 16:47:02 +0530 using RuboCop version 0.73.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 1
Lint/DuplicateMethods:
Exclude:
- 'app/controllers/api/v1/reports_controller.rb'
# Offense count: 1
Lint/RescueException:
Exclude:
- 'app/builders/messages/message_builder.rb'
# Offense count: 4
Lint/ShadowingOuterLocalVariable:
Exclude:
- 'app/controllers/api/v1/reports_controller.rb'
# Offense count: 3
# Configuration parameters: AllowKeywordBlockArguments.
Lint/UnderscorePrefixedVariableName:
Exclude:
- 'app/models/account.rb'
- 'deploy/before_symlink.rb'
# Offense count: 18
Lint/UselessAssignment:
Exclude:
- 'app/controllers/api/v1/callbacks_controller.rb'
- 'app/controllers/api/v1/facebook_indicators_controller.rb'
- 'app/listeners/action_cable_listener.rb'
- 'app/listeners/reporting_listener.rb'
- 'app/models/channel/facebook_page.rb'
- 'app/models/facebook_page.rb'
# Offense count: 14
Metrics/AbcSize:
Max: 26
# Offense count: 1
# Configuration parameters: CountComments, ExcludedMethods.
# ExcludedMethods: refine
Metrics/BlockLength:
Max: 30
# Offense count: 2
Metrics/CyclomaticComplexity:
Max: 7
# Offense count: 10
# Configuration parameters: CountComments, ExcludedMethods.
Metrics/MethodLength:
Max: 19
# Offense count: 1
Metrics/PerceivedComplexity:
Max: 8
# Offense count: 6
Naming/AccessorMethodName:
Exclude:
- 'app/builders/report_builder.rb'
- 'app/controllers/api/v1/accounts_controller.rb'
- 'app/controllers/api/v1/callbacks_controller.rb'
- 'app/controllers/api/v1/conversations_controller.rb'
# Offense count: 9
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
Naming/MemoizedInstanceVariableName:
Exclude:
- 'app/controllers/api/base_controller.rb'
- 'app/controllers/api/v1/conversations_controller.rb'
- 'app/controllers/api/v1/webhooks_controller.rb'
- 'app/controllers/application_controller.rb'
- 'app/models/message.rb'
- 'lib/integrations/widget/outgoing_message_builder.rb'
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge:
Exclude:
- 'app/controllers/api/v1/callbacks_controller.rb'
- 'app/models/message.rb'
# Offense count: 1
# Cop supports --auto-correct.
Performance/StringReplacement:
Exclude:
- 'lib/events/base.rb'
# Offense count: 4
# Configuration parameters: Prefixes.
# Prefixes: when, with, without
RSpec/ContextWording:
Exclude:
- 'spec/models/contact_spec.rb'
- 'spec/models/user_spec.rb'
# Offense count: 1
RSpec/DescribeClass:
Exclude:
- 'spec/mailers/confirmation_instructions_spec.rb'
# Offense count: 1
RSpec/DescribeSymbol:
Exclude:
- 'spec/mailers/confirmation_instructions_spec.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: AllowConsecutiveOneLiners.
RSpec/EmptyLineAfterExample:
Exclude:
- 'spec/models/user_spec.rb'
# Offense count: 1
# Configuration parameters: Max.
RSpec/ExampleLength:
Exclude:
- 'spec/models/conversation_spec.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_line_only, single_statement_only, disallow
RSpec/ImplicitSubject:
Exclude:
- 'spec/models/user_spec.rb'
# Offense count: 7
# Configuration parameters: AggregateFailuresByDefault.
RSpec/MultipleExpectations:
Max: 7
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: not_to, to_not
RSpec/NotToNot:
Exclude:
- 'spec/mailers/confirmation_instructions_spec.rb'
# Offense count: 1
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
RSpec/VerifiedDoubles:
Exclude:
- 'spec/models/conversation_spec.rb'
# Offense count: 4
# Cop supports --auto-correct.
Rails/ActiveRecordAliases:
Exclude:
- 'app/controllers/api/v1/agents_controller.rb'
- 'app/controllers/api/v1/callbacks_controller.rb'
- 'app/controllers/api/v1/canned_responses_controller.rb'
- 'app/controllers/api/v1/contacts_controller.rb'
# Offense count: 2
# Cop supports --auto-correct.
Rails/BelongsTo:
Exclude:
- 'app/models/message.rb'
- 'app/models/user.rb'
# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/EnumHash:
Exclude:
- 'app/models/attachment.rb'
- 'app/models/conversation.rb'
- 'app/models/message.rb'
- 'app/models/user.rb'
# Offense count: 1
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasManyOrHasOneDependent:
Exclude:
- 'app/models/user.rb'
# Offense count: 1
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/InverseOf:
Exclude:
- 'app/models/user.rb'
# Offense count: 1
# Configuration parameters: Include.
# Include: app/controllers/**/*.rb
Rails/LexicallyScopedActionFilter:
Exclude:
- 'app/controllers/home_controller.rb'
# Offense count: 2
# Configuration parameters: Include.
# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb
Rails/Output:
Exclude:
- 'app/bot/bot.rb'
- 'app/builders/account_builder.rb'
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: strict, flexible
Rails/TimeZone:
Exclude:
- 'app/builders/report_builder.rb'
- 'lib/reports/update_account_identity.rb'
- 'lib/reports/update_agent_identity.rb'
- 'lib/reports/update_identity.rb'
- 'spec/models/conversation_spec.rb'
# Offense count: 8
# Cop supports --auto-correct.
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/Validation:
Exclude:
- 'app/models/canned_response.rb'
- 'app/models/channel/facebook_page.rb'
- 'app/models/facebook_page.rb'
- 'app/models/telegram_bot.rb'
- 'app/models/user.rb'
# Offense count: 15
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle.
# SupportedStyles: nested, compact
Style/ClassAndModuleChildren:
Exclude:
- 'app/builders/messages/message_builder.rb'
- 'app/controllers/api/v1/inbox_members_controller.rb'
- 'app/models/channel/facebook_page.rb'
- 'app/models/channel/web_widget.rb'
- 'app/presenters/conversations/event_data_presenter.rb'
- 'app/services/facebook/send_reply_service.rb'
- 'lib/integrations/facebook/delivery_status.rb'
- 'lib/integrations/facebook/message_creator.rb'
- 'lib/integrations/facebook/message_parser.rb'
- 'lib/integrations/widget/incoming_message_builder.rb'
# Offense count: 4
Style/CommentedKeyword:
Exclude:
- 'app/controllers/api/v1/callbacks_controller.rb'
- 'app/controllers/api/v1/conversations/assignments_controller.rb'
- 'app/controllers/api/v1/conversations/labels_controller.rb'
- 'app/controllers/api/v1/labels_controller.rb'
# Offense count: 1
# Configuration parameters: AllowIfModifier.
Style/IfInsideElse:
Exclude:
- 'app/finders/conversation_finder.rb'
# Offense count: 1
Style/MixinUsage:
Exclude:
- 'app/bot/bot.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'app/controllers/api/v1/callbacks_controller.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: implicit, explicit
Style/RescueStandardError:
Exclude:
- 'app/models/channel/facebook_page.rb'
+1 -13
View File
@@ -23,18 +23,6 @@ module.exports = {
},
'@storybook/addon-links',
'@storybook/addon-essentials',
{
/**
* Fix Storybook issue with PostCSS@8
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
*/
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
webpackFinal: config => {
const newConfig = {
@@ -47,7 +35,7 @@ module.exports = {
newConfig.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
use: ['style-loader', 'css-loader', 'sass-loader'],
include: path.resolve(__dirname, '../app/javascript'),
});
-4
View File
@@ -4,12 +4,10 @@ import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import Vuelidate from 'vuelidate';
import Multiselect from 'vue-multiselect';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import WootUiKit from '../app/javascript/dashboard/components';
import i18n from '../app/javascript/dashboard/i18n';
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
import '../app/javascript/dashboard/assets/scss/storybook.scss';
@@ -17,8 +15,6 @@ Vue.use(VueI18n);
Vue.use(Vuelidate);
Vue.use(WootUiKit);
Vue.use(Vuex);
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
Vue.component('multiselect', Multiselect);
Vue.component('fluent-icon', FluentIcon);
+24 -38
View File
@@ -3,8 +3,8 @@ source 'https://rubygems.org'
ruby '3.2.2'
##-- base gems for rails --##
gem 'rack-cors', '2.0.0', require: 'rack/cors'
gem 'rails', '~> 7.0.8.4'
gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 7'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@@ -15,7 +15,7 @@ gem 'browser'
gem 'hashie'
gem 'jbuilder'
gem 'kaminari'
gem 'responders', '>= 3.1.1'
gem 'responders'
gem 'rest-client'
gem 'telephone_number'
gem 'time_diff'
@@ -34,13 +34,11 @@ gem 'commonmarker'
# Validate Data against JSON Schema
gem 'json_schemer'
# Rack middleware for blocking & throttling abusive requests
gem 'rack-attack', '>= 6.7.0'
gem 'rack-attack'
# a utility tool for streaming, flexible and safe downloading of remote files
gem 'down'
# authentication type to fetch and send mail over oauth2.0
gem 'gmail_xoauth'
# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2
gem 'net-smtp', '~> 0.3.4'
# Prevent CSV injection
gem 'csv-safe'
@@ -61,7 +59,7 @@ gem 'redis-namespace'
gem 'activerecord-import'
##--- gems for server & infra configuration ---##
gem 'dotenv-rails', '>= 3.0.0'
gem 'dotenv-rails'
gem 'foreman'
gem 'puma'
gem 'webpacker'
@@ -69,16 +67,14 @@ gem 'webpacker'
gem 'barnes'
##--- gems for authentication & authorization ---##
gem 'devise', '>= 4.9.4'
gem 'devise'
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
gem 'devise_token_auth', '>= 1.2.3'
gem 'devise_token_auth'
# authorization
gem 'jwt'
gem 'pundit'
# super admin
gem 'administrate', '>= 0.20.1'
gem 'administrate-field-active_storage', '>= 1.0.3'
gem 'administrate-field-belongs_to_search', '>= 0.9.0'
gem 'administrate'
##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -94,7 +90,7 @@ gem 'twitty', '~> 0.1.5'
# facebook client
gem 'koala'
# slack client
gem 'slack-ruby-client', '~> 2.2.0'
gem 'slack-ruby-client', '~> 2.0.0'
# for dialogflow integrations
gem 'google-cloud-dialogflow-v2'
gem 'grpc'
@@ -109,20 +105,20 @@ gem 'google-cloud-translate-v3'
gem 'ddtrace', require: false
gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'newrelic-sidekiq-metrics', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.14.0', require: false
gem 'sentry-rails', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.15.0', require: false
gem 'sentry-sidekiq', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.2.4'
gem 'sidekiq'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0'
gem 'sidekiq-cron'
##-- Push notification service --##
gem 'fcm'
gem 'web-push', '>= 3.0.1'
gem 'web-push'
##-- geocoding / parse location from ip --##
# http://www.rubygeocoder.com/
@@ -156,24 +152,17 @@ gem 'stripe'
gem 'faker'
# Include logrange conditionally in intializer using env variable
gem 'lograge', '~> 0.14.0', require: false
gem 'lograge', '~> 0.12.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.1'
gem 'audited', '~> 5.3'
# need for google auth
gem 'omniauth', '>= 2.1.2'
gem 'omniauth-google-oauth2', '>= 1.1.2'
gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2'
## Gems for reponse bot
# adds cosine similarity to postgres using vector extension
gem 'neighbor'
gem 'pgvector'
# Convert Website HTML to Markdown
gem 'reverse_markdown'
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
### Gems required only in specific deployment environments ###
##############################################################
@@ -188,7 +177,7 @@ group :development do
gem 'bullet'
gem 'letter_opener'
gem 'scss_lint', require: false
gem 'web-console', '>= 4.2.1'
gem 'web-console'
# used in swagger build
gem 'json_refs'
@@ -197,10 +186,8 @@ group :development do
gem 'squasher'
# profiling
gem 'rack-mini-profiler', '>= 3.2.0', require: false
gem 'rack-mini-profiler', require: false
gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request', '>= 0.8.0'
end
group :test do
@@ -222,13 +209,12 @@ group :development, :test do
gem 'bundle-audit', require: false
gem 'byebug', platform: :mri
gem 'climate_control'
gem 'debug', '~> 1.8'
gem 'factory_bot_rails', '>= 6.4.3'
gem 'factory_bot_rails'
gem 'listen'
gem 'mock_redis'
gem 'pry-rails'
gem 'rspec_junit_formatter'
gem 'rspec-rails', '>= 6.0.3'
gem 'rspec-rails'
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
+208 -255
View File
@@ -33,70 +33,70 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
actioncable (7.0.4.3)
actionpack (= 7.0.4.3)
activesupport (= 7.0.4.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
actionmailbox (7.0.4.3)
actionpack (= 7.0.4.3)
activejob (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8.4)
actionpack (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activesupport (= 7.0.8.4)
actionmailer (7.0.4.3)
actionpack (= 7.0.4.3)
actionview (= 7.0.4.3)
activejob (= 7.0.4.3)
activesupport (= 7.0.4.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8.4)
actionview (= 7.0.8.4)
activesupport (= 7.0.8.4)
rack (~> 2.0, >= 2.2.4)
actionpack (7.0.4.3)
actionview (= 7.0.4.3)
activesupport (= 7.0.4.3)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.4)
actionpack (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
actiontext (7.0.4.3)
actionpack (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8.4)
activesupport (= 7.0.8.4)
actionview (7.0.4.3)
activesupport (= 7.0.4.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
activejob (7.0.8.4)
activesupport (= 7.0.8.4)
activejob (7.0.4.3)
activesupport (= 7.0.4.3)
globalid (>= 0.3.6)
activemodel (7.0.8.4)
activesupport (= 7.0.8.4)
activerecord (7.0.8.4)
activemodel (= 7.0.8.4)
activesupport (= 7.0.8.4)
activemodel (7.0.4.3)
activesupport (= 7.0.4.3)
activerecord (7.0.4.3)
activemodel (= 7.0.4.3)
activesupport (= 7.0.4.3)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activestorage (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activesupport (= 7.0.8.4)
activestorage (7.0.4.3)
actionpack (= 7.0.4.3)
activejob (= 7.0.4.3)
activerecord (= 7.0.4.3)
activesupport (= 7.0.4.3)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8.4)
activesupport (7.0.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -105,30 +105,22 @@ GEM
activerecord (>= 6.0, < 7.1)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
administrate (0.20.1)
actionpack (>= 6.0, < 8.0)
actionview (>= 6.0, < 8.0)
activerecord (>= 6.0, < 8.0)
jquery-rails (~> 4.6.0)
kaminari (~> 1.2.2)
administrate (0.18.0)
actionpack (>= 5.0)
actionview (>= 5.0)
activerecord (>= 5.0)
jquery-rails (>= 4.0)
kaminari (>= 1.0)
sassc-rails (~> 2.1)
selectize-rails (~> 0.6)
administrate-field-active_storage (1.0.3)
administrate (>= 0.2.2)
rails (>= 7.0)
administrate-field-belongs_to_search (0.9.0)
administrate (>= 0.3, < 1.0)
jbuilder (~> 2)
rails (>= 4.2, < 7.2)
selectize-rails (~> 0.6)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_extras (7.1.0)
audited (5.4.1)
activerecord (>= 5.0, < 7.7)
activesupport (>= 5.0, < 7.7)
audited (5.3.3)
activerecord (>= 5.0, < 7.1)
request_store (~> 1.2)
aws-eventstream (1.2.0)
aws-partitions (1.760.0)
aws-sdk-core (3.171.1)
@@ -148,15 +140,13 @@ GEM
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.7)
bcrypt (3.1.18)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.1)
browser (5.3.1)
builder (3.3.0)
builder (3.2.4)
bullet (7.0.7)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
@@ -168,14 +158,14 @@ GEM
byebug (11.1.3)
climate_control (1.2.0)
coderay (1.1.3)
commonmarker (0.23.10)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
commonmarker (0.23.9)
concurrent-ruby (1.2.2)
connection_pool (2.4.0)
crack (0.4.5)
rexml
crass (1.0.6)
csv-safe (3.2.1)
cypress-on-rails (1.16.0)
cypress-on-rails (1.13.1)
rack
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
@@ -183,37 +173,34 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
date (3.3.3)
ddtrace (1.11.1)
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
libdatadog (~> 2.0.0.1.0)
libddwaf (~> 1.8.2.0.0)
msgpack
debase-ruby_core_source (3.2.0)
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
declarative (0.0.20)
devise (4.9.4)
devise (4.9.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise_token_auth (1.2.3)
devise_token_auth (1.2.1)
bcrypt (~> 3.0)
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 7.2)
diff-lcs (1.5.1)
rails (>= 4.2.0, < 7.1)
diff-lcs (1.5.0)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (3.1.2)
dotenv-rails (3.1.2)
dotenv (= 3.1.2)
railties (>= 6.1)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
down (5.4.0)
addressable (~> 2.8)
ecma-re-validator (0.4.0)
@@ -230,15 +217,16 @@ GEM
facebook-messenger (2.0.1)
httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5)
factory_bot (6.4.5)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday (2.7.4)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-mashify (0.1.1)
@@ -246,8 +234,7 @@ GEM
hashie
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.1.0)
net-http
faraday-net_http (3.0.2)
faraday-net_http_persistent (2.1.0)
faraday (~> 2.5)
net-http-persistent (~> 4.0)
@@ -256,13 +243,13 @@ GEM
fcm (1.0.8)
faraday (>= 1.0.0, < 3.0)
googleauth (~> 1)
ffi (1.16.3)
ffi (1.15.5)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
flag_shih_tzu (0.3.23)
foreman (0.87.2)
fugit (1.9.0)
fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
gapic-common (0.18.0)
@@ -274,10 +261,10 @@ GEM
googleauth (~> 1.0)
grpc (~> 1.36)
geocoder (1.8.1)
gli (2.21.1)
globalid (1.2.1)
activesupport (>= 6.1)
gmail_xoauth (0.4.3)
gli (2.21.0)
globalid (1.1.0)
activesupport (>= 5.0)
gmail_xoauth (0.4.2)
oauth (>= 0.3.6)
google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1)
@@ -316,16 +303,16 @@ GEM
google-cloud-translate-v3 (0.6.0)
gapic-common (>= 0.17.1, < 2.a)
google-cloud-errors (~> 1.0)
google-protobuf (3.25.3)
google-protobuf (3.25.3-arm64-darwin)
google-protobuf (3.25.3-x86_64-darwin)
google-protobuf (3.25.3-x86_64-linux)
google-protobuf (3.22.3)
google-protobuf (3.22.3-arm64-darwin)
google-protobuf (3.22.3-x86_64-darwin)
google-protobuf (3.22.3-x86_64-linux)
googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27)
googleapis-common-protos-types (1.14.0)
google-protobuf (~> 3.18)
googleapis-common-protos-types (1.6.0)
google-protobuf (~> 3.14)
googleauth (1.5.2)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
@@ -335,17 +322,14 @@ GEM
signet (>= 0.16, < 2.a)
groupdate (6.2.1)
activesupport (>= 5.2)
grpc (1.62.0)
google-protobuf (~> 3.25)
grpc (1.54.0)
google-protobuf (~> 3.21)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-arm64-darwin)
google-protobuf (~> 3.25)
grpc (1.54.0-x86_64-darwin)
google-protobuf (~> 3.21)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-x86_64-darwin)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-x86_64-linux)
google-protobuf (~> 3.25)
grpc (1.54.0-x86_64-linux)
google-protobuf (~> 3.21)
googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1)
hairtrigger (1.0.0)
@@ -355,6 +339,7 @@ GEM
hana (1.3.7)
hashdiff (1.0.1)
hashie (5.0.0)
hkdf (1.0.0)
http (5.1.1)
addressable (~> 2.8)
http-cookie (~> 1.0)
@@ -368,19 +353,16 @@ GEM
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.14.5)
i18n (1.13.0)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.6.0)
irb (1.7.2)
reline (>= 0.3.6)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.6.2)
jquery-rails (4.6.0)
jquery-rails (4.5.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
@@ -392,8 +374,7 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.8.1)
base64
jwt (2.7.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -434,69 +415,61 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.14.0)
lograge (0.12.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.22.0)
loofah (2.20.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
nokogiri (>= 1.5.9)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
marcel (1.0.2)
maxminddb (0.1.22)
memoist (0.16.2)
meta_request (0.8.2)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 8)
method_source (1.1.0)
method_source (1.0.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.23.1)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
minitest (5.18.0)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.3.0)
neighbor (0.2.3)
activerecord (>= 5.2)
net-http (0.4.1)
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.12)
net-imap (0.3.4)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
net-protocol (0.2.1)
timeout
net-smtp (0.3.4)
net-smtp (0.3.3)
net-protocol
netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.2)
newrelic_rpm (>= 8.0.0)
newrelic-sidekiq-metrics (1.6.1)
newrelic_rpm (~> 8)
sidekiq
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
nokogiri (1.16.5)
mini_portile2 (~> 2.8.2)
newrelic_rpm (8.16.0)
nio4r (2.5.9)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.16.5-arm64-darwin)
nokogiri (1.14.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.5-x86_64-darwin)
nokogiri (1.14.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.5-x86_64-linux)
nokogiri (1.14.3-x86_64-linux)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@@ -511,22 +484,22 @@ GEM
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
omniauth (2.1.2)
omniauth (2.1.1)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-google-oauth2 (1.1.2)
omniauth-google-oauth2 (1.1.1)
jwt (>= 2.0)
oauth2 (~> 2.0)
oauth2 (~> 2.0.6)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8)
omniauth-oauth2 (~> 1.8.0)
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
omniauth-rails_csrf_protection (1.0.2)
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
openssl (3.2.0)
openssl (3.1.0)
orm_adapter (0.5.0)
os (1.1.4)
parallel (1.23.0)
@@ -536,7 +509,6 @@ GEM
pg_search (2.3.6)
activerecord (>= 5.2)
activesupport (>= 5.2)
pgvector (0.1.1)
procore-sift (1.0.0)
activerecord (>= 6.1)
pry (0.14.2)
@@ -545,78 +517,71 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
puma (6.4.2)
puma (6.2.2)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.0)
rack (2.2.9)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
rack (< 4)
rack-cors (2.0.0)
racc (1.6.2)
rack (2.2.7)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-mini-profiler (3.2.0)
rack-mini-profiler (3.1.0)
rack (>= 1.2.0)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (3.0.6)
rack
rack-proxy (0.7.6)
rack
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rails (7.0.8.4)
actioncable (= 7.0.8.4)
actionmailbox (= 7.0.8.4)
actionmailer (= 7.0.8.4)
actionpack (= 7.0.8.4)
actiontext (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activemodel (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
rails (7.0.4.3)
actioncable (= 7.0.4.3)
actionmailbox (= 7.0.4.3)
actionmailer (= 7.0.4.3)
actionpack (= 7.0.4.3)
actiontext (= 7.0.4.3)
actionview (= 7.0.4.3)
activejob (= 7.0.4.3)
activemodel (= 7.0.4.3)
activerecord (= 7.0.4.3)
activestorage (= 7.0.4.3)
activesupport (= 7.0.4.3)
bundler (>= 1.15.0)
railties (= 7.0.8.4)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
railties (= 7.0.4.3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
railties (7.0.4.3)
actionpack (= 7.0.4.3)
activesupport (= 7.0.4.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.2.1)
rake (13.0.6)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.22.1)
redis-client (0.14.1)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
regexp_parser (2.8.0)
reline (0.3.6)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.1)
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
rest-client (2.1.0)
@@ -625,27 +590,24 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.2.8)
strscan (>= 3.0.9)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
rexml (3.2.5)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.2)
rspec-support (~> 3.12.0)
rspec-rails (6.0.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.0)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.50.2)
@@ -703,24 +665,23 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.17.3)
sentry-rails (5.9.0)
railties (>= 5.0)
sentry-ruby (~> 5.17.3)
sentry-ruby (5.17.3)
bigdecimal
sentry-ruby (~> 5.9.0)
sentry-ruby (5.9.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.17.3)
sentry-ruby (~> 5.17.3)
sentry-sidekiq (5.9.0)
sentry-ruby (~> 5.9.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.2.4)
sidekiq (7.1.0)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.19.0)
sidekiq-cron (1.12.0)
redis-client (>= 0.14.0)
sidekiq-cron (1.10.0)
fugit (~> 1.8)
globalid (>= 1.0.1)
sidekiq (>= 6)
@@ -734,12 +695,13 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-ruby-client (2.2.0)
slack-ruby-client (2.0.0)
faraday (>= 2.0)
faraday-mashify
faraday-multipart
gli
hashie
websocket-driver
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
@@ -747,7 +709,7 @@ GEM
spring-watcher-listen (2.1.0)
listen (>= 2.7, < 4.0)
spring (>= 4)
sprockets (4.2.1)
sprockets (4.2.0)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2)
@@ -758,15 +720,14 @@ GEM
stackprof (0.2.25)
statsd-ruby (1.5.0)
stripe (8.5.0)
strscan (3.1.0)
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.3.1)
tilt (2.3.0)
thor (1.2.1)
tilt (2.1.0)
time_diff (0.3.0)
activesupport
i18n
timeout (0.4.1)
timeout (0.3.2)
trailblazer-option (0.1.2)
twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0)
@@ -786,20 +747,20 @@ GEM
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
uri (0.13.0)
uri_template (0.7.0)
valid_email2 (4.0.6)
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.4)
version_gem (1.1.2)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
web-push (3.0.1)
web-push (3.0.0)
hkdf (~> 1.0)
jwt (~> 2.0)
openssl (~> 3.0)
webmock (3.18.1)
@@ -812,19 +773,19 @@ GEM
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.8.1)
websocket-driver (0.7.6)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wisper (2.0.0)
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.15)
zeitwerk (2.6.8)
PLATFORMS
arm64-darwin-20
arm64-darwin-21
arm64-darwin-22
arm64-darwin-21
ruby
x86_64-darwin-18
x86_64-darwin-20
@@ -836,12 +797,10 @@ DEPENDENCIES
active_record_query_trace
activerecord-import
acts-as-taggable-on
administrate (>= 0.20.1)
administrate-field-active_storage (>= 1.0.3)
administrate-field-belongs_to_search (>= 0.9.0)
administrate
annotate
attr_extras
audited (~> 5.4, >= 5.4.1)
audited (~> 5.3)
aws-sdk-s3
azure-storage-blob!
barnes
@@ -857,16 +816,15 @@ DEPENDENCIES
cypress-on-rails
database_cleaner
ddtrace
debug (~> 1.8)
devise (>= 4.9.4)
devise
devise-secure_password!
devise_token_auth (>= 1.2.3)
dotenv-rails (>= 3.0.0)
devise_token_auth
dotenv-rails
down
elastic-apm
email_reply_trimmer
facebook-messenger
factory_bot_rails (>= 6.4.3)
factory_bot_rails
faker
fcm
flag_shih_tzu
@@ -893,36 +851,31 @@ DEPENDENCIES
line-bot-api
liquid
listen
lograge (~> 0.14.0)
lograge (~> 0.12.0)
maxminddb
meta_request (>= 0.8.0)
mock_redis
neighbor
net-smtp (~> 0.3.4)
newrelic-sidekiq-metrics (>= 1.6.2)
newrelic-sidekiq-metrics
newrelic_rpm
omniauth (>= 2.1.2)
omniauth-google-oauth2 (>= 1.1.2)
omniauth
omniauth-google-oauth2
omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2)
omniauth-rails_csrf_protection (~> 1.0)
pg
pg_search
pgvector
procore-sift
pry-rails
puma
pundit
rack-attack (>= 6.7.0)
rack-cors (= 2.0.0)
rack-mini-profiler (>= 3.2.0)
rack-attack
rack-cors
rack-mini-profiler
rack-timeout
rails (~> 7.0.8.4)
rails (~> 7)
redis
redis-namespace
responders (>= 3.1.1)
responders
rest-client
reverse_markdown
rspec-rails (>= 6.0.3)
rspec-rails
rspec_junit_formatter
rubocop
rubocop-performance
@@ -931,14 +884,14 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.14.0)
sentry-rails
sentry-ruby
sentry-sidekiq (>= 5.15.0)
sentry-sidekiq
shoulda-matchers
sidekiq (>= 7.2.4)
sidekiq-cron (>= 1.12.0)
sidekiq
sidekiq-cron
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
slack-ruby-client (~> 2.0.0)
spring
spring-watcher-listen
squasher
@@ -952,8 +905,8 @@ DEPENDENCIES
tzinfo-data
uglifier
valid_email2
web-console (>= 4.2.1)
web-push (>= 3.0.1)
web-console
web-push
webmock
webpacker
wisper (= 2.0.0)
@@ -963,4 +916,4 @@ RUBY VERSION
ruby 3.2.2p185
BUNDLED WITH
2.4.6
2.4.10
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2017-2024 Chatwoot Inc.
Copyright (c) 2017-2021 Chatwoot Inc.
Portions of this software are licensed as follows:
+1 -18
View File
@@ -17,9 +17,6 @@ db_migrate:
db_seed:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:seed
db_reset:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:reset
db:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:chatwoot_prepare
@@ -33,23 +30,9 @@ burn:
bundle && yarn
run:
@if [ -f ./.overmind.sock ]; then \
echo "Overmind is already running. Use 'make force_run' to start a new instance."; \
else \
overmind start -f Procfile.dev; \
fi
force_run:
rm -f ./.overmind.sock
overmind start -f Procfile.dev
debug:
overmind connect backend
debug_worker:
overmind connect worker
docker:
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
.PHONY: setup db_create db_migrate db_seed db_reset db console server burn docker run force_run debug debug_worker
.PHONY: setup db_create db_migrate db_seed db console server burn docker run
+1 -1
View File
@@ -1,4 +1,4 @@
backend: bin/rails s -p 3000
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
frontend: bin/webpack-dev-server
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
+2 -2
View File
@@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
frontend: bin/webpack-dev-server
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
+13 -11
View File
@@ -1,12 +1,10 @@
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
<p align="center">
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
___
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
</p>
# Chatwoot
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p>
<p align="center">
<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"/>
</a>
@@ -15,7 +13,9 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
</a>
</p>
<p>
___
<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>
<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>
@@ -23,13 +23,15 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
<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 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://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a>
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a>
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p>
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
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.
@@ -117,4 +119,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
*Chatwoot* &copy; 2017-2024, Chatwoot Inc - Released under the MIT License.
*Chatwoot* &copy; 2017-2023, Chatwoot Inc - Released under the MIT License.
+1 -1
View File
@@ -1 +1 @@
3.9.0
2.17.0
+1 -1
View File
@@ -1 +1 @@
2.8.0
2.3.0
+1 -9
View File
@@ -55,14 +55,11 @@
"plan": "heroku-redis:mini"
},
{
"plan": "heroku-postgresql:essential-0"
"plan": "heroku-postgresql:mini"
}
],
"stack": "heroku-20",
"buildpacks": [
{
"url": "heroku/nodejs"
},
{
"url": "heroku/ruby"
}
@@ -72,11 +69,6 @@
"scripts": {
"test": "bundle exec rake test"
}
},
"review": {
"scripts": {
"postdeploy": "bundle exec rails db:seed"
}
}
}
}
+2 -2
View File
@@ -59,7 +59,7 @@ class ContactIdentifyAction
def existing_email_contact
return if params[:email].blank?
@existing_email_contact ||= account.contacts.from_email(params[:email])
@existing_email_contact ||= account.contacts.find_by(email: params[:email])
end
def existing_phone_number_contact
@@ -104,7 +104,7 @@ class ContactIdentifyAction
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.discard_invalid_attrs if discard_invalid_attrs
@contact.save!
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? && !@contact.avatar.attached?
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end
def merge_contact(base_contact, merge_contact)
-5
View File
@@ -12,7 +12,6 @@ class ContactMergeAction
merge_conversations
merge_messages
merge_contact_inboxes
merge_contact_notes
merge_and_remove_mergee_contact
end
@base_contact
@@ -34,10 +33,6 @@ class ContactMergeAction
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
def merge_contact_notes
Note.where(contact_id: @mergee_contact.id, account_id: @mergee_contact.account_id).update(contact_id: @base_contact.id)
end
def merge_messages
Message.where(sender: @mergee_contact).update(sender: @base_contact)
end
-1
View File
@@ -1,6 +1,5 @@
//= link_tree ../images
//= link administrate/application.css
//= link administrate/application.js
//= link administrate-field-active_storage/application.css
//= link dashboardChart.js
//= link secretField.js
+1 -1
View File
@@ -11,7 +11,7 @@ function prepareData(data) {
function getChartOptions() {
var fontFamily =
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
'Inter,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
return {
responsive: true,
legend: { labels: { fontFamily } },
@@ -25,8 +25,7 @@
@import 'components/flashes';
@import 'components/form-actions';
@import 'components/main-content';
@import 'components/navigation';
@import 'components/pagination';
@import 'components/search';
@import 'components/reports';
@import 'custom_styles';
@@ -24,6 +24,14 @@ select {
font-size: $base-font-size;
}
input,
select,
textarea {
display: block;
font-family: $base-font-family;
font-size: 16px;
}
[type="color"],
[type="date"],
[type="datetime-local"],
@@ -43,7 +51,6 @@ textarea {
background-color: $white;
border: $base-border;
border-radius: $base-border-radius;
font-family: $base-font-family;
padding: 0.5em;
transition: border-color $base-duration $base-timing;
width: 100%;
@@ -1,7 +1,7 @@
html {
background-color: $color-white;
box-sizing: border-box;
font-size: 16px;
font-size: 10px;
-webkit-font-smoothing: antialiased;
}
@@ -4,8 +4,8 @@
float: left;
margin-bottom: $base-spacing;
margin-top: 0.25em;
text-align: left;
width: calc(16% - 1rem);
text-align: right;
width: calc(15% - 1rem);
}
.preserve-whitespace {
@@ -16,8 +16,8 @@
.attribute-data {
float: left;
margin-bottom: $base-spacing;
margin-left: 1.25rem;
width: calc(84% - 0.625rem);
margin-left: 2rem;
width: calc(85% - 1rem);
}
.attribute--nested {
@@ -10,7 +10,7 @@ input[type="submit"],
color: $white;
cursor: pointer;
display: inline-block;
font-size: $font-size-small;
font-size: $font-size-default;
-webkit-font-smoothing: antialiased;
font-weight: $font-weight-medium;
line-height: 1;
@@ -9,22 +9,22 @@
.field-unit__label {
float: left;
margin-left: 0.625rem;
margin-left: 1rem;
text-align: right;
width: calc(15% - 0.625rem);
width: calc(15% - 1rem);
}
.field-unit__field {
float: left;
margin-left: 1.25rem;
max-width: 31.15rem;
margin-left: 2rem;
max-width: 50rem;
width: 100%;
}
.field-unit--nested {
border: $base-border;
margin-left: 7.5%;
max-width: 37.5rem;
max-width: 60rem;
padding: $small-spacing;
width: 100%;
@@ -1,3 +1,3 @@
.form-actions {
margin-left: calc(15% + 1.25rem);
margin-left: calc(15% + 2rem);
}
@@ -1,22 +1,13 @@
.main-content {
font-size: $font-size-default;
left: 21rem;
left: 23rem;
position: absolute;
right: 0;
top: 0;
}
.main-content__body {
font-size: $font-size-small;
padding: $space-two;
table {
font-size: $font-size-small;
}
form {
margin-top: $space-two;
}
}
.main-content__header {
@@ -24,12 +15,12 @@
background-color: $color-white;
border-bottom: 1px solid $color-border;
display: flex;
min-height: 3.5rem;
min-height: 5.6rem;
padding: $space-small $space-normal;
}
.main-content__page-title {
font-size: $font-size-medium;
font-size: $font-size-large;
font-weight: $font-weight-medium;
margin-right: auto;
}
@@ -0,0 +1,78 @@
.logo-brand {
margin-bottom: $space-normal;
padding: $space-normal $space-smaller $space-small;
text-align: center;
}
.navigation {
background: $white;
border-right: 1px solid $color-border;
display: flex;
flex-direction: column;
font-size: $font-size-default;
font-weight: $font-weight-medium;
height: 100%;
justify-content: flex-start;
left: 0;
margin: 0;
overflow: auto;
padding: $space-normal;
position: fixed;
top: 0;
width: 23rem;
z-index: 1023;
li {
align-items: center;
display: flex;
a {
color: $color-gray;
text-decoration: none;
}
i {
min-width: $space-medium;
}
}
}
.navigation__link {
background-color: transparent;
color: $color-gray;
display: block;
line-height: 1;
margin-bottom: $space-smaller;
padding: $space-one;
&:hover {
color: $blue;
a {
color: $blue;
}
}
&.navigation__link--active {
background-color: $color-background;
border-radius: $base-border-radius;
color: $blue;
a {
color: $blue;
}
}
}
.logout {
bottom: $space-normal;
left: $space-normal;
position: fixed;
}
.app-version {
color: $color-gray;
font-size: $font-size-small;
padding-top: $space-smaller;
}
@@ -1,7 +1,7 @@
.search {
margin-left: auto;
margin-right: 1.25rem;
max-width: 27.5rem;
margin-right: 2rem;
max-width: 44rem;
position: relative;
width: 100%;
}
@@ -1,24 +0,0 @@
// custom styles for the dashboard
.feature-cell {
background: $color-extra-light-blue;
border-radius: 10px;
float: left;
margin-left: 8px;
margin-top: 6px;
padding: 4px 12px;
.icon-container {
margin-right: 2px;
}
.value-container {
margin-left: 6px;
}
}
.feature-container {
max-width: 100rem;
}
@@ -4,7 +4,7 @@ $base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto
sans-serif !default;
$heading-font-family: $base-font-family !default;
$base-font-size: 16px !default;
$base-font-size: 10px !default;
$base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default;
@@ -1,30 +1,30 @@
// Font sizes
$font-size-nano: 0.5rem;
$font-size-micro: 0.675rem;
$font-size-mini: 0.75rem;
$font-size-small: 0.875rem;
$font-size-default: 1rem;
$font-size-medium: 1.125rem;
$font-size-large: 1.375rem;
$font-size-big: 1.5rem;
$font-size-bigger: 1.75rem;
$font-size-mega: 2.125rem;
$font-size-giga: 2.5rem;
$font-size-nano: 0.8rem;
$font-size-micro: 1.0rem;
$font-size-mini: 1.2rem;
$font-size-small: 1.4rem;
$font-size-default: 1.6rem;
$font-size-medium: 1.8rem;
$font-size-large: 2.2rem;
$font-size-big: 2.4rem;
$font-size-bigger: 3.0rem;
$font-size-mega: 3.4rem;
$font-size-giga: 4.0rem;
// spaces
$zero: 0;
$space-micro: 0.125rem;
$space-smaller: 0.25rem;
$space-small: 0.5rem;
$space-one: 0.675rem;
$space-slab: 0.75rem;
$space-normal: 1rem;
$space-two: 1.25rem;
$space-medium: 1.5rem;
$space-large: 2rem;
$space-larger: 3rem;
$space-jumbo: 4rem;
$space-mega: 6.25rem;
$space-micro: 0.2rem;
$space-smaller: 0.4rem;
$space-small: 0.8rem;
$space-one: 1rem;
$space-slab: 1.2rem;
$space-normal: 1.6rem;
$space-two: 2.0rem;
$space-medium: 2.4rem;
$space-large: 3.2rem;
$space-larger: 4.8rem;
$space-jumbo: 6.4rem;
$space-mega: 10.0rem;
// font-weight
$font-weight-feather: 100;
@@ -78,6 +78,11 @@ $conv-header-height: 4rem;
$inbox-thumb-size: 4.8rem;
// Spinner
$spinkit-spinner-color: $color-white !default;
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
$spinkit-size: 1.6rem !default;
// Snackbar default
$woot-snackbar-bg: #323232;
$woot-snackbar-button: #ffeb3b;
+4 -14
View File
@@ -2,7 +2,7 @@
class AccountBuilder
include CustomExceptions::Account
pattr_initialize [:account_name, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
def perform
if @user.nil?
@@ -15,22 +15,12 @@ class AccountBuilder
end
[@user, @account]
rescue StandardError => e
Rails.logger.debug e.inspect
puts e.inspect
raise e
end
private
def user_full_name
# the empty string ensures that not-null constraint is not violated
@user_full_name || ''
end
def account_name
# the empty string ensures that not-null constraint is not violated
@account_name || ''
end
def validate_email
address = ValidEmail2::Address.new(@email)
if address.valid? # && !address.disposable?
@@ -49,7 +39,7 @@ class AccountBuilder
end
def create_account
@account = Account.create!(name: account_name, locale: I18n.locale)
@account = Account.create!(name: @account_name, locale: I18n.locale)
Current.account = @account
end
@@ -74,7 +64,7 @@ class AccountBuilder
@user = User.new(email: @email,
password: user_password,
password_confirmation: user_password,
name: user_full_name)
name: @user_full_name)
@user.type = 'SuperAdmin' if @super_admin
@user.confirm if @confirmed
@user.save!
-54
View File
@@ -1,54 +0,0 @@
# The AgentBuilder class is responsible for creating a new agent.
# It initializes with necessary attributes and provides a perform method
# to create a user and account user in a transaction.
class AgentBuilder
# Initializes an AgentBuilder with necessary attributes.
# @param email [String] the email of the user.
# @param name [String] the name of the user.
# @param role [String] the role of the user, defaults to 'agent' if not provided.
# @param inviter [User] the user who is inviting the agent (Current.user in most cases).
# @param availability [String] the availability status of the user, defaults to 'offline' if not provided.
# @param auto_offline [Boolean] the auto offline status of the user.
pattr_initialize [:email, { name: '' }, :inviter, :account, { role: :agent }, { availability: :offline }, { auto_offline: false }]
# Creates a user and account user in a transaction.
# @return [User] the created user.
def perform
ActiveRecord::Base.transaction do
@user = find_or_create_user
create_account_user
end
@user
end
private
# Finds a user by email or creates a new one with a temporary password.
# @return [User] the found or created user.
def find_or_create_user
user = User.from_email(email)
return user if user
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password)
end
# Checks if the user needs confirmation.
# @return [Boolean] true if the user is persisted and not confirmed, false otherwise.
def user_needs_confirmation?
@user.persisted? && !@user.confirmed?
end
# Creates an account user linking the user to the current account.
def create_account_user
AccountUser.create!({
account_id: account.id,
user_id: @user.id,
inviter_id: inviter.id
}.merge({
role: role,
availability: availability,
auto_offline: auto_offline
}.compact))
end
end
@@ -19,9 +19,9 @@ class ContactInboxWithContactBuilder
ActiveRecord::Base.transaction(requires_new: true) do
build_contact_with_contact_inbox
update_contact_avatar(@contact) unless @contact.avatar.attached?
@contact_inbox
end
update_contact_avatar(@contact) unless @contact.avatar.attached?
@contact_inbox
end
private
@@ -75,7 +75,7 @@ class ContactInboxWithContactBuilder
def find_contact_by_email(email)
return if email.blank?
account.contacts.from_email(email)
account.contacts.find_by(email: email.downcase)
end
def find_contact_by_phone_number(phone_number)
@@ -25,9 +25,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
build_contact_inbox
build_message
end
rescue Koala::Facebook::AuthenticationError => e
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
@@ -53,23 +51,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
def conversation
@conversation ||= set_conversation_based_on_inbox_config
end
def set_conversation_based_on_inbox_config
if @inbox.lock_to_single_conversation
Conversation.where(conversation_params).order(created_at: :desc).first || build_conversation
else
find_or_build_for_multiple_conversations
end
end
def find_or_build_for_multiple_conversations
# If lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
last_conversation = Conversation.where(conversation_params).where.not(status: :resolved).order(created_at: :desc).first
return build_conversation if last_conversation.nil?
last_conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
end
def build_conversation
@@ -111,9 +93,6 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: @message_type,
content: response.content,
source_id: response.identifier,
content_attributes: {
in_reply_to_external_id: response.in_reply_to_external_id
},
sender: @outgoing_echo ? nil : @contact_inbox.contact
}
end
@@ -126,15 +105,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
}
end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def contact_params
begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(@sender_id) || {}
rescue Koala::Facebook::AuthenticationError => e
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue Koala::Facebook::ClientError => e
@@ -152,6 +127,4 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
process_contact_params_result(result)
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end
@@ -20,9 +20,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
ActiveRecord::Base.transaction do
build_message
end
rescue Koala::Facebook::AuthenticationError => e
Rails.logger.warn("Instagram authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue StandardError => e
@@ -48,10 +46,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
@outgoing_echo ? recipient_id : sender_id
end
def message_is_unsupported?
message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true
end
def sender_id
@messaging[:sender][:id]
end
@@ -69,28 +63,9 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
end
def conversation
@conversation ||= set_conversation_based_on_inbox_config
end
def instagram_direct_message_conversation
Conversation.where(conversation_params)
.where("additional_attributes ->> 'type' = 'instagram_direct_message'")
end
def set_conversation_based_on_inbox_config
if @inbox.lock_to_single_conversation
instagram_direct_message_conversation.order(created_at: :desc).first || build_conversation
else
find_or_build_for_multiple_conversations
end
end
def find_or_build_for_multiple_conversations
last_conversation = instagram_direct_message_conversation.where.not(status: :resolved).order(created_at: :desc).first
return build_conversation if last_conversation.nil?
last_conversation
@conversation ||= Conversation.where(
"additional_attributes ->> 'type' = 'instagram_direct_message'"
).find_by(conversation_params) || build_conversation
end
def message_content
@@ -101,10 +76,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
end
def message_reply_attributes
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
end
def build_message
return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files?
@@ -125,7 +96,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
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,
additional_attributes: { type: 'instagram_direct_message' }
@@ -141,20 +111,14 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
end
def message_params
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,
content_attributes: {
in_reply_to_external_id: message_reply_attributes
}
sender: @outgoing_echo ? nil : contact
}
params[:content_attributes][:is_unsupported] = true if message_is_unsupported?
params
end
def already_sent_from_chatwoot?
+10 -48
View File
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
@user = user
@message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments]
@automation_rule = content_attributes&.dig(:automation_rule_id)
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = content_attributes&.dig(:in_reply_to)
@items = content_attributes&.dig(:items)
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
end
def perform
@@ -26,38 +26,6 @@ class Messages::MessageBuilder
private
# Extracts content attributes from the given params.
# - Converts ActionController::Parameters to a regular hash if needed.
# - Attempts to parse a JSON string if content is a string.
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
def content_attributes
params = convert_to_hash(@params)
content_attributes = params.fetch(:content_attributes, {})
return parse_json(content_attributes) if content_attributes.is_a?(String)
return content_attributes if content_attributes.is_a?(Hash)
{}
end
# Converts the given object to a hash.
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
# Otherwise, returns the object as-is.
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
# Attempts to parse a string as JSON.
# If successful, returns the parsed hash with symbolized names.
# If unsuccessful, returns nil.
def parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
def process_attachments
return if @attachments.blank?
@@ -80,22 +48,17 @@ class Messages::MessageBuilder
def process_emails
return unless @conversation.inbox&.inbox_type == 'Email'
cc_emails = process_email_string(@params[:cc_emails])
bcc_emails = process_email_string(@params[:bcc_emails])
to_emails = process_email_string(@params[:to_emails])
cc_emails = []
cc_emails = @params[:cc_emails].gsub(/\s+/, '').split(',') if @params[:cc_emails].present?
all_email_addresses = cc_emails + bcc_emails + to_emails
bcc_emails = []
bcc_emails = @params[:bcc_emails].gsub(/\s+/, '').split(',') if @params[:bcc_emails].present?
all_email_addresses = cc_emails + bcc_emails
validate_email_addresses(all_email_addresses)
@message.content_attributes[:cc_emails] = cc_emails
@message.content_attributes[:bcc_emails] = bcc_emails
@message.content_attributes[:to_emails] = to_emails
end
def process_email_string(email_string)
return [] if email_string.blank?
email_string.gsub(/\s+/, '').split(',')
end
def validate_email_addresses(all_emails)
@@ -149,8 +112,7 @@ class Messages::MessageBuilder
content_type: @params[:content_type],
items: @items,
in_reply_to: @in_reply_to,
echo_id: @params[:echo_id],
source_id: @params[:source_id]
echo_id: @params[:echo_id]
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
end
end
+6 -8
View File
@@ -1,20 +1,22 @@
class NotificationBuilder
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!, :secondary_actor]
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
def perform
return unless user_subscribed_to_notification?
build_notification
end
private
def current_user
def secondary_actor
Current.user
end
def user_subscribed_to_notification?
notification_setting = user.notification_settings.find_by(account_id: account.id)
# added for the case where an assignee might be removed from the account but remains in conversation
return false if notification_setting.blank?
return if notification_setting.blank?
return true if notification_setting.public_send("email_#{notification_type}?")
return true if notification_setting.public_send("push_#{notification_type}?")
@@ -23,15 +25,11 @@ class NotificationBuilder
end
def build_notification
# Create conversation_creation notification only if user is subscribed to it
return if notification_type == 'conversation_creation' && !user_subscribed_to_notification?
user.notifications.create!(
notification_type: notification_type,
account: account,
primary_actor: primary_actor,
# secondary_actor is secondary_actor if present, else current_user
secondary_actor: secondary_actor || current_user
secondary_actor: secondary_actor
)
end
end
+11 -42
View File
@@ -15,15 +15,12 @@ class V2::ReportBuilder
end
def timeseries
return send(params[:metric]) if metric_valid?
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
{}
send(params[:metric])
end
# For backward compatible with old report
def build
if %w[avg_first_response_time avg_resolution_time reply_time].include?(params[:metric])
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
timeseries.each_with_object([]) do |p, arr|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
end
@@ -36,34 +33,18 @@ class V2::ReportBuilder
def summary
{
conversations_count: conversations.count,
incoming_messages_count: incoming_messages.count,
outgoing_messages_count: outgoing_messages.count,
conversations_count: conversations_count.values.sum,
incoming_messages_count: incoming_messages_count.values.sum,
outgoing_messages_count: outgoing_messages_count.values.sum,
avg_first_response_time: avg_first_response_time_summary,
avg_resolution_time: avg_resolution_time_summary,
resolutions_count: resolutions.count,
reply_time: reply_time_summary
}
end
def short_summary
{
conversations_count: conversations.count,
avg_first_response_time: avg_first_response_time_summary,
avg_resolution_time: avg_resolution_time_summary
}
end
def bot_summary
{
bot_resolutions_count: bot_resolutions.count,
bot_handoffs_count: bot_handoffs.count
resolutions_count: resolutions_count.values.sum
}
end
def conversation_metrics
if params[:type].equal?(:account)
live_conversations
conversations
else
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
end
@@ -71,18 +52,6 @@ class V2::ReportBuilder
private
def metric_valid?
%w[conversations_count
incoming_messages_count
outgoing_messages_count
avg_first_response_time
avg_resolution_time reply_time
resolutions_count
bot_resolutions_count
bot_handoffs_count
reply_time].include?(params[:metric])
end
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
@@ -120,19 +89,19 @@ class V2::ReportBuilder
email: @user.email,
thumbnail: @user.avatar_url,
availability: account_user.availability_status,
metric: live_conversations
metric: conversations
}
end
end
def live_conversations
def conversations
@open_conversations = scope.conversations.where(account_id: @account.id).open
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
metric = {
open: @open_conversations.count,
unattended: @open_conversations.unattended.count
unattended: @open_conversations.count - first_response_count
}
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
metric[:pending] = @open_conversations.pending.count if params[:type].equal?(:account)
metric
end
end
@@ -1,53 +0,0 @@
class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
pattr_initialize [:account!, :params!]
def build
set_grouped_conversations_count
set_grouped_avg_reply_time
set_grouped_avg_first_response_time
set_grouped_avg_resolution_time
prepare_report
end
private
def set_grouped_conversations_count
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count
end
def set_grouped_avg_resolution_time
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
end
def set_grouped_avg_first_response_time
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
end
def set_grouped_avg_reply_time
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
end
def group_by_key
:user_id
end
def reporting_events
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
end
def prepare_report
account.account_users.each_with_object([]) do |account_user, arr|
arr << {
id: account_user.user_id,
conversations_count: @grouped_conversations_count[account_user.user_id],
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
}
end
end
def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
end
end
@@ -1,17 +0,0 @@
class V2::Reports::BaseSummaryBuilder
include DateRangeHelper
private
def group_by_key
# Override this method
end
def get_grouped_average(events)
events.group(group_by_key).average(average_value_key)
end
def average_value_key
params[:business_hours].present? ? :value_in_business_hours : :value
end
end
@@ -1,54 +0,0 @@
class V2::Reports::BotMetricsBuilder
include DateRangeHelper
attr_reader :account, :params
def initialize(account, params)
@account = account
@params = params
end
def metrics
{
conversation_count: bot_conversations.count,
message_count: bot_messages.count,
resolution_rate: bot_resolution_rate.to_i,
handoff_rate: bot_handoff_rate.to_i
}
end
private
def bot_activated_inbox_ids
@bot_activated_inbox_ids ||= account.inboxes.filter(&:active_bot?).map(&:id)
end
def bot_conversations
@bot_conversations ||= account.conversations.where(inbox_id: bot_activated_inbox_ids).where(created_at: range)
end
def bot_messages
@bot_messages ||= account.messages.outgoing.where(conversation_id: bot_conversations.ids).where(created_at: range)
end
def bot_resolutions_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
created_at: range).distinct.count
end
def bot_handoffs_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff,
created_at: range).distinct.count
end
def bot_resolution_rate
return 0 if bot_conversations.count.zero?
bot_resolutions_count.to_f / bot_conversations.count * 100
end
def bot_handoff_rate
return 0 if bot_conversations.count.zero?
bot_handoffs_count.to_f / bot_conversations.count * 100
end
end
@@ -1,30 +0,0 @@
class V2::Reports::Conversations::BaseReportBuilder
pattr_initialize :account, :params
private
AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze
COUNT_METRICS = %w[
conversations_count
incoming_messages_count
outgoing_messages_count
resolutions_count
bot_resolutions_count
bot_handoffs_count
].freeze
def builder_class(metric)
case metric
when *AVG_METRICS
V2::Reports::Timeseries::AverageReportBuilder
when *COUNT_METRICS
V2::Reports::Timeseries::CountReportBuilder
end
end
def log_invalid_metric
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
{}
end
end
@@ -1,30 +0,0 @@
class V2::Reports::Conversations::MetricBuilder < V2::Reports::Conversations::BaseReportBuilder
def summary
{
conversations_count: count('conversations_count'),
incoming_messages_count: count('incoming_messages_count'),
outgoing_messages_count: count('outgoing_messages_count'),
avg_first_response_time: count('avg_first_response_time'),
avg_resolution_time: count('avg_resolution_time'),
resolutions_count: count('resolutions_count'),
reply_time: count('reply_time')
}
end
def bot_summary
{
bot_resolutions_count: count('bot_resolutions_count'),
bot_handoffs_count: count('bot_handoffs_count')
}
end
private
def count(metric)
builder_class(metric).new(account, builder_params(metric)).aggregate_value
end
def builder_params(metric)
params.merge({ metric: metric })
end
end
@@ -1,21 +0,0 @@
class V2::Reports::Conversations::ReportBuilder < V2::Reports::Conversations::BaseReportBuilder
def timeseries
perform_action(:timeseries)
end
def aggregate_value
perform_action(:aggregate_value)
end
private
def perform_action(method_name)
return builder.new(account, params).public_send(method_name) if builder.present?
log_invalid_metric
end
def builder
builder_class(params[:metric])
end
end
@@ -1,49 +0,0 @@
class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
pattr_initialize [:account!, :params!]
def build
set_grouped_conversations_count
set_grouped_avg_reply_time
set_grouped_avg_first_response_time
set_grouped_avg_resolution_time
prepare_report
end
private
def set_grouped_conversations_count
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count
end
def set_grouped_avg_resolution_time
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
end
def set_grouped_avg_first_response_time
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
end
def set_grouped_avg_reply_time
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
end
def reporting_events
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
end
def group_by_key
'conversations.team_id'
end
def prepare_report
account.teams.each_with_object([]) do |team, arr|
arr << {
id: team.id,
conversations_count: @grouped_conversations_count[team.id],
avg_resolution_time: @grouped_avg_resolution_time[team.id],
avg_first_response_time: @grouped_avg_first_response_time[team.id],
avg_reply_time: @grouped_avg_reply_time[team.id]
}
end
end
end
@@ -1,48 +0,0 @@
class V2::Reports::Timeseries::AverageReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
grouped_average_time = reporting_events.average(average_value_key)
grouped_event_count = reporting_events.count
grouped_average_time.each_with_object([]) do |element, arr|
event_date, average_time = element
arr << {
value: average_time,
timestamp: event_date.in_time_zone(timezone).to_i,
count: grouped_event_count[event_date]
}
end
end
def aggregate_value
object_scope.average(average_value_key)
end
private
def event_name
metric_to_event_name = {
avg_first_response_time: :first_response,
avg_resolution_time: :conversation_resolved,
reply_time: :reply_time
}
metric_to_event_name[params[:metric].to_sym]
end
def object_scope
scope.reporting_events.where(name: event_name, created_at: range)
end
def reporting_events
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year hour],
time_zone: timezone
)
end
def average_value_key
@average_value_key ||= params[:business_hours].present? ? :value_in_business_hours : :value
end
end
@@ -1,46 +0,0 @@
class V2::Reports::Timeseries::BaseTimeseriesBuilder
include TimezoneHelper
include DateRangeHelper
DEFAULT_GROUP_BY = 'day'.freeze
pattr_initialize :account, :params
def scope
case params[:type].to_sym
when :account
account
when :inbox
inbox
when :agent
user
when :label
label
when :team
team
end
end
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
def user
@user ||= account.users.find(params[:id])
end
def label
@label ||= account.labels.find(params[:id])
end
def team
@team ||= account.teams.find(params[:id])
end
def group_by
@group_by ||= %w[day week month year hour].include?(params[:group_by]) ? params[:group_by] : DEFAULT_GROUP_BY
end
def timezone
@timezone ||= timezone_name_from_offset(params[:timezone_offset])
end
end
@@ -1,71 +0,0 @@
class V2::Reports::Timeseries::CountReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
grouped_count.each_with_object([]) do |element, arr|
event_date, event_count = element
# The `event_date` is in Date format (without time), such as "Wed, 15 May 2024".
# We need a timestamp for the start of the day. However, we can't use `event_date.to_time.to_i`
# because it converts the date to 12:00 AM server timezone.
# The desired output should be 12:00 AM in the specified timezone.
arr << { value: event_count, timestamp: event_date.in_time_zone(timezone).to_i }
end
end
def aggregate_value
object_scope.count
end
private
def metric
@metric ||= params[:metric]
end
def object_scope
send("scope_for_#{metric}")
end
def scope_for_conversations_count
scope.conversations.where(account_id: account.id, created_at: range)
end
def scope_for_incoming_messages_count
scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order)
end
def scope_for_outgoing_messages_count
scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order)
end
def scope_for_resolutions_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_resolved,
conversations: { status: :resolved }, created_at: range
).distinct
end
def scope_for_bot_resolutions_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_resolved,
conversations: { status: :resolved }, created_at: range
).distinct
end
def scope_for_bot_handoffs_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_handoff,
created_at: range
).distinct
end
def grouped_count
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year hour],
time_zone: timezone
).count
end
end
@@ -9,6 +9,7 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B
mergee_contact: @mergee_contact
)
contact_merge_action.perform
render json: @base_contact
end
private
@@ -10,18 +10,11 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
def show; end
def create
@agent_bot = Current.account.agent_bots.create!(permitted_params.except(:avatar_url))
process_avatar_from_url
@agent_bot = Current.account.agent_bots.create!(permitted_params)
end
def update
@agent_bot.update!(permitted_params.except(:avatar_url))
process_avatar_from_url
end
def avatar
@agent_bot.avatar.purge if @agent_bot.avatar.attached?
@agent_bot
@agent_bot.update!(permitted_params)
end
def destroy
@@ -37,10 +30,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
end
def permitted_params
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content])
end
def process_avatar_from_url
::Avatar::AvatarFromUrlJob.perform_later(@agent_bot, params[:avatar_url]) if params[:avatar_url].present?
params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
end
end
@@ -1,26 +1,16 @@
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
before_action :fetch_agent, except: [:create, :index, :bulk_create]
before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization
before_action :find_user, only: [:create]
before_action :validate_limit, only: [:create]
before_action :validate_limit_for_bulk_create, only: [:bulk_create]
before_action :create_user, only: [:create]
before_action :save_account_user, only: [:create]
def index
@agents = agents
end
def create
builder = AgentBuilder.new(
email: new_agent_params['email'],
name: new_agent_params['name'],
role: new_agent_params['role'],
availability: new_agent_params['availability'],
auto_offline: new_agent_params['auto_offline'],
inviter: current_user,
account: Current.account
)
@agent = builder.perform
end
def create; end
def update
@agent.update!(agent_params.slice(:name).compact)
@@ -33,30 +23,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
head :ok
end
def bulk_create
emails = params[:emails]
emails.each do |email|
builder = AgentBuilder.new(
email: email,
name: email.split('@').first,
inviter: current_user,
account: Current.account
)
begin
builder.perform
rescue ActiveRecord::RecordInvalid => e
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
end
end
# This endpoint is used to bulk create agents during onboarding
# onboarding_step key in present in Current account custom attributes, since this is a one time operation
Current.account.custom_attributes.delete('onboarding_step')
Current.account.save!
head :ok
end
private
def check_authorization
@@ -67,34 +33,47 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
@agent = agents.find(params[:id])
end
def find_user
@user = User.find_by(email: new_agent_params[:email])
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
return @user.send_confirmation_instructions if @user
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
end
def save_account_user
AccountUser.create!({
account_id: Current.account.id,
user_id: @user.id,
inviter_id: current_user.id
}.merge({
role: new_agent_params[:role],
availability: new_agent_params[:availability],
auto_offline: new_agent_params[:auto_offline]
}.compact))
end
def agent_params
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
end
def new_agent_params
# intial string ensures the password requirements are met
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
end
def agents
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
end
def validate_limit_for_bulk_create
limit_available = params[:emails].count <= available_agent_count
render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available
end
def validate_limit
render_payment_required('Account limit exceeded. Please purchase more licenses') unless can_add_agent?
end
def available_agent_count
Current.account.usage_limits[:agents] - agents.count
end
def can_add_agent?
available_agent_count.positive?
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
end
def delete_user_record(agent)
@@ -1,7 +1,7 @@
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :portal
before_action :check_authorization
before_action :fetch_article, except: [:index, :create, :reorder]
before_action :fetch_article, except: [:index, :create, :attach_file, :reorder]
before_action :set_current_page, only: [:index]
def index
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
@articles_count = @all_articles.count
@articles = if list_params[:category_slug].present?
@all_articles.order_by_position.page(@current_page)
@all_articles.order_by_position.page(@current_page).per(50)
else
@all_articles.order_by_updated_at.page(@current_page)
end
@@ -36,6 +36,17 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
head :ok
end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:background_image].tempfile,
filename: params[:background_image].original_filename,
content_type: params[:background_image].content_type
)
file_blob.save!
render json: { file_url: url_for(file_blob) }
end
def reorder
Article.update_positions(params[:positions_hash])
head :ok
@@ -20,6 +20,16 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
@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 update
ActiveRecord::Base.transaction do
automation_rule_update
@@ -21,6 +21,6 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseControll
end
def permitted_params
params.permit(:type, :snoozed_until, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []])
params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []])
end
end
@@ -14,18 +14,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
@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)
end
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
Rails.logger.error "Error in register_facebook_page: #{e.message}"
# Additional log statements
log_additional_info
end
def log_additional_info
Rails.logger.debug do
"user_access_token: #{params[:user_access_token]} , page_access_token: #{params[:page_access_token]} ,
page_id: #{params[:page_id]}, inbox_name: #{params[:inbox_name]}"
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
end
@@ -40,8 +30,6 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
instagram_id = response['instagram_business_account']['id']
facebook_channel.update(instagram_id: instagram_id)
rescue StandardError => e
Rails.logger.error "Error in set_instagram_id: #{e.message}"
end
# get params[:inbox_id], current_account. params[:omniauth_token]
@@ -73,7 +61,6 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
fb_page&.reauthorized!
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
Rails.logger.error "Error in update_fb_page: #{e.message}"
end
end
@@ -90,7 +77,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
koala.exchange_access_token_info(omniauth_token)['access_token']
rescue StandardError => e
Rails.logger.error "Error in long_lived_token: #{e.message}"
Rails.logger.error e
end
def mark_already_existing_facebook_pages(data)
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def category_params
params.require(:category).permit(
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
)
end
@@ -2,9 +2,13 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
before_action :authorize_request
def create
process_create
rescue StandardError => e
render_could_not_create_error(e.message)
ActiveRecord::Base.transaction do
authenticate_twilio
build_inbox
setup_webhooks if @twilio_channel.sms?
rescue StandardError => e
render_could_not_create_error(e.message)
end
end
private
@@ -13,20 +17,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
authorize ::Inbox
end
def process_create
ActiveRecord::Base.transaction do
authenticate_twilio
build_inbox
setup_webhooks if @twilio_channel.sms?
end
end
def authenticate_twilio
client = if permitted_params[:api_key_sid].present?
Twilio::REST::Client.new(permitted_params[:api_key_sid], permitted_params[:auth_token], permitted_params[:account_sid])
else
Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
end
client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
client.messages.list(limit: 1)
end
@@ -48,7 +40,6 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
@twilio_channel = Current.account.twilio_sms.create!(
account_sid: permitted_params[:account_sid],
auth_token: permitted_params[:auth_token],
api_key_sid: permitted_params[:api_key_sid],
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
phone_number: phone_number,
medium: medium
@@ -61,7 +52,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
def permitted_params
params.require(:twilio_channel).permit(
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium, :api_key_sid
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium
)
end
end
@@ -1,21 +0,0 @@
class Api::V1::Accounts::ContactInboxesController < Api::V1::Accounts::BaseController
before_action :ensure_inbox
def filter
contact_inbox = @inbox.contact_inboxes.where(inbox_id: permitted_params[:inbox_id], source_id: permitted_params[:source_id])
return head :not_found if contact_inbox.empty?
@contact = contact_inbox.first.contact
end
private
def ensure_inbox
@inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
authorize @inbox, :show?
end
def permitted_params
params.permit(:inbox_id, :source_id)
end
end
@@ -1,13 +1,11 @@
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
include HmacConcern
before_action :ensure_inbox, only: [:create]
def create
@contact_inbox = ContactInboxBuilder.new(
contact: @contact,
inbox: @inbox,
source_id: params[:source_id],
hmac_verified: hmac_verified?
source_id: params[:source_id]
).perform
end
@@ -4,7 +4,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
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 :created_at, internal_name: :order_on_created_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]
@@ -18,19 +17,18 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def index
@contacts_count = resolved_contacts.count
@contacts = fetch_contacts(resolved_contacts)
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
end
def search
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
contacts = resolved_contacts.where(
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
search: "%#{params[:q].strip}%"
)
@contacts_count = contacts.count
@contacts = fetch_contacts(contacts)
@contacts = fetch_contacts_with_conversation_count(contacts)
end
def import
@@ -44,13 +42,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
head :ok
end
def export
column_names = params['column_names']
filter_params = { :payload => params.permit!['payload'], :label => params.permit!['label'] }
Account::ContactsExportJob.perform_later(Current.account.id, Current.user.id, column_names, filter_params)
head :ok, message: I18n.t('errors.contacts.export.success')
end
# returns online contacts
def active
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
@@ -62,14 +53,10 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def show; end
def filter
result = ::Contacts::FilterService.new(Current.account, Current.user, params.permit!).perform
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
contacts = result[:contacts]
@contacts_count = result[:count]
@contacts = fetch_contacts(contacts)
rescue CustomExceptions::CustomFilter::InvalidAttribute,
CustomExceptions::CustomFilter::InvalidOperator,
CustomExceptions::CustomFilter::InvalidValue => e
render_could_not_create_error(e.message)
@contacts = fetch_contacts_with_conversation_count(contacts)
end
def contactable_inboxes
@@ -88,14 +75,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
@contact.save!
@contact_inbox = build_contact_inbox
process_avatar_from_url
process_avatar
end
end
def update
@contact.assign_attributes(contact_update_params)
@contact.save!
process_avatar_from_url
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
end
def destroy
@@ -131,14 +118,17 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@current_page = params[:page] || 1
end
def fetch_contacts(contacts)
contacts_with_avatar = filtrate(contacts)
.includes([{ avatar_attachment: [:blob] }])
.page(@current_page).per(RESULTS_PER_PAGE)
def fetch_contacts_with_conversation_count(contacts)
conversation_count_sub_query = 'SELECT COUNT(*) FROM "conversations" WHERE "conversations"."contact_id" = "contacts"."id"'
contacts_with_conversation_count = filtrate(contacts)
.select("contacts.*, (#{conversation_count_sub_query}) as conversations_count")
.group('contacts.id')
.includes([{ avatar_attachment: [:blob] }])
.page(@current_page).per(RESULTS_PER_PAGE)
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
contacts_with_avatar
contacts_with_conversation_count
end
def build_contact_inbox
@@ -153,7 +143,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def permitted_params
params.permit(:name, :identifier, :email, :phone_number, :avatar, :blocked, :avatar_url, additional_attributes: {}, custom_attributes: {})
params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
end
def contact_custom_attributes
@@ -179,7 +169,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
end
def process_avatar_from_url
def process_avatar
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end
@@ -14,8 +14,7 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
def set_agent
@agent = Current.account.users.find_by(id: params[:assignee_id])
@conversation.assignee = @agent
@conversation.save!
@conversation.update_assignee(@agent)
render_agent
end
@@ -1,4 +1,5 @@
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
include EnsureCurrentAccountHelper
before_action :conversation
private
@@ -1,28 +0,0 @@
class Api::V1::Accounts::Conversations::DraftMessagesController < Api::V1::Accounts::Conversations::BaseController
def show
render json: { has_draft: false } and return unless Redis::Alfred.exists?(draft_redis_key)
draft_message = Redis::Alfred.get(draft_redis_key)
render json: { has_draft: true, message: draft_message }
end
def update
Redis::Alfred.set(draft_redis_key, draft_message_params)
head :ok
end
def destroy
Redis::Alfred.delete(draft_redis_key)
head :ok
end
private
def draft_redis_key
format(Redis::Alfred::CONVERSATION_DRAFT_MESSAGE, id: @conversation.id)
end
def draft_message_params
params.dig(:draft_message, :message) || ''
end
end
@@ -18,15 +18,6 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
end
end
def retry
return if message.blank?
message.update!(status: :sent, content_attributes: {})
::SendReplyJob.perform_later(message.id)
rescue StandardError => e
render_could_not_create_error(e.message)
end
def translate
return head :ok if already_translated_content_available?
@@ -1,7 +1,6 @@
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
include Events::Types
include DateRangeHelper
include HmacConcern
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
before_action :inbox, :contact, :contact_inbox, only: [:create]
@@ -36,18 +35,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
end
def update
@conversation.update!(permitted_update_params)
end
def filter
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
@conversations = result[:conversations]
@conversations_count = result[:count]
rescue CustomExceptions::CustomFilter::InvalidAttribute,
CustomExceptions::CustomFilter::InvalidOperator,
CustomExceptions::CustomFilter::InvalidValue => e
render_could_not_create_error(e.message)
end
def mute
@@ -68,26 +59,13 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def toggle_status
# FIXME: move this logic into a service object
if pending_to_open_by_bot?
@conversation.bot_handoff!
elsif params[:status].present?
if params[:status]
set_conversation_status
@status = @conversation.save!
else
@status = @conversation.toggle_status
end
assign_conversation if should_assign_conversation?
end
def pending_to_open_by_bot?
return false unless Current.user.is_a?(AgentBot)
@conversation.status == 'pending' && params[:status] == 'open'
end
def should_assign_conversation?
@conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end
def toggle_priority
@@ -118,11 +96,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
private
def permitted_update_params
# TODO: Move the other conversation attributes to this method and remove specific endpoints for each attribute
params.permit(:priority)
end
def update_last_seen_on_conversation(last_seen_at, update_assignee)
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, last_seen_at)
@@ -131,13 +104,16 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
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
@conversation.assignee = current_user
@conversation.save!
@agent = Current.account.users.find(current_user.id)
@conversation.update_assignee(@agent)
end
def conversation
@@ -176,8 +152,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
ContactInboxBuilder.new(
contact: @contact,
inbox: @inbox,
source_id: params[:source_id],
hmac_verified: hmac_verified?
source_id: params[:source_id]
).perform
end
@@ -189,5 +164,3 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.assignee_id? && Current.user == @conversation.assignee
end
end
Api::V1::Accounts::ConversationsController.prepend_mod_with('Api::V1::Accounts::ConversationsController')
@@ -39,8 +39,6 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
:attribute_display_type,
:attribute_key,
:attribute_model,
:regex_pattern,
:regex_cue,
attribute_values: []
)
end
@@ -1,5 +1,4 @@
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_custom_filters, except: [:create]
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
DEFAULT_FILTER_TYPE = 'conversation'.freeze
@@ -12,7 +11,6 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
@custom_filter = current_user.custom_filters.create!(
permitted_payload.merge(account_id: Current.account.id)
)
render json: { error: @custom_filter.errors.messages }, status: :unprocessable_entity and return unless @custom_filter.valid?
end
def update
@@ -1,32 +0,0 @@
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController
include GoogleConcern
before_action :check_authorization
def create
email = params[:authorization][:email]
redirect_url = google_client.auth_code.authorize_url(
{
redirect_uri: "#{base_url}/google/callback",
scope: 'email profile https://mail.google.com/',
response_type: 'code',
prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it
access_type: 'offline', # the default is 'online'
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
}
)
if redirect_url
cache_key = "google::#{email.downcase}"
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
end
end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end
@@ -44,7 +44,26 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def update
@inbox.update!(permitted_params.except(:channel))
update_inbox_working_hours
update_channel if channel_update_required?
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]
end
def agent_bot
@@ -63,7 +82,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def destroy
::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
::DeleteObjectJob.perform_later(@inbox) if @inbox.present?
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
end
@@ -84,35 +103,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
end
def update_inbox_working_hours
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
end
def update_channel
channel_attributes = get_channel_attributes(@inbox.channel_type)
return if permitted_params(channel_attributes)[:channel].blank?
validate_and_update_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
reauthorize_and_update_channel(channel_attributes)
update_channel_feature_flags
end
def channel_update_required?
permitted_params(get_channel_attributes(@inbox.channel_type))[:channel].present?
end
def validate_and_update_email_channel(channel_attributes)
validate_email_channel(channel_attributes)
rescue StandardError => e
render json: { message: e }, status: :unprocessable_entity and return
end
def reauthorize_and_update_channel(channel_attributes)
@inbox.channel.reauthorized! if @inbox.channel.respond_to?(:reauthorized!)
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
end
def update_channel_feature_flags
return unless @inbox.web_widget?
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
@@ -124,7 +114,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name]
:lock_to_single_conversation, :portal_id]
end
def permitted_params(channel_attributes = [])
@@ -1,93 +0,0 @@
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
before_action :fetch_conversation, only: [:link_issue, :linked_issues]
def teams
teams = linear_processor_service.teams
if teams[:error]
render json: { error: teams[:error] }, status: :unprocessable_entity
else
render json: teams[:data], status: :ok
end
end
def team_entities
team_id = permitted_params[:team_id]
team_entities = linear_processor_service.team_entities(team_id)
if team_entities[:error]
render json: { error: team_entities[:error] }, status: :unprocessable_entity
else
render json: team_entities[:data], status: :ok
end
end
def create_issue
issue = linear_processor_service.create_issue(permitted_params)
if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity
else
render json: issue[:data], status: :ok
end
end
def link_issue
issue_id = permitted_params[:issue_id]
title = permitted_params[:title]
issue = linear_processor_service.link_issue(conversation_link, issue_id, title)
if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity
else
render json: issue[:data], status: :ok
end
end
def unlink_issue
link_id = permitted_params[:link_id]
issue = linear_processor_service.unlink_issue(link_id)
if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity
else
render json: issue[:data], status: :ok
end
end
def linked_issues
issues = linear_processor_service.linked_issues(conversation_link)
if issues[:error]
render json: { error: issues[:error] }, status: :unprocessable_entity
else
render json: issues[:data], status: :ok
end
end
def search_issue
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
term = params[:q]
issues = linear_processor_service.search_issue(term)
if issues[:error]
render json: { error: issues[:error] }, status: :unprocessable_entity
else
render json: issues[:data], status: :ok
end
end
private
def conversation_link
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{Current.account.id}/conversations/#{@conversation.display_id}"
end
def fetch_conversation
@conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id])
end
def linear_processor_service
Integrations::Linear::ProcessorService.new(account: Current.account)
end
def permitted_params
params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
end
end
@@ -1,27 +1,27 @@
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
before_action :check_admin_authorization?
before_action :fetch_hook, only: [:update, :destroy, :list_all_channels]
def list_all_channels
@channels = channel_builder.fetch_channels
end
before_action :fetch_hook, only: [:update, :destroy]
def create
hook_builder = Integrations::Slack::HookBuilder.new(
account: Current.account,
code: params[:code],
inbox_id: params[:inbox_id]
)
@hook = hook_builder.perform
ActiveRecord::Base.transaction do
builder = Integrations::Slack::HookBuilder.new(
account: Current.account,
code: params[:code],
inbox_id: params[:inbox_id]
)
@hook = builder.perform
create_chatwoot_slack_channel
end
end
def update
@hook = channel_builder.update(permitted_params[:reference_id])
render json: { error: I18n.t('errors.slack.invalid_channel_id') }, status: :unprocessable_entity if @hook.blank?
create_chatwoot_slack_channel
render json: @hook
end
def destroy
@hook.destroy!
head :ok
end
@@ -31,11 +31,11 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'slack')
end
def channel_builder
Integrations::Slack::ChannelBuilder.new(hook: @hook)
end
def permitted_params
params.permit(:reference_id)
def create_chatwoot_slack_channel
channel = params[:channel] || 'customer-conversations'
builder = Integrations::Slack::ChannelBuilder.new(
hook: @hook, channel: channel
)
builder.perform
end
end
@@ -39,6 +39,16 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
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 execute
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
@@ -12,8 +12,8 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
}
)
if redirect_url
cache_key = "microsoft::#{email.downcase}"
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
email = email.downcase
::Redis::Alfred.setex(email, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
@@ -1,15 +1,14 @@
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
RESULTS_PER_PAGE = 15
include DateRangeHelper
before_action :fetch_notification, only: [:update, :destroy, :snooze, :unread]
before_action :fetch_notification, only: [:update]
before_action :set_primary_actor, only: [:read_all]
before_action :set_current_page, only: [:index]
def index
@notifications = notification_finder.notifications
@unread_count = notification_finder.unread_count
@count = notification_finder.count
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
@count = notifications.count
@notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
end
def read_all
@@ -29,36 +28,11 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
render json: @notification
end
def unread
@notification.update(read_at: nil)
render json: @notification
end
def destroy
@notification.destroy
head :ok
end
def destroy_all
if params[:type] == 'read'
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read)
else
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all)
end
head :ok
end
def unread_count
@unread_count = notification_finder.unread_count
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
render json: @unread_count
end
def snooze
updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil)
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until]
render json: @notification
end
private
def set_primary_actor
@@ -76,7 +50,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
@current_page = params[:page] || 1
end
def notification_finder
@notification_finder ||= NotificationFinder.new(Current.user, Current.account, params)
def notifications
@notifications ||= current_user.notifications.where(account_id: current_account.id)
end
end
@@ -1,7 +1,7 @@
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
include ::FileTypeHelper
before_action :fetch_portal, except: [:index, :create]
before_action :fetch_portal, except: [:index, :create, :attach_file]
before_action :check_authorization
before_action :set_current_page, only: [:index]
@@ -47,17 +47,22 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok
end
def logo
@portal.logo.purge if @portal.logo.attached?
head :ok
end
def process_attached_logo
blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_by(id: blob_id)
@portal.logo.attach(blob)
end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:logo].tempfile,
filename: params[:logo].original_filename,
content_type: params[:logo].content_type
)
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
private
def fetch_portal
@@ -1,13 +0,0 @@
class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
def create
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
file_blob.save!
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
end
end
+3 -24
View File
@@ -5,13 +5,11 @@ class Api::V1::AccountsController < Api::BaseController
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
only: [:create], raise: false
before_action :check_signup_enabled, only: [:create]
before_action :ensure_account_name, only: [:create]
before_action :validate_captcha, only: [:create]
before_action :fetch_account, except: [:create]
before_action :check_authorization, except: [:create]
rescue_from CustomExceptions::Account::InvalidEmail,
CustomExceptions::Account::InvalidParams,
CustomExceptions::Account::UserExists,
CustomExceptions::Account::UserErrors,
with: :render_error_response
@@ -39,15 +37,11 @@ class Api::V1::AccountsController < Api::BaseController
end
def cache_keys
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
render json: { cache_keys: cache_keys_for_account }, status: :ok
render json: { cache_keys: get_cache_keys }, status: :ok
end
def update
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
@account.custom_attributes.merge!(custom_attributes_params)
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
@account.save!
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
end
def update_active_at
@@ -58,18 +52,7 @@ class Api::V1::AccountsController < Api::BaseController
private
def ensure_account_name
# ensure that account_name and user_full_name is present
# this is becuase the account builder and the models validations are not triggered
# this change is to align the behaviour with the v2 accounts controller
# since these values are not required directly there
return if account_params[:account_name].present?
return if account_params[:user_full_name].present?
raise CustomExceptions::Account::InvalidParams.new({})
end
def cache_keys_for_account
def get_cache_keys
{
label: fetch_value_for_key(params[:id], Label.name.underscore),
inbox: fetch_value_for_key(params[:id], Inbox.name.underscore),
@@ -86,10 +69,6 @@ class Api::V1::AccountsController < Api::BaseController
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
end
def custom_attributes_params
params.permit(:industry, :company_size, :timezone)
end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
end
@@ -1,15 +1,7 @@
class Api::V1::Integrations::WebhooksController < ApplicationController
def create
builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params)
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
response = builder.perform
render json: response
end
private
# TODO: This is a temporary solution to permit all params for slack unfurling job.
# We should only permit the params that we use. Handle all the params based on events and send it to the respective services.
def permitted_params
params.permit!
end
end
+1 -12
View File
@@ -10,9 +10,7 @@ class Api::V1::ProfilesController < Api::BaseController
@user.update!(password_params.except(:current_password))
end
@user.assign_attributes(profile_params)
@user.custom_attributes.merge!(custom_attributes_params)
@user.save!
@user.update!(profile_params)
end
def avatar
@@ -33,11 +31,6 @@ class Api::V1::ProfilesController < Api::BaseController
head :ok
end
def resend_confirmation
@user.send_confirmation_instructions unless @user.confirmed?
head :ok
end
private
def set_user
@@ -64,10 +57,6 @@ class Api::V1::ProfilesController < Api::BaseController
)
end
def custom_attributes_params
params.require(:profile).permit(:phone_number)
end
def password_params
params.require(:profile).permit(
:current_password,
@@ -79,9 +79,6 @@ class Api::V1::Widget::BaseController < ApplicationController
sender: @contact,
content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id,
content_attributes: {
in_reply_to: permitted_params[:message][:reply_to]
},
echo_id: permitted_params[:message][:echo_id],
message_type: :incoming
}
@@ -1,6 +1,5 @@
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
include Events::Types
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
def index
@conversation = conversation
@@ -28,21 +27,22 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
conversation.contact_last_seen_at = DateTime.now.utc
conversation.save!
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
head :ok
end
def transcript
if conversation.present? && conversation.contact.present? && conversation.contact.email.present?
if permitted_params[:email].present? && conversation.present?
ConversationReplyMailer.with(account: conversation.account).conversation_transcript(
conversation,
conversation.contact.email
permitted_params[:email]
)&.deliver_later
end
head :ok
end
def toggle_typing
head :ok && return if conversation.nil?
case permitted_params[:typing_status]
when 'on'
trigger_typing_event(CONVERSATION_TYPING_ON)
@@ -54,6 +54,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def toggle_status
return head :not_found if conversation.nil?
return head :forbidden unless @web_widget.end_conversation?
unless conversation.resolved?
@@ -79,10 +81,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
end
def render_not_found_if_empty
return head :not_found if conversation.nil?
end
def permitted_params
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
message: [:content, :referer_url, :timestamp, :echo_id],

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