Merge branch 'develop' into feature/conversation-refactor
This commit is contained in:
commit
b07bdfda1d
764 changed files with 25854 additions and 5499 deletions
|
@ -7,7 +7,7 @@ defaults: &defaults
|
||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
docker:
|
||||||
# specify the version you desire here
|
# specify the version you desire here
|
||||||
- image: circleci/ruby:2.6.5-node-browsers
|
- image: circleci/ruby:2.7.0-node-browsers
|
||||||
|
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
|
|
|
@ -26,3 +26,7 @@ exclude_patterns:
|
||||||
- "node_modules/**/*"
|
- "node_modules/**/*"
|
||||||
- "lib/tasks/auto_annotate_models.rake"
|
- "lib/tasks/auto_annotate_models.rake"
|
||||||
- "app/test-matchers.js"
|
- "app/test-matchers.js"
|
||||||
|
- "docs/*"
|
||||||
|
- "**/*.md"
|
||||||
|
- "**/*.yml"
|
||||||
|
- "app/javascript/dashboard/i18n/locale"
|
||||||
|
|
62
.env.example
62
.env.example
|
@ -1,6 +1,19 @@
|
||||||
SECRET_KEY_BASE=
|
# Used to verify the integrity of signed cookies. so ensure a secure value is set
|
||||||
|
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||||
|
|
||||||
#redis config
|
# Replace with the URL you are planning to use for your app
|
||||||
|
FRONTEND_URL=http://0.0.0.0:3000
|
||||||
|
|
||||||
|
# Force all access to the app over SSL, default is set to false
|
||||||
|
FORCE_SSL=false
|
||||||
|
|
||||||
|
# This lets you control new sign ups on your chatwoot installation
|
||||||
|
# true : default option, allows sign ups
|
||||||
|
# false : disables all the end points related to sign ups
|
||||||
|
# api_only: disables the UI for signup, but you can create sign ups via the account apis
|
||||||
|
ENABLE_ACCOUNT_SIGNUP=true
|
||||||
|
|
||||||
|
# Redis config
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
# If you are using docker-compose, set this variable's value to be any string,
|
# 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
|
# which will be the password for the redis service running inside the docker-compose
|
||||||
|
@ -14,18 +27,7 @@ POSTGRES_PASSWORD=
|
||||||
RAILS_ENV=development
|
RAILS_ENV=development
|
||||||
RAILS_MAX_THREADS=5
|
RAILS_MAX_THREADS=5
|
||||||
|
|
||||||
#fb app
|
# Mail outgoing
|
||||||
FB_VERIFY_TOKEN=
|
|
||||||
FB_APP_SECRET=
|
|
||||||
FB_APP_ID=
|
|
||||||
|
|
||||||
#twitter app
|
|
||||||
TWITTER_APP_ID=
|
|
||||||
TWITTER_CONSUMER_KEY=
|
|
||||||
TWITTER_CONSUMER_SECRET=
|
|
||||||
TWITTER_ENVIRONMENT=
|
|
||||||
|
|
||||||
#mail
|
|
||||||
MAILER_SENDER_EMAIL=accounts@chatwoot.com
|
MAILER_SENDER_EMAIL=accounts@chatwoot.com
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_DOMAIN=chatwoot.com
|
SMTP_DOMAIN=chatwoot.com
|
||||||
|
@ -37,23 +39,47 @@ SMTP_PASSWORD=
|
||||||
SMTP_AUTHENTICATION=
|
SMTP_AUTHENTICATION=
|
||||||
SMTP_ENABLE_STARTTLS_AUTO=
|
SMTP_ENABLE_STARTTLS_AUTO=
|
||||||
|
|
||||||
#misc
|
# Mail Incoming
|
||||||
FRONTEND_URL=http://0.0.0.0:3000
|
# Use one of the following based on the email ingress service
|
||||||
|
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||||
|
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||||
|
MAILGUN_INGRESS_SIGNING_KEY=
|
||||||
|
MANDRILL_INGRESS_API_KEY=
|
||||||
|
|
||||||
|
# Storage
|
||||||
ACTIVE_STORAGE_SERVICE=local
|
ACTIVE_STORAGE_SERVICE=local
|
||||||
|
|
||||||
#s3
|
# Amazon S3
|
||||||
S3_BUCKET_NAME=
|
S3_BUCKET_NAME=
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
AWS_REGION=
|
AWS_REGION=
|
||||||
|
|
||||||
#sentry
|
# Sentry
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|
||||||
|
# Log settings
|
||||||
|
# Disable if you want to write logs to a file
|
||||||
|
RAILS_LOG_TO_STDOUT=true
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_SIZE=500
|
||||||
|
|
||||||
# Credentials to access sidekiq dashboard in production
|
# Credentials to access sidekiq dashboard in production
|
||||||
SIDEKIQ_AUTH_USERNAME=
|
SIDEKIQ_AUTH_USERNAME=
|
||||||
SIDEKIQ_AUTH_PASSWORD=
|
SIDEKIQ_AUTH_PASSWORD=
|
||||||
|
|
||||||
|
### This environment variables are only required if you are setting up social media channels
|
||||||
|
#facebook
|
||||||
|
FB_VERIFY_TOKEN=
|
||||||
|
FB_APP_SECRET=
|
||||||
|
FB_APP_ID=
|
||||||
|
|
||||||
|
# Twitter
|
||||||
|
TWITTER_APP_ID=
|
||||||
|
TWITTER_CONSUMER_KEY=
|
||||||
|
TWITTER_CONSUMER_SECRET=
|
||||||
|
TWITTER_ENVIRONMENT=
|
||||||
|
|
||||||
#### This environment variables are only required in hosted version which has billing
|
#### This environment variables are only required in hosted version which has billing
|
||||||
ENABLE_BILLING=
|
ENABLE_BILLING=
|
||||||
|
|
||||||
|
|
10
.eslintrc.js
10
.eslintrc.js
|
@ -1,8 +1,8 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['airbnb/base', 'prettier', 'plugin:vue/recommended'],
|
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: 'babel-eslint',
|
parser: 'babel-eslint',
|
||||||
ecmaVersion: 2017,
|
ecmaVersion: 2020,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: ['html', 'prettier', 'babel'],
|
plugins: ['html', 'prettier', 'babel'],
|
||||||
|
@ -24,10 +24,12 @@ module.exports = {
|
||||||
'multiline': {
|
'multiline': {
|
||||||
'max': 1,
|
'max': 1,
|
||||||
'allowFirstLine': false
|
'allowFirstLine': false
|
||||||
}
|
},
|
||||||
}],
|
}],
|
||||||
'vue/html-self-closing': 'off',
|
'vue/html-self-closing': 'off',
|
||||||
"vue/no-v-html": 'off'
|
"vue/no-v-html": 'off',
|
||||||
|
'import/extensions': ['off']
|
||||||
|
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -37,6 +37,10 @@ public/packs*
|
||||||
*.swo
|
*.swo
|
||||||
*.un~
|
*.un~
|
||||||
.jest-cache
|
.jest-cache
|
||||||
|
|
||||||
|
#VS Code files
|
||||||
|
.vscode
|
||||||
|
|
||||||
# ignore jetbrains IDE files
|
# ignore jetbrains IDE files
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
@ -49,3 +53,5 @@ coverage
|
||||||
# ignore packages
|
# ignore packages
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
*.dump
|
||||||
|
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
12.16.1
|
72
.rubocop.yml
72
.rubocop.yml
|
@ -4,6 +4,10 @@ require:
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
inherit_from: .rubocop_todo.yml
|
inherit_from: .rubocop_todo.yml
|
||||||
|
|
||||||
|
Lint/RaiseException:
|
||||||
|
Enabled: true
|
||||||
|
Lint/StructNewOverride:
|
||||||
|
Enabled: true
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Max: 150
|
Max: 150
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
|
@ -16,6 +20,12 @@ Style/FrozenStringLiteralComment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Style/SymbolArray:
|
Style/SymbolArray:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Style/HashEachMethods:
|
||||||
|
Enabled: true
|
||||||
|
Style/HashTransformKeys:
|
||||||
|
Enabled: true
|
||||||
|
Style/HashTransformValues:
|
||||||
|
Enabled: true
|
||||||
Style/GlobalVars:
|
Style/GlobalVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/redis.rb'
|
- 'config/initializers/redis.rb'
|
||||||
|
@ -41,14 +51,58 @@ RSpec/NestedGroups:
|
||||||
Max: 4
|
Max: 4
|
||||||
RSpec/MessageSpies:
|
RSpec/MessageSpies:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Metrics/MethodLength:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20161123131628_devise_token_auth_create_users.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'
|
||||||
|
- 'lib/webhooks/chargebee.rb'
|
||||||
|
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||||
|
Metrics/AbcSize:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||||
|
Metrics/CyclomaticComplexity:
|
||||||
|
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'
|
||||||
|
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'
|
||||||
|
Rails/UniqueValidationWithoutIndex:
|
||||||
|
Exclude:
|
||||||
|
- 'app/models/channel/twitter_profile.rb'
|
||||||
|
- 'app/models/webhook.rb'
|
||||||
AllCops:
|
AllCops:
|
||||||
Exclude:
|
Exclude:
|
||||||
- db/*
|
- 'bin/**/*'
|
||||||
- bin/**/*
|
- 'db/schema.rb'
|
||||||
- db/**/*
|
- 'config/**/*'
|
||||||
- config/**/*
|
- 'public/**/*'
|
||||||
- public/**/*
|
- 'vendor/**/*'
|
||||||
- vendor/**/*
|
- 'node_modules/**/*'
|
||||||
- node_modules/**/*
|
- 'lib/tasks/auto_annotate_models.rake'
|
||||||
- lib/tasks/auto_annotate_models.rake
|
- 'config/environments/**/*'
|
||||||
- config/environments/**/*
|
- 'tmp/**/*'
|
||||||
|
- 'storage/**/*'
|
||||||
|
|
|
@ -282,15 +282,6 @@ Style/GlobalVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/redis/alfred.rb'
|
- 'lib/redis/alfred.rb'
|
||||||
|
|
||||||
# Offense count: 7
|
|
||||||
# Configuration parameters: MinBodyLength.
|
|
||||||
Style/GuardClause:
|
|
||||||
Exclude:
|
|
||||||
- 'app/builders/account_builder.rb'
|
|
||||||
- 'app/models/attachment.rb'
|
|
||||||
- 'app/models/message.rb'
|
|
||||||
- 'lib/webhooks/chargebee.rb'
|
|
||||||
|
|
||||||
# Offense count: 4
|
# Offense count: 4
|
||||||
Style/IdenticalConditionalBranches:
|
Style/IdenticalConditionalBranches:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2.6.5
|
2.7.0
|
||||||
|
|
|
@ -82,7 +82,7 @@ linters:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
ImportantRule:
|
ImportantRule:
|
||||||
enabled: true
|
enabled: false
|
||||||
|
|
||||||
ImportPath:
|
ImportPath:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -252,7 +252,7 @@ linters:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
UnnecessaryParentReference:
|
UnnecessaryParentReference:
|
||||||
enabled: true
|
enabled: false
|
||||||
|
|
||||||
UrlFormat:
|
UrlFormat:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
16
Gemfile
16
Gemfile
|
@ -1,6 +1,6 @@
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '2.6.5'
|
ruby '2.7.0'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
|
@ -17,6 +17,7 @@ gem 'jbuilder'
|
||||||
gem 'kaminari'
|
gem 'kaminari'
|
||||||
gem 'responders'
|
gem 'responders'
|
||||||
gem 'rest-client'
|
gem 'rest-client'
|
||||||
|
gem 'telephone_number'
|
||||||
gem 'time_diff'
|
gem 'time_diff'
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
gem 'valid_email2'
|
gem 'valid_email2'
|
||||||
|
@ -25,11 +26,12 @@ gem 'uglifier'
|
||||||
|
|
||||||
##-- for active storage --##
|
##-- for active storage --##
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage', require: false
|
gem 'azure-storage-blob', require: false
|
||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'mini_magick'
|
gem 'mini_magick'
|
||||||
|
|
||||||
##-- gems for database --#
|
##-- gems for database --#
|
||||||
|
gem 'groupdate'
|
||||||
gem 'pg'
|
gem 'pg'
|
||||||
gem 'redis'
|
gem 'redis'
|
||||||
gem 'redis-namespace'
|
gem 'redis-namespace'
|
||||||
|
@ -61,9 +63,9 @@ gem 'chargebee'
|
||||||
##--- gems for channels ---##
|
##--- gems for channels ---##
|
||||||
gem 'facebook-messenger'
|
gem 'facebook-messenger'
|
||||||
gem 'telegram-bot-ruby'
|
gem 'telegram-bot-ruby'
|
||||||
|
gem 'twilio-ruby', '~> 5.32.0'
|
||||||
# twitty will handle subscription of twitter account events
|
# twitty will handle subscription of twitter account events
|
||||||
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||||
|
|
||||||
# facebook client
|
# facebook client
|
||||||
gem 'koala'
|
gem 'koala'
|
||||||
# Random name generator
|
# Random name generator
|
||||||
|
@ -78,11 +80,17 @@ gem 'sentry-raven'
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
|
|
||||||
|
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||||
|
gem 'flag_shih_tzu'
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'annotate'
|
gem 'annotate'
|
||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
gem 'letter_opener'
|
gem 'letter_opener'
|
||||||
gem 'web-console'
|
gem 'web-console'
|
||||||
|
|
||||||
|
# used in swagger build
|
||||||
|
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
|
@ -93,7 +101,7 @@ group :development, :test do
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails'
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
gem 'mock_redis'
|
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'rspec-rails', '~> 4.0.0.beta2'
|
gem 'rspec-rails', '~> 4.0.0.beta2'
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
|
|
303
Gemfile.lock
303
Gemfile.lock
|
@ -1,65 +1,80 @@
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/chatwoot/twitty
|
remote: https://github.com/chatwoot/twitty
|
||||||
revision: c1edd557401d1e8a197b19e738f82e39507a8e2d
|
revision: af4f3e45dca55e42c64f7741a1fedfaa94d36419
|
||||||
specs:
|
specs:
|
||||||
twitty (0.1.0)
|
twitty (0.1.0)
|
||||||
oauth
|
oauth
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/sds/mock_redis
|
||||||
|
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||||
|
ref: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||||
|
specs:
|
||||||
|
mock_redis (0.22.0)
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/tzmfreedom/json_refs
|
||||||
|
revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d
|
||||||
|
ref: e32deb0
|
||||||
|
specs:
|
||||||
|
json_refs (0.1.2)
|
||||||
|
hana
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
action-cable-testing (0.6.0)
|
action-cable-testing (0.6.1)
|
||||||
actioncable (>= 5.0)
|
actioncable (>= 5.0)
|
||||||
actioncable (6.0.2.1)
|
actioncable (6.0.2.2)
|
||||||
actionpack (= 6.0.2.1)
|
actionpack (= 6.0.2.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.0.2.1)
|
actionmailbox (6.0.2.2)
|
||||||
actionpack (= 6.0.2.1)
|
actionpack (= 6.0.2.2)
|
||||||
activejob (= 6.0.2.1)
|
activejob (= 6.0.2.2)
|
||||||
activerecord (= 6.0.2.1)
|
activerecord (= 6.0.2.2)
|
||||||
activestorage (= 6.0.2.1)
|
activestorage (= 6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.0.2.1)
|
actionmailer (6.0.2.2)
|
||||||
actionpack (= 6.0.2.1)
|
actionpack (= 6.0.2.2)
|
||||||
actionview (= 6.0.2.1)
|
actionview (= 6.0.2.2)
|
||||||
activejob (= 6.0.2.1)
|
activejob (= 6.0.2.2)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.0.2.1)
|
actionpack (6.0.2.2)
|
||||||
actionview (= 6.0.2.1)
|
actionview (= 6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
rack (~> 2.0, >= 2.0.8)
|
rack (~> 2.0, >= 2.0.8)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.0.2.1)
|
actiontext (6.0.2.2)
|
||||||
actionpack (= 6.0.2.1)
|
actionpack (= 6.0.2.2)
|
||||||
activerecord (= 6.0.2.1)
|
activerecord (= 6.0.2.2)
|
||||||
activestorage (= 6.0.2.1)
|
activestorage (= 6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.0.2.1)
|
actionview (6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
activejob (6.0.2.1)
|
activejob (6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.0.2.1)
|
activemodel (6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
activerecord (6.0.2.1)
|
activerecord (6.0.2.2)
|
||||||
activemodel (= 6.0.2.1)
|
activemodel (= 6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
activestorage (6.0.2.1)
|
activestorage (6.0.2.2)
|
||||||
actionpack (= 6.0.2.1)
|
actionpack (= 6.0.2.2)
|
||||||
activejob (= 6.0.2.1)
|
activejob (= 6.0.2.2)
|
||||||
activerecord (= 6.0.2.1)
|
activerecord (= 6.0.2.2)
|
||||||
marcel (~> 0.3.1)
|
marcel (~> 0.3.1)
|
||||||
activesupport (6.0.2.1)
|
activesupport (6.0.2.2)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
|
@ -69,46 +84,44 @@ GEM
|
||||||
activerecord (>= 5.0, < 6.1)
|
activerecord (>= 5.0, < 6.1)
|
||||||
addressable (2.7.0)
|
addressable (2.7.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
annotate (3.0.3)
|
annotate (3.1.1)
|
||||||
activerecord (>= 3.2, < 7.0)
|
activerecord (>= 3.2, < 7.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.0)
|
ast (2.4.0)
|
||||||
attr_extras (6.2.3)
|
attr_extras (6.2.3)
|
||||||
aws-eventstream (1.0.3)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.269.0)
|
aws-partitions (1.296.0)
|
||||||
aws-sdk-core (3.89.1)
|
aws-sdk-core (3.94.0)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.28.0)
|
aws-sdk-kms (1.30.0)
|
||||||
aws-sdk-core (~> 3, >= 3.71.0)
|
aws-sdk-core (~> 3, >= 3.71.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.60.1)
|
aws-sdk-s3 (1.61.2)
|
||||||
aws-sdk-core (~> 3, >= 3.83.0)
|
aws-sdk-core (~> 3, >= 3.83.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.1.0)
|
aws-sigv4 (1.1.1)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||||
axiom-types (0.1.1)
|
axiom-types (0.1.1)
|
||||||
descendants_tracker (~> 0.0.4)
|
descendants_tracker (~> 0.0.4)
|
||||||
ice_nine (~> 0.11.0)
|
ice_nine (~> 0.11.0)
|
||||||
thread_safe (~> 0.3, >= 0.3.1)
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
azure-core (0.1.15)
|
azure-storage-blob (2.0.0)
|
||||||
faraday (~> 0.9)
|
azure-storage-common (~> 2.0)
|
||||||
faraday_middleware (~> 0.10)
|
nokogiri (~> 1.10.4)
|
||||||
nokogiri (~> 1.6)
|
azure-storage-common (2.0.1)
|
||||||
azure-storage (0.15.0.preview)
|
faraday (~> 1.0)
|
||||||
azure-core (~> 0.1)
|
faraday_middleware (~> 1.0.0.rc1)
|
||||||
faraday (~> 0.9)
|
nokogiri (~> 1.10.4)
|
||||||
faraday_middleware (~> 0.10)
|
|
||||||
nokogiri (~> 1.6, >= 1.6.8)
|
|
||||||
bcrypt (3.1.13)
|
bcrypt (3.1.13)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.4.5)
|
bootsnap (1.4.6)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.7.2)
|
brakeman (4.8.1)
|
||||||
browser (3.0.3)
|
browser (4.0.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (6.1.0)
|
bullet (6.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -119,13 +132,13 @@ GEM
|
||||||
bundler (>= 1.2.0, < 3)
|
bundler (>= 1.2.0, < 3)
|
||||||
thor (~> 0.18)
|
thor (~> 0.18)
|
||||||
byebug (11.1.1)
|
byebug (11.1.1)
|
||||||
chargebee (2.7.3)
|
chargebee (2.7.5)
|
||||||
json_pure (~> 2.1)
|
json_pure (~> 2.1)
|
||||||
rest-client (>= 1.8, < 3.0)
|
rest-client (>= 1.8, < 3.0)
|
||||||
coderay (1.1.2)
|
coderay (1.1.2)
|
||||||
coercible (1.0.0)
|
coercible (1.0.0)
|
||||||
descendants_tracker (~> 0.0.1)
|
descendants_tracker (~> 0.0.1)
|
||||||
concurrent-ruby (1.1.5)
|
concurrent-ruby (1.1.6)
|
||||||
connection_pool (2.2.2)
|
connection_pool (2.2.2)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
declarative (0.0.10)
|
declarative (0.0.10)
|
||||||
|
@ -143,7 +156,7 @@ GEM
|
||||||
devise (> 3.5.2, < 5)
|
devise (> 3.5.2, < 5)
|
||||||
rails (>= 4.2.0, < 6.1)
|
rails (>= 4.2.0, < 6.1)
|
||||||
diff-lcs (1.3)
|
diff-lcs (1.3)
|
||||||
digest-crc (0.4.1)
|
digest-crc (0.5.1)
|
||||||
docile (1.3.2)
|
docile (1.3.2)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
|
@ -157,22 +170,23 @@ GEM
|
||||||
facebook-messenger (1.4.1)
|
facebook-messenger (1.4.1)
|
||||||
httparty (~> 0.13, >= 0.13.7)
|
httparty (~> 0.13, >= 0.13.7)
|
||||||
rack (>= 1.4.5)
|
rack (>= 1.4.5)
|
||||||
factory_bot (5.1.1)
|
factory_bot (5.1.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
factory_bot_rails (5.1.1)
|
factory_bot_rails (5.1.1)
|
||||||
factory_bot (~> 5.1.0)
|
factory_bot (~> 5.1.0)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
faker (2.10.1)
|
faker (2.11.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (0.17.3)
|
faraday (1.0.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
faraday_middleware (0.14.0)
|
faraday_middleware (1.0.0)
|
||||||
faraday (>= 0.7.4, < 1.0)
|
faraday (~> 1.0)
|
||||||
ffi (1.12.2)
|
ffi (1.12.2)
|
||||||
foreman (0.87.0)
|
flag_shih_tzu (0.3.23)
|
||||||
|
foreman (0.87.1)
|
||||||
globalid (0.4.2)
|
globalid (0.4.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
google-api-client (0.36.4)
|
google-api-client (0.38.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (~> 0.9)
|
googleauth (~> 0.9)
|
||||||
httpclient (>= 2.8.1, < 3.0)
|
httpclient (>= 2.8.1, < 3.0)
|
||||||
|
@ -183,29 +197,32 @@ GEM
|
||||||
google-cloud-core (1.5.0)
|
google-cloud-core (1.5.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (~> 1.0)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.3.0)
|
google-cloud-env (1.3.1)
|
||||||
faraday (~> 0.11)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
google-cloud-errors (1.0.0)
|
google-cloud-errors (1.0.0)
|
||||||
google-cloud-storage (1.25.1)
|
google-cloud-storage (1.26.0)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-api-client (~> 0.33)
|
google-api-client (~> 0.33)
|
||||||
google-cloud-core (~> 1.2)
|
google-cloud-core (~> 1.2)
|
||||||
googleauth (~> 0.9)
|
googleauth (~> 0.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (0.10.0)
|
googleauth (0.12.0)
|
||||||
faraday (~> 0.12)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
memoist (~> 0.16)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (~> 0.12)
|
signet (~> 0.14)
|
||||||
|
groupdate (5.0.0)
|
||||||
|
activesupport (>= 5)
|
||||||
haikunator (1.1.0)
|
haikunator (1.1.0)
|
||||||
|
hana (1.3.5)
|
||||||
hashie (4.1.0)
|
hashie (4.1.0)
|
||||||
http-accept (1.7.0)
|
http-accept (1.7.0)
|
||||||
http-cookie (1.0.3)
|
http-cookie (1.0.3)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httparty (0.17.3)
|
httparty (0.18.0)
|
||||||
mime-types (~> 3.0)
|
mime-types (~> 3.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
|
@ -214,11 +231,11 @@ GEM
|
||||||
ice_nine (0.11.2)
|
ice_nine (0.11.2)
|
||||||
inflecto (0.0.2)
|
inflecto (0.0.2)
|
||||||
jaro_winkler (1.5.4)
|
jaro_winkler (1.5.4)
|
||||||
jbuilder (2.9.1)
|
jbuilder (2.10.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0.0)
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
json (2.3.0)
|
json (2.3.0)
|
||||||
json_pure (2.2.0)
|
json_pure (2.3.0)
|
||||||
jwt (2.2.1)
|
jwt (2.2.1)
|
||||||
kaminari (1.2.0)
|
kaminari (1.2.0)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
|
@ -236,14 +253,14 @@ GEM
|
||||||
addressable
|
addressable
|
||||||
faraday
|
faraday
|
||||||
json (>= 1.8)
|
json (>= 1.8)
|
||||||
launchy (2.4.3)
|
launchy (2.5.0)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.7)
|
||||||
letter_opener (1.7.0)
|
letter_opener (1.7.0)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
listen (3.2.1)
|
listen (3.2.1)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.4.0)
|
loofah (2.5.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -251,7 +268,7 @@ GEM
|
||||||
marcel (0.3.3)
|
marcel (0.3.3)
|
||||||
mimemagic (~> 0.3.2)
|
mimemagic (~> 0.3.2)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
method_source (0.9.2)
|
method_source (1.0.0)
|
||||||
mime-types (3.3.1)
|
mime-types (3.3.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2019.1009)
|
mime-types-data (3.2019.1009)
|
||||||
|
@ -260,35 +277,34 @@ GEM
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
minitest (5.14.0)
|
minitest (5.14.0)
|
||||||
mock_redis (0.22.0)
|
msgpack (1.3.3)
|
||||||
msgpack (1.3.1)
|
|
||||||
multi_json (1.14.1)
|
multi_json (1.14.1)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
nightfury (1.0.1)
|
nightfury (1.0.1)
|
||||||
nio4r (2.5.2)
|
nio4r (2.5.2)
|
||||||
nokogiri (1.10.7)
|
nokogiri (1.10.9)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
oauth (0.5.4)
|
oauth (0.5.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.0.1)
|
os (1.1.0)
|
||||||
parallel (1.19.1)
|
parallel (1.19.1)
|
||||||
parser (2.7.0.2)
|
parser (2.7.1.1)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.0)
|
||||||
pg (1.2.2)
|
pg (1.2.3)
|
||||||
pry (0.12.2)
|
pry (0.13.1)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1)
|
||||||
method_source (~> 0.9.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.3)
|
public_suffix (4.0.4)
|
||||||
puma (4.3.1)
|
puma (4.3.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
rack (2.1.2)
|
rack (2.2.2)
|
||||||
rack-cache (1.11.0)
|
rack-cache (1.11.1)
|
||||||
rack (>= 0.4)
|
rack (>= 0.4)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
|
@ -298,29 +314,29 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (6.0.2.1)
|
rails (6.0.2.2)
|
||||||
actioncable (= 6.0.2.1)
|
actioncable (= 6.0.2.2)
|
||||||
actionmailbox (= 6.0.2.1)
|
actionmailbox (= 6.0.2.2)
|
||||||
actionmailer (= 6.0.2.1)
|
actionmailer (= 6.0.2.2)
|
||||||
actionpack (= 6.0.2.1)
|
actionpack (= 6.0.2.2)
|
||||||
actiontext (= 6.0.2.1)
|
actiontext (= 6.0.2.2)
|
||||||
actionview (= 6.0.2.1)
|
actionview (= 6.0.2.2)
|
||||||
activejob (= 6.0.2.1)
|
activejob (= 6.0.2.2)
|
||||||
activemodel (= 6.0.2.1)
|
activemodel (= 6.0.2.2)
|
||||||
activerecord (= 6.0.2.1)
|
activerecord (= 6.0.2.2)
|
||||||
activestorage (= 6.0.2.1)
|
activestorage (= 6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.3.0)
|
||||||
railties (= 6.0.2.1)
|
railties (= 6.0.2.2)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.3.0)
|
rails-html-sanitizer (1.3.0)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
railties (6.0.2.1)
|
railties (6.0.2.2)
|
||||||
actionpack (= 6.0.2.1)
|
actionpack (= 6.0.2.2)
|
||||||
activesupport (= 6.0.2.1)
|
activesupport (= 6.0.2.2)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.20.3, < 2.0)
|
thor (>= 0.20.3, < 2.0)
|
||||||
|
@ -335,7 +351,7 @@ GEM
|
||||||
redis-rack-cache (2.2.1)
|
redis-rack-cache (2.2.1)
|
||||||
rack-cache (>= 1.10, < 2)
|
rack-cache (>= 1.10, < 2)
|
||||||
redis-store (>= 1.6, < 2)
|
redis-store (>= 1.6, < 2)
|
||||||
redis-store (1.8.1)
|
redis-store (1.8.2)
|
||||||
redis (>= 4, < 5)
|
redis (>= 4, < 5)
|
||||||
representable (3.0.4)
|
representable (3.0.4)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
|
@ -350,15 +366,16 @@ GEM
|
||||||
mime-types (>= 1.16, < 4.0)
|
mime-types (>= 1.16, < 4.0)
|
||||||
netrc (~> 0.8)
|
netrc (~> 0.8)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
|
rexml (3.2.4)
|
||||||
rspec-core (3.9.1)
|
rspec-core (3.9.1)
|
||||||
rspec-support (~> 3.9.1)
|
rspec-support (~> 3.9.1)
|
||||||
rspec-expectations (3.9.0)
|
rspec-expectations (3.9.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.9.0)
|
rspec-support (~> 3.9.0)
|
||||||
rspec-mocks (3.9.1)
|
rspec-mocks (3.9.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.9.0)
|
rspec-support (~> 3.9.0)
|
||||||
rspec-rails (4.0.0.beta4)
|
rspec-rails (4.0.0)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
|
@ -367,19 +384,21 @@ GEM
|
||||||
rspec-mocks (~> 3.9)
|
rspec-mocks (~> 3.9)
|
||||||
rspec-support (~> 3.9)
|
rspec-support (~> 3.9)
|
||||||
rspec-support (3.9.2)
|
rspec-support (3.9.2)
|
||||||
rubocop (0.79.0)
|
rubocop (0.81.0)
|
||||||
jaro_winkler (~> 1.5.1)
|
jaro_winkler (~> 1.5.1)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.0.1)
|
parser (>= 2.7.0.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
rexml
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 1.7)
|
unicode-display_width (>= 1.4.0, < 2.0)
|
||||||
rubocop-performance (1.5.2)
|
rubocop-performance (1.5.2)
|
||||||
rubocop (>= 0.71.0)
|
rubocop (>= 0.71.0)
|
||||||
rubocop-rails (2.4.2)
|
rubocop-rails (2.5.2)
|
||||||
|
activesupport
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.72.0)
|
rubocop (>= 0.72.0)
|
||||||
rubocop-rspec (1.37.1)
|
rubocop-rspec (1.38.1)
|
||||||
rubocop (>= 0.68.1)
|
rubocop (>= 0.68.1)
|
||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.10.1)
|
||||||
sass (3.7.4)
|
sass (3.7.4)
|
||||||
|
@ -387,25 +406,26 @@ GEM
|
||||||
sass-listen (4.0.0)
|
sass-listen (4.0.0)
|
||||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||||
rb-inotify (~> 0.9, >= 0.9.7)
|
rb-inotify (~> 0.9, >= 0.9.7)
|
||||||
scout_apm (2.6.6)
|
scout_apm (2.6.7)
|
||||||
parser
|
parser
|
||||||
scss_lint (0.59.0)
|
scss_lint (0.59.0)
|
||||||
sass (~> 3.5, >= 3.5.5)
|
sass (~> 3.5, >= 3.5.5)
|
||||||
seed_dump (3.3.1)
|
seed_dump (3.3.1)
|
||||||
activerecord (>= 4)
|
activerecord (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
sentry-raven (2.13.0)
|
semantic_range (2.3.0)
|
||||||
faraday (>= 0.7.6, < 1.0)
|
sentry-raven (3.0.0)
|
||||||
shoulda-matchers (4.2.0)
|
faraday (>= 1.0)
|
||||||
|
shoulda-matchers (4.3.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
sidekiq (6.0.4)
|
sidekiq (6.0.6)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (>= 2.0.0)
|
rack (~> 2.0)
|
||||||
rack-protection (>= 2.0.0)
|
rack-protection (>= 2.0.0)
|
||||||
redis (>= 4.1.0)
|
redis (>= 4.1.0)
|
||||||
signet (0.12.0)
|
signet (0.14.0)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
faraday (~> 0.9)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
simplecov (0.17.1)
|
simplecov (0.17.1)
|
||||||
|
@ -428,12 +448,17 @@ GEM
|
||||||
faraday
|
faraday
|
||||||
inflecto
|
inflecto
|
||||||
virtus
|
virtus
|
||||||
|
telephone_number (1.4.6)
|
||||||
thor (0.20.3)
|
thor (0.20.3)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
time_diff (0.3.0)
|
time_diff (0.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
i18n
|
i18n
|
||||||
tzinfo (1.2.6)
|
twilio-ruby (5.32.0)
|
||||||
|
faraday (~> 1.0.0)
|
||||||
|
jwt (>= 1.5, <= 2.5)
|
||||||
|
nokogiri (>= 1.6, < 2.0)
|
||||||
|
tzinfo (1.2.7)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
tzinfo-data (1.2019.3)
|
tzinfo-data (1.2019.3)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
|
@ -442,10 +467,10 @@ GEM
|
||||||
execjs (>= 0.3.0, < 3)
|
execjs (>= 0.3.0, < 3)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.6)
|
unf_ext (0.0.7.7)
|
||||||
unicode-display_width (1.6.1)
|
unicode-display_width (1.7.0)
|
||||||
uniform_notifier (1.13.0)
|
uniform_notifier (1.13.0)
|
||||||
valid_email2 (3.1.3)
|
valid_email2 (3.2.2)
|
||||||
activemodel (>= 3.2)
|
activemodel (>= 3.2)
|
||||||
mail (~> 2.5)
|
mail (~> 2.5)
|
||||||
virtus (1.0.5)
|
virtus (1.0.5)
|
||||||
|
@ -460,15 +485,16 @@ GEM
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
webpacker (4.2.2)
|
webpacker (5.0.1)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 5.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 4.2)
|
railties (>= 5.2)
|
||||||
|
semantic_range (>= 2.3.0)
|
||||||
websocket-driver (0.7.1)
|
websocket-driver (0.7.1)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.4)
|
websocket-extensions (0.1.4)
|
||||||
wisper (2.0.0)
|
wisper (2.0.0)
|
||||||
zeitwerk (2.2.2)
|
zeitwerk (2.3.0)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -479,7 +505,7 @@ DEPENDENCIES
|
||||||
annotate
|
annotate
|
||||||
attr_extras
|
attr_extras
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
azure-storage
|
azure-storage-blob
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
browser
|
browser
|
||||||
|
@ -493,18 +519,21 @@ DEPENDENCIES
|
||||||
facebook-messenger
|
facebook-messenger
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
faker
|
faker
|
||||||
|
flag_shih_tzu
|
||||||
foreman
|
foreman
|
||||||
google-cloud-storage
|
google-cloud-storage
|
||||||
|
groupdate
|
||||||
haikunator
|
haikunator
|
||||||
hashie
|
hashie
|
||||||
jbuilder
|
jbuilder
|
||||||
|
json_refs!
|
||||||
jwt
|
jwt
|
||||||
kaminari
|
kaminari
|
||||||
koala
|
koala
|
||||||
letter_opener
|
letter_opener
|
||||||
listen
|
listen
|
||||||
mini_magick
|
mini_magick
|
||||||
mock_redis
|
mock_redis!
|
||||||
nightfury
|
nightfury
|
||||||
pg
|
pg
|
||||||
pry-rails
|
pry-rails
|
||||||
|
@ -532,7 +561,9 @@ DEPENDENCIES
|
||||||
spring
|
spring
|
||||||
spring-watcher-listen
|
spring-watcher-listen
|
||||||
telegram-bot-ruby
|
telegram-bot-ruby
|
||||||
|
telephone_number
|
||||||
time_diff
|
time_diff
|
||||||
|
twilio-ruby (~> 5.32.0)
|
||||||
twitty!
|
twitty!
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
uglifier
|
uglifier
|
||||||
|
@ -542,7 +573,7 @@ DEPENDENCIES
|
||||||
wisper (= 2.0.0)
|
wisper (= 2.0.0)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.6.5p114
|
ruby 2.7.0p0
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.0.2
|
2.1.2
|
||||||
|
|
12
README.md
12
README.md
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://storage.googleapis.com/chatwoot-assets/woot-logo.svg" alt="Woot-logo" width="240">
|
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240">
|
||||||
|
|
||||||
<div align="center">A simple and elegant live chat software</div>
|
<div align="center">A simple and elegant live chat software</div>
|
||||||
<div align="center">An opensource alternative to Intercom, Zendesk, Drift, Crisp etc.</div>
|
<div align="center">An opensource alternative to Intercom, Zendesk, Drift, Crisp etc.</div>
|
||||||
|
@ -23,7 +23,7 @@ ___
|
||||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/badge/chat-Discord-violet?logo=discord" alt="Chat on Discord"></a>
|
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/badge/chat-Discord-violet?logo=discord" alt="Chat on Discord"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
![ChatUI progess](https://storage.googleapis.com/chatwoot-assets/dashboard-screen.png)
|
![ChatUI progess](https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png)
|
||||||
|
|
||||||
## Background
|
## Background
|
||||||
|
|
||||||
|
@ -52,16 +52,10 @@ Follow this [link](https://www.chatwoot.com/docs/environment-variables) to under
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
You can use our official Docker image from [https://hub.docker.com/r/chatwoot/chatwoot](https://hub.docker.com/r/chatwoot/chatwoot)
|
Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`.
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull chatwoot/chatwoot
|
|
||||||
```
|
|
||||||
|
|
||||||
Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker.
|
Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker.
|
||||||
|
|
||||||
Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using docker composer.
|
|
||||||
|
|
||||||
## Contributors ✨
|
## Contributors ✨
|
||||||
|
|
||||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||||
|
|
47
app/actions/contact_identify_action.rb
Normal file
47
app/actions/contact_identify_action.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
class ContactIdentifyAction
|
||||||
|
pattr_initialize [:contact!, :params!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||||
|
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||||
|
update_contact
|
||||||
|
end
|
||||||
|
@contact
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= @contact.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def existing_identified_contact
|
||||||
|
return if params[:identifier].blank?
|
||||||
|
|
||||||
|
@existing_identified_contact ||= Contact.where(account_id: account.id).find_by(identifier: params[:identifier])
|
||||||
|
end
|
||||||
|
|
||||||
|
def existing_email_contact
|
||||||
|
return if params[:email].blank?
|
||||||
|
|
||||||
|
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_contacts?(existing_contact, _contact)
|
||||||
|
existing_contact && existing_contact.id != @contact.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_contact
|
||||||
|
@contact.update!(params.slice(:name, :email, :identifier))
|
||||||
|
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_contact(base_contact, merge_contact)
|
||||||
|
ContactMergeAction.new(
|
||||||
|
account: account,
|
||||||
|
base_contact: base_contact,
|
||||||
|
mergee_contact: merge_contact
|
||||||
|
).perform
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,9 +5,11 @@ class ContactMergeAction
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
validate_contacts
|
validate_contacts
|
||||||
merge_conversations
|
merge_conversations
|
||||||
|
merge_messages
|
||||||
merge_contact_inboxes
|
merge_contact_inboxes
|
||||||
remove_mergee_contact
|
remove_mergee_contact
|
||||||
end
|
end
|
||||||
|
@base_contact
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -15,7 +17,7 @@ class ContactMergeAction
|
||||||
def validate_contacts
|
def validate_contacts
|
||||||
return if belongs_to_account?(@base_contact) && belongs_to_account?(@mergee_contact)
|
return if belongs_to_account?(@base_contact) && belongs_to_account?(@mergee_contact)
|
||||||
|
|
||||||
raise Exception, 'contact does not belong to the account'
|
raise StandardError, 'contact does not belong to the account'
|
||||||
end
|
end
|
||||||
|
|
||||||
def belongs_to_account?(contact)
|
def belongs_to_account?(contact)
|
||||||
|
@ -26,6 +28,10 @@ class ContactMergeAction
|
||||||
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merge_messages
|
||||||
|
Message.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||||
|
end
|
||||||
|
|
||||||
def merge_contact_inboxes
|
def merge_contact_inboxes
|
||||||
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,18 +42,26 @@ class AccountBuilder
|
||||||
|
|
||||||
def create_and_link_user
|
def create_and_link_user
|
||||||
password = Time.now.to_i
|
password = Time.now.to_i
|
||||||
@user = @account.users.new(email: @email,
|
@user = User.new(email: @email,
|
||||||
password: password,
|
password: password,
|
||||||
password_confirmation: password,
|
password_confirmation: password,
|
||||||
role: User.roles['administrator'],
|
|
||||||
name: email_to_name(@email))
|
name: email_to_name(@email))
|
||||||
if @user.save!
|
if @user.save!
|
||||||
|
link_user_to_account(@user, @account)
|
||||||
@user
|
@user
|
||||||
else
|
else
|
||||||
raise UserErrors.new(errors: @user.errors)
|
raise UserErrors.new(errors: @user.errors)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def link_user_to_account(user, account)
|
||||||
|
AccountUser.create!(
|
||||||
|
account_id: account.id,
|
||||||
|
user_id: user.id,
|
||||||
|
role: AccountUser.roles['administrator']
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def email_to_name(email)
|
def email_to_name(email)
|
||||||
name = email[/[^@]+/]
|
name = email[/[^@]+/]
|
||||||
name.split('.').map(&:capitalize).join(' ')
|
name.split('.').map(&:capitalize).join(' ')
|
||||||
|
|
38
app/builders/contact_builder.rb
Normal file
38
app/builders/contact_builder.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
class ContactBuilder
|
||||||
|
pattr_initialize [:source_id!, :inbox!, :contact_attributes!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
|
||||||
|
return contact_inbox if contact_inbox
|
||||||
|
|
||||||
|
build_contact
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def account
|
||||||
|
@account ||= inbox.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_contact
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
contact = account.contacts.create!(
|
||||||
|
name: contact_attributes[:name],
|
||||||
|
phone_number: contact_attributes[:phone_number],
|
||||||
|
email: contact_attributes[:email],
|
||||||
|
identifier: contact_attributes[:identifier],
|
||||||
|
additional_attributes: contact_attributes[:additional_attributes]
|
||||||
|
)
|
||||||
|
contact_inbox = ::ContactInbox.create!(
|
||||||
|
contact_id: contact.id,
|
||||||
|
inbox_id: inbox.id,
|
||||||
|
source_id: source_id
|
||||||
|
)
|
||||||
|
|
||||||
|
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
|
||||||
|
contact_inbox
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,3 @@
|
||||||
require 'open-uri'
|
|
||||||
|
|
||||||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||||
# Assumptions
|
# Assumptions
|
||||||
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
||||||
|
@ -36,16 +34,14 @@ class Messages::MessageBuilder
|
||||||
return if contact.present?
|
return if contact.present?
|
||||||
|
|
||||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||||
avatar_resource = LocalResource.new(contact_params[:remote_avatar_url])
|
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
|
||||||
@contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
|
||||||
|
|
||||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_message
|
def build_message
|
||||||
@message = conversation.messages.create!(message_params)
|
@message = conversation.messages.create!(message_params)
|
||||||
(response.attachments || []).each do |attachment|
|
(response.attachments || []).each do |attachment|
|
||||||
attachment_obj = @message.build_attachment(attachment_params(attachment).except(:remote_file_url))
|
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||||
attachment_obj.save!
|
attachment_obj.save!
|
||||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,16 +1,31 @@
|
||||||
class Messages::Outgoing::NormalBuilder
|
class Messages::Outgoing::NormalBuilder
|
||||||
|
include ::FileTypeHelper
|
||||||
attr_reader :message
|
attr_reader :message
|
||||||
|
|
||||||
def initialize(user, conversation, params)
|
def initialize(user, conversation, params)
|
||||||
@content = params[:message]
|
@content = params[:content]
|
||||||
@private = ['1', 'true', 1, true].include? params[:private]
|
@private = params[:private] || false
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
@user = user
|
@user = user
|
||||||
@fb_id = params[:fb_id]
|
@fb_id = params[:fb_id]
|
||||||
|
@content_type = params[:content_type]
|
||||||
|
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||||
|
@attachments = params[:attachments]
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@message = @conversation.messages.create!(message_params)
|
@message = @conversation.messages.build(message_params)
|
||||||
|
if @attachments.present?
|
||||||
|
@attachments.each do |uploaded_attachment|
|
||||||
|
attachment = @message.attachments.new(
|
||||||
|
account_id: @message.account_id,
|
||||||
|
file_type: file_type(uploaded_attachment&.content_type)
|
||||||
|
)
|
||||||
|
attachment.file.attach(uploaded_attachment)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@message.save
|
||||||
|
@message
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -22,8 +37,10 @@ class Messages::Outgoing::NormalBuilder
|
||||||
message_type: :outgoing,
|
message_type: :outgoing,
|
||||||
content: @content,
|
content: @content,
|
||||||
private: @private,
|
private: @private,
|
||||||
user_id: @user.id,
|
user_id: @user&.id,
|
||||||
source_id: @fb_id
|
source_id: @fb_id,
|
||||||
|
content_type: @content_type,
|
||||||
|
items: @items
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
111
app/builders/v2/report_builder.rb
Normal file
111
app/builders/v2/report_builder.rb
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
class V2::ReportBuilder
|
||||||
|
attr_reader :account, :params
|
||||||
|
|
||||||
|
def initialize(account, params)
|
||||||
|
@account = account
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def timeseries
|
||||||
|
send(params[:metric])
|
||||||
|
end
|
||||||
|
|
||||||
|
# For backward compatible with old report
|
||||||
|
def build
|
||||||
|
timeseries.each_with_object([]) do |p, arr|
|
||||||
|
arr << { value: p[1], timestamp: p[0].to_time.to_i }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def summary
|
||||||
|
{
|
||||||
|
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.values.sum
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def scope
|
||||||
|
return account if params[:type].match?('account')
|
||||||
|
return inbox if params[:type].match?('inbox')
|
||||||
|
return user if params[:type].match?('agent')
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
@inbox ||= account.inboxes.where(id: params[:id]).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def user
|
||||||
|
@user ||= account.users.where(id: params[:id]).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversations_count
|
||||||
|
scope.conversations
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.count
|
||||||
|
end
|
||||||
|
|
||||||
|
# unscoped removes all scopes added to a model previously
|
||||||
|
def incoming_messages_count
|
||||||
|
scope.messages.unscoped.where(account_id: account.id).incoming
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def outgoing_messages_count
|
||||||
|
scope.messages.unscoped.where(account_id: account.id).outgoing
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolutions_count
|
||||||
|
scope.conversations
|
||||||
|
.resolved
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_first_response_time
|
||||||
|
scope.events
|
||||||
|
.where(name: 'first_response')
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_resolution_time
|
||||||
|
scope.events.where(name: 'conversation_resolved')
|
||||||
|
.group_by_day(:created_at, range: range, default_value: 0)
|
||||||
|
.average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def range
|
||||||
|
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Taking average of average is not too accurate
|
||||||
|
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||||
|
# TODO: Will optimize this later
|
||||||
|
def avg_resolution_time_summary
|
||||||
|
return 0 if avg_resolution_time.values.empty?
|
||||||
|
|
||||||
|
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_first_response_time_summary
|
||||||
|
return 0 if avg_first_response_time.values.empty?
|
||||||
|
|
||||||
|
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_date_time(datetime)
|
||||||
|
return datetime if datetime.is_a?(DateTime)
|
||||||
|
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||||
|
|
||||||
|
DateTime.strptime(datetime, '%s')
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,9 +1,16 @@
|
||||||
class Api::BaseController < ApplicationController
|
class Api::BaseController < ApplicationController
|
||||||
|
include AccessTokenAuthHelper
|
||||||
respond_to :json
|
respond_to :json
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_access_token!, if: :authenticate_by_access_token?
|
||||||
|
before_action :validate_bot_access_token!, if: :authenticate_by_access_token?
|
||||||
|
before_action :authenticate_user!, unless: :authenticate_by_access_token?
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def authenticate_by_access_token?
|
||||||
|
request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present?
|
||||||
|
end
|
||||||
|
|
||||||
def set_conversation
|
def set_conversation
|
||||||
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
|
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
|
||||||
end
|
end
|
||||||
|
|
54
app/controllers/api/v1/accounts/accounts_controller.rb
Normal file
54
app/controllers/api/v1/accounts/accounts_controller.rb
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
class Api::V1::Accounts::AccountsController < Api::BaseController
|
||||||
|
include AuthHelper
|
||||||
|
|
||||||
|
skip_before_action :verify_authenticity_token, only: [:create]
|
||||||
|
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
|
||||||
|
only: [:create], raise: false
|
||||||
|
before_action :check_signup_enabled, only: [:create]
|
||||||
|
before_action :check_authorization, except: [:create]
|
||||||
|
before_action :fetch_account, except: [:create]
|
||||||
|
|
||||||
|
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||||
|
CustomExceptions::Account::UserExists,
|
||||||
|
CustomExceptions::Account::UserErrors,
|
||||||
|
with: :render_error_response
|
||||||
|
|
||||||
|
def create
|
||||||
|
@user = AccountBuilder.new(
|
||||||
|
account_name: account_params[:account_name],
|
||||||
|
email: account_params[:email]
|
||||||
|
).perform
|
||||||
|
if @user
|
||||||
|
send_auth_headers(@user)
|
||||||
|
render 'devise/auth.json', locals: { resource: @user }
|
||||||
|
else
|
||||||
|
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render 'api/v1/accounts/show.json'
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
authorize(Account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_account
|
||||||
|
@account = current_user.accounts.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_params
|
||||||
|
params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :domain_emails_enabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_signup_enabled
|
||||||
|
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::Actions::ContactMergesController < Api::BaseController
|
class Api::V1::Accounts::Actions::ContactMergesController < Api::BaseController
|
||||||
before_action :set_base_contact, only: [:create]
|
before_action :set_base_contact, only: [:create]
|
||||||
before_action :set_mergee_contact, only: [:create]
|
before_action :set_mergee_contact, only: [:create]
|
||||||
|
|
69
app/controllers/api/v1/accounts/agents_controller.rb
Normal file
69
app/controllers/api/v1/accounts/agents_controller.rb
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
class Api::V1::Accounts::AgentsController < Api::BaseController
|
||||||
|
before_action :fetch_agent, except: [:create, :index]
|
||||||
|
before_action :check_authorization
|
||||||
|
before_action :find_user, only: [:create]
|
||||||
|
before_action :create_user, only: [:create]
|
||||||
|
before_action :save_account_user, only: [:create]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@agents = agents
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@agent.account_user.destroy
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@agent.update!(agent_params.except(:role))
|
||||||
|
@agent.account_user.update!(role: agent_params[:role]) if agent_params[:role]
|
||||||
|
render 'api/v1/models/user.json', locals: { resource: @agent }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
render 'api/v1/models/user.json', locals: { resource: @user }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
authorize(User)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_agent
|
||||||
|
@agent = agents.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_user
|
||||||
|
@user = User.find_by(email: new_agent_params[:email])
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_user
|
||||||
|
return 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,
|
||||||
|
role: new_agent_params[:role],
|
||||||
|
inviter_id: current_user.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def agent_params
|
||||||
|
params.require(:agent).permit(:email, :name, :role)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_agent_params
|
||||||
|
time = Time.now.to_i
|
||||||
|
params.require(:agent).permit(:email, :name, :role)
|
||||||
|
.merge!(password: time, password_confirmation: time, inviter: current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def agents
|
||||||
|
@agents ||= current_account.users
|
||||||
|
end
|
||||||
|
end
|
105
app/controllers/api/v1/accounts/callbacks_controller.rb
Normal file
105
app/controllers/api/v1/accounts/callbacks_controller.rb
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
class Api::V1::Accounts::CallbacksController < Api::BaseController
|
||||||
|
before_action :inbox, only: [:reauthorize_page]
|
||||||
|
|
||||||
|
def register_facebook_page
|
||||||
|
user_access_token = params[:user_access_token]
|
||||||
|
page_access_token = params[:page_access_token]
|
||||||
|
page_id = params[:page_id]
|
||||||
|
inbox_name = params[:inbox_name]
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
facebook_channel = current_account.facebook_pages.create!(
|
||||||
|
page_id: page_id, user_access_token: user_access_token,
|
||||||
|
page_access_token: page_access_token
|
||||||
|
)
|
||||||
|
@facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||||
|
set_avatar(@facebook_inbox, page_id)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def facebook_pages
|
||||||
|
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||||
|
end
|
||||||
|
|
||||||
|
# get params[:inbox_id], current_account, params[:omniauth_token]
|
||||||
|
def reauthorize_page
|
||||||
|
if @inbox&.facebook?
|
||||||
|
fb_page_id = @inbox.channel.page_id
|
||||||
|
page_details = fb_object.get_connections('me', 'accounts')
|
||||||
|
|
||||||
|
if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] })
|
||||||
|
update_fb_page(fb_page_id, page_detail['access_token'])
|
||||||
|
return head :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
head :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
@inbox = current_account.inboxes.find_by(id: params[:inbox_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_fb_page(fb_page_id, access_token)
|
||||||
|
get_fb_page(fb_page_id)&.update!(
|
||||||
|
user_access_token: @user_access_token, page_access_token: access_token
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_fb_page(fb_page_id)
|
||||||
|
current_account.facebook_pages.find_by(page_id: fb_page_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fb_object
|
||||||
|
@user_access_token = long_lived_token(params[:omniauth_token])
|
||||||
|
Koala::Facebook::API.new(@user_access_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def long_lived_token(omniauth_token)
|
||||||
|
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
|
||||||
|
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger e
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_already_existing_facebook_pages(data)
|
||||||
|
return [] if data.empty?
|
||||||
|
|
||||||
|
data.inject([]) do |result, page_detail|
|
||||||
|
page_detail[:exists] = current_account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
|
||||||
|
result << page_detail
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_avatar(facebook_inbox, page_id)
|
||||||
|
uri = get_avatar_url(page_id)
|
||||||
|
|
||||||
|
return unless uri
|
||||||
|
|
||||||
|
avatar_resource = LocalResource.new(uri)
|
||||||
|
facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_avatar_url(page_id)
|
||||||
|
begin
|
||||||
|
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
|
||||||
|
uri = URI.parse(url)
|
||||||
|
tries = 3
|
||||||
|
begin
|
||||||
|
response = uri.open(redirect: false)
|
||||||
|
rescue OpenURI::HTTPRedirect => e
|
||||||
|
uri = e.uri # assigned from the "Location" response header
|
||||||
|
retry if (tries -= 1).positive?
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
pic_url = response.base_uri.to_s
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||||
|
pic_url = nil
|
||||||
|
end
|
||||||
|
pic_url
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::CannedResponsesController < Api::BaseController
|
class Api::V1::Accounts::CannedResponsesController < Api::BaseController
|
||||||
before_action :fetch_canned_response, only: [:update, :destroy]
|
before_action :fetch_canned_response, only: [:update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
|
@ -0,0 +1,57 @@
|
||||||
|
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController
|
||||||
|
before_action :authorize_request
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
authenticate_twilio
|
||||||
|
build_inbox
|
||||||
|
setup_webhooks if @twilio_channel.sms?
|
||||||
|
rescue Twilio::REST::TwilioError => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
|
rescue StandardError => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorize_request
|
||||||
|
authorize ::Inbox
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_twilio
|
||||||
|
client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
|
||||||
|
client.messages.list(limit: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_webhooks
|
||||||
|
::Twilio::WebhookSetupService.new(inbox: @inbox).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
def phone_number
|
||||||
|
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def medium
|
||||||
|
permitted_params[:medium]
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_inbox
|
||||||
|
@twilio_channel = current_account.twilio_sms.create!(
|
||||||
|
account_sid: permitted_params[:account_sid],
|
||||||
|
auth_token: permitted_params[:auth_token],
|
||||||
|
phone_number: phone_number,
|
||||||
|
medium: medium
|
||||||
|
)
|
||||||
|
@inbox = current_account.inboxes.create(
|
||||||
|
name: permitted_params[:name],
|
||||||
|
channel: @twilio_channel
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.require(:twilio_channel).permit(
|
||||||
|
:account_id, :phone_number, :account_sid, :auth_token, :name, :medium
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::Contacts::ConversationsController < Api::BaseController
|
class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController
|
||||||
def index
|
def index
|
||||||
@conversations = current_account.conversations.includes(
|
@conversations = current_account.conversations.includes(
|
||||||
:assignee, :contact, :inbox
|
:assignee, :contact, :inbox
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::ContactsController < Api::BaseController
|
class Api::V1::Accounts::ContactsController < Api::BaseController
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
|
@ -1,7 +1,8 @@
|
||||||
class Api::V1::Conversations::AssignmentsController < Api::BaseController
|
class Api::V1::Accounts::Conversations::AssignmentsController < Api::BaseController
|
||||||
before_action :set_conversation, only: [:create]
|
before_action :set_conversation, only: [:create]
|
||||||
|
|
||||||
def create # assign agent to a conversation
|
# assign agent to a conversation
|
||||||
|
def create
|
||||||
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
|
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
|
||||||
assignee = current_account.users.find_by(id: params[:assignee_id])
|
assignee = current_account.users.find_by(id: params[:assignee_id])
|
||||||
@conversation.update_assignee(assignee)
|
@conversation.update_assignee(assignee)
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::Conversations::LabelsController < Api::BaseController
|
class Api::V1::Accounts::Conversations::LabelsController < Api::BaseController
|
||||||
before_action :set_conversation, only: [:create, :index]
|
before_action :set_conversation, only: [:create, :index]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -6,7 +6,8 @@ class Api::V1::Conversations::LabelsController < Api::BaseController
|
||||||
@labels = @conversation.label_list
|
@labels = @conversation.label_list
|
||||||
end
|
end
|
||||||
|
|
||||||
def index # all labels of the current conversation
|
# all labels of the current conversation
|
||||||
|
def index
|
||||||
@labels = @conversation.label_list
|
@labels = @conversation.label_list
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
class Api::V1::Accounts::Conversations::MessagesController < Api::BaseController
|
||||||
|
before_action :set_conversation, only: [:index, :create]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@messages = message_finder.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
|
||||||
|
@message = mb.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def message_finder
|
||||||
|
@message_finder ||= MessageFinder.new(@conversation, params)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,6 @@
|
||||||
class Api::V1::ConversationsController < Api::BaseController
|
class Api::V1::Accounts::ConversationsController < Api::BaseController
|
||||||
before_action :set_conversation, except: [:index]
|
before_action :conversation, except: [:index]
|
||||||
|
before_action :contact_inbox, only: [:create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
result = conversation_finder.perform
|
result = conversation_finder.perform
|
||||||
|
@ -7,10 +8,12 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||||
@conversations_count = result[:count]
|
@conversations_count = result[:count]
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def create
|
||||||
@messages = messages_finder.perform
|
@conversation = ::Conversation.create!(conversation_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
@status = @conversation.toggle_status
|
@status = @conversation.toggle_status
|
||||||
end
|
end
|
||||||
|
@ -27,15 +30,24 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||||
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
|
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_conversation
|
def conversation
|
||||||
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
|
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def contact_inbox
|
||||||
|
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_params
|
||||||
|
{
|
||||||
|
account_id: current_account.id,
|
||||||
|
inbox_id: @contact_inbox.inbox_id,
|
||||||
|
contact_id: @contact_inbox.contact_id,
|
||||||
|
contact_inbox_id: @contact_inbox.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def conversation_finder
|
def conversation_finder
|
||||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def messages_finder
|
|
||||||
@message_finder ||= MessageFinder.new(@conversation, params)
|
|
||||||
end
|
|
||||||
end
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::FacebookIndicatorsController < Api::BaseController
|
class Api::V1::Accounts::FacebookIndicatorsController < Api::BaseController
|
||||||
before_action :set_access_token
|
before_action :set_access_token
|
||||||
around_action :handle_with_exception
|
around_action :handle_with_exception
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ class Api::V1::FacebookIndicatorsController < Api::BaseController
|
||||||
def handle_with_exception
|
def handle_with_exception
|
||||||
yield
|
yield
|
||||||
rescue Facebook::Messenger::Error => e
|
rescue Facebook::Messenger::Error => e
|
||||||
|
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::InboxMembersController < Api::BaseController
|
class Api::V1::Accounts::InboxMembersController < Api::BaseController
|
||||||
before_action :fetch_inbox, only: [:create, :show]
|
before_action :fetch_inbox, only: [:create, :show]
|
||||||
before_action :current_agents_ids, only: [:create]
|
before_action :current_agents_ids, only: [:create]
|
||||||
|
|
66
app/controllers/api/v1/accounts/inboxes_controller.rb
Normal file
66
app/controllers/api/v1/accounts/inboxes_controller.rb
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
class Api::V1::Accounts::InboxesController < Api::BaseController
|
||||||
|
before_action :check_authorization
|
||||||
|
before_action :fetch_inbox, except: [:index, :create]
|
||||||
|
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@inboxes = policy_scope(current_account.inboxes)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget'
|
||||||
|
@inbox = current_account.inboxes.build(name: permitted_params[:name], channel: channel)
|
||||||
|
@inbox.avatar.attach(permitted_params[:avatar])
|
||||||
|
@inbox.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@inbox.update(inbox_update_params.except(:channel))
|
||||||
|
@inbox.channel.update!(inbox_update_params[:channel]) if @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_agent_bot
|
||||||
|
if @agent_bot
|
||||||
|
agent_bot_inbox = @inbox.agent_bot_inbox || AgentBotInbox.new(inbox: @inbox)
|
||||||
|
agent_bot_inbox.agent_bot = @agent_bot
|
||||||
|
agent_bot_inbox.save!
|
||||||
|
elsif @inbox.agent_bot_inbox.present?
|
||||||
|
@inbox.agent_bot_inbox.destroy!
|
||||||
|
end
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@inbox.destroy
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_inbox
|
||||||
|
@inbox = current_account.inboxes.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_agent_bot
|
||||||
|
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||||
|
end
|
||||||
|
|
||||||
|
def web_widgets
|
||||||
|
current_account.web_widgets
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
authorize(Inbox)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:id, :avatar, :name, channel: [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :agent_away_message])
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox_update_params
|
||||||
|
params.permit(:enable_auto_assignment, :name, :avatar, channel: [:website_url, :widget_color, :welcome_title,
|
||||||
|
:welcome_tagline, :agent_away_message])
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,6 @@
|
||||||
class Api::V1::LabelsController < Api::BaseController
|
class Api::V1::Accounts::LabelsController < Api::BaseController
|
||||||
def index # list all labels in account
|
# list all labels in account
|
||||||
|
def index
|
||||||
@labels = current_account.all_conversation_tags
|
@labels = current_account.all_conversation_tags
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
|
||||||
|
before_action :set_user, :load_notification_setting
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def update
|
||||||
|
update_flags
|
||||||
|
@notification_setting.save!
|
||||||
|
render action: 'show'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_user
|
||||||
|
@user = current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_notification_setting
|
||||||
|
@notification_setting = @user.notification_settings.find_by(account_id: current_account.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notification_setting_params
|
||||||
|
params.require(:notification_settings).permit(selected_email_flags: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_flags
|
||||||
|
@notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::ReportsController < Api::BaseController
|
class Api::V1::Accounts::ReportsController < Api::BaseController
|
||||||
include CustomExceptions::Report
|
include CustomExceptions::Report
|
||||||
include Constants::Report
|
include Constants::Report
|
||||||
|
|
||||||
|
@ -36,10 +36,6 @@ class Api::V1::ReportsController < Api::BaseController
|
||||||
current_user.account
|
current_user.account
|
||||||
end
|
end
|
||||||
|
|
||||||
def agent
|
|
||||||
@agent ||= current_account.users.find(params[:agent_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_summary_metrics
|
def account_summary_metrics
|
||||||
summary_metrics(ACCOUNT_METRICS, :account_summary_params, AVG_ACCOUNT_METRICS)
|
summary_metrics(ACCOUNT_METRICS, :account_summary_params, AVG_ACCOUNT_METRICS)
|
||||||
end
|
end
|
||||||
|
@ -51,16 +47,16 @@ class Api::V1::ReportsController < Api::BaseController
|
||||||
def summary_metrics(metrics, calc_function, avg_metrics)
|
def summary_metrics(metrics, calc_function, avg_metrics)
|
||||||
metrics.each_with_object({}) do |metric, result|
|
metrics.each_with_object({}) do |metric, result|
|
||||||
data = ReportBuilder.new(current_account, send(calc_function, metric)).build
|
data = ReportBuilder.new(current_account, send(calc_function, metric)).build
|
||||||
|
result[metric] = calculate_metric(data, metric, avg_metrics)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_metric(data, metric, avg_metrics)
|
||||||
|
sum = data.inject(0) { |val, hash| val + hash[:value].to_i }
|
||||||
if avg_metrics.include?(metric)
|
if avg_metrics.include?(metric)
|
||||||
sum = data.inject(0) { |sum, hash| sum + hash[:value].to_i }
|
|
||||||
sum /= data.length unless sum.zero?
|
sum /= data.length unless sum.zero?
|
||||||
else
|
|
||||||
sum = data.inject(0) { |sum, hash| sum + hash[:value].to_i }
|
|
||||||
end
|
|
||||||
|
|
||||||
result[metric] = sum
|
|
||||||
end
|
end
|
||||||
|
sum
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_summary_params(metric)
|
def account_summary_params(metric)
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::SubscriptionsController < Api::BaseController
|
class Api::V1::Accounts::SubscriptionsController < Api::BaseController
|
||||||
skip_before_action :check_subscription
|
skip_before_action :check_subscription
|
||||||
|
|
||||||
before_action :check_billing_enabled
|
before_action :check_billing_enabled
|
|
@ -1,4 +1,4 @@
|
||||||
class Api::V1::Inbox::WebhooksController < Api::BaseController
|
class Api::V1::Accounts::WebhooksController < Api::BaseController
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :fetch_webhook, only: [:update, :destroy]
|
before_action :fetch_webhook, only: [:update, :destroy]
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ class Api::V1::Inbox::WebhooksController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def webhook_params
|
def webhook_params
|
||||||
params.require(:webhook).permit(:account_id, :inbox_id, :urls).merge(urls: params[:urls])
|
params.require(:webhook).permit(:inbox_id, :url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_webhook
|
def fetch_webhook
|
|
@ -1,33 +0,0 @@
|
||||||
class Api::V1::AccountsController < Api::BaseController
|
|
||||||
include AuthHelper
|
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token, only: [:create]
|
|
||||||
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
|
|
||||||
only: [:create], raise: false
|
|
||||||
|
|
||||||
rescue_from CustomExceptions::Account::InvalidEmail,
|
|
||||||
CustomExceptions::Account::UserExists,
|
|
||||||
CustomExceptions::Account::UserErrors,
|
|
||||||
with: :render_error_response
|
|
||||||
|
|
||||||
def create
|
|
||||||
@user = AccountBuilder.new(
|
|
||||||
account_name: account_params[:account_name],
|
|
||||||
email: account_params[:email]
|
|
||||||
).perform
|
|
||||||
if @user
|
|
||||||
send_auth_headers(@user)
|
|
||||||
render json: {
|
|
||||||
data: @user.token_validation_response
|
|
||||||
}
|
|
||||||
else
|
|
||||||
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def account_params
|
|
||||||
params.permit(:account_name, :email)
|
|
||||||
end
|
|
||||||
end
|
|
8
app/controllers/api/v1/agent_bots_controller.rb
Normal file
8
app/controllers/api/v1/agent_bots_controller.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
class Api::V1::AgentBotsController < Api::BaseController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_before_action :check_subscription
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: AgentBot.all
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,52 +0,0 @@
|
||||||
class Api::V1::AgentsController < Api::BaseController
|
|
||||||
before_action :fetch_agent, except: [:create, :index]
|
|
||||||
before_action :check_authorization
|
|
||||||
before_action :build_agent, only: [:create]
|
|
||||||
|
|
||||||
def index
|
|
||||||
@agents = agents
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@agent.destroy
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@agent.update!(agent_params)
|
|
||||||
render json: @agent
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@agent.save!
|
|
||||||
render json: @agent
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
authorize(User)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_agent
|
|
||||||
@agent = agents.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_agent
|
|
||||||
@agent = agents.new(new_agent_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def agent_params
|
|
||||||
params.require(:agent).permit(:email, :name, :role)
|
|
||||||
end
|
|
||||||
|
|
||||||
def new_agent_params
|
|
||||||
time = Time.now.to_i
|
|
||||||
params.require(:agent).permit(:email, :name, :role)
|
|
||||||
.merge!(password: time, password_confirmation: time, inviter: current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
def agents
|
|
||||||
@agents ||= current_account.users
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,107 +0,0 @@
|
||||||
require 'rest-client'
|
|
||||||
require 'telegram/bot'
|
|
||||||
class Api::V1::CallbacksController < ApplicationController
|
|
||||||
skip_before_action :verify_authenticity_token, only: [:register_facebook_page]
|
|
||||||
skip_before_action :authenticate_user!, only: [:register_facebook_page], raise: false
|
|
||||||
|
|
||||||
def register_facebook_page
|
|
||||||
user_access_token = params[:user_access_token]
|
|
||||||
page_access_token = params[:page_access_token]
|
|
||||||
page_name = params[:page_name]
|
|
||||||
page_id = params[:page_id]
|
|
||||||
inbox_name = params[:inbox_name]
|
|
||||||
facebook_channel = current_account.facebook_pages.create!(
|
|
||||||
name: page_name, page_id: page_id, user_access_token: user_access_token,
|
|
||||||
page_access_token: page_access_token
|
|
||||||
)
|
|
||||||
set_avatar(facebook_channel, page_id)
|
|
||||||
inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
|
||||||
render json: inbox
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_facebook_pages
|
|
||||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
|
||||||
end
|
|
||||||
|
|
||||||
# get params[:inbox_id], current_account, params[:omniauth_token]
|
|
||||||
def reauthorize_page
|
|
||||||
if @inbox&.first&.facebook?
|
|
||||||
fb_page_id = @inbox.channel.page_id
|
|
||||||
page_details = fb_object.get_connections('me', 'accounts')
|
|
||||||
|
|
||||||
(page_details || []).each do |page_detail|
|
|
||||||
if fb_page_id == page_detail['id'] # found the page which has to be reauthorised
|
|
||||||
update_fb_page(fb_page_id, page_detail['access_token'])
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
head :unprocessable_entity
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def inbox
|
|
||||||
@inbox = current_account.inboxes.find_by(id: params[:inbox_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_fb_page
|
|
||||||
if fb_page(fb_page_id)
|
|
||||||
fb_page.update_attributes!(
|
|
||||||
user_access_token: @user_access_token, page_access_token: access_token
|
|
||||||
)
|
|
||||||
head :ok
|
|
||||||
else
|
|
||||||
head :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fb_page(fb_page_id)
|
|
||||||
current_account.facebook_pages.find_by(page_id: fb_page_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fb_object
|
|
||||||
@user_access_token = long_lived_token(params[:omniauth_token])
|
|
||||||
Koala::Facebook::API.new(@user_access_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def long_lived_token(omniauth_token)
|
|
||||||
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
|
|
||||||
long_lived_token = koala.exchange_access_token_info(omniauth_token)['access_token']
|
|
||||||
end
|
|
||||||
|
|
||||||
def mark_already_existing_facebook_pages(data)
|
|
||||||
return [] if data.empty?
|
|
||||||
|
|
||||||
data.inject([]) do |result, page_detail|
|
|
||||||
current_account.facebook_pages.exists?(page_id: page_detail['id']) ? page_detail.merge!(exists: true) : page_detail.merge!(exists: false)
|
|
||||||
result << page_detail
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_avatar(facebook_channel, page_id)
|
|
||||||
avatar_resource = LocalResource.new(get_avatar_url(page_id))
|
|
||||||
facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_avatar_url(page_id)
|
|
||||||
begin
|
|
||||||
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
|
|
||||||
uri = URI.parse(url)
|
|
||||||
tries = 3
|
|
||||||
begin
|
|
||||||
response = uri.open(redirect: false)
|
|
||||||
rescue OpenURI::HTTPRedirect => e
|
|
||||||
uri = e.uri # assigned from the "Location" response header
|
|
||||||
retry if (tries -= 1) > 0
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
pic_url = response.base_uri.to_s
|
|
||||||
Rails.logger.info(pic_url)
|
|
||||||
rescue StandardError => e
|
|
||||||
pic_url = nil
|
|
||||||
end
|
|
||||||
pic_url
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,8 +0,0 @@
|
||||||
class Api::V1::Conversations::MessagesController < Api::BaseController
|
|
||||||
before_action :set_conversation, only: [:create]
|
|
||||||
|
|
||||||
def create
|
|
||||||
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
|
|
||||||
@message = mb.perform
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,31 +0,0 @@
|
||||||
class Api::V1::InboxesController < Api::BaseController
|
|
||||||
before_action :check_authorization
|
|
||||||
before_action :fetch_inbox, only: [:destroy, :update]
|
|
||||||
|
|
||||||
def index
|
|
||||||
@inboxes = policy_scope(current_account.inboxes)
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@inbox.destroy
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@inbox.update(inbox_update_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def fetch_inbox
|
|
||||||
@inbox = current_account.inboxes.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_authorization
|
|
||||||
authorize(Inbox)
|
|
||||||
end
|
|
||||||
|
|
||||||
def inbox_update_params
|
|
||||||
params.require(:inbox).permit(:enable_auto_assignment)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -5,6 +5,7 @@ class Api::V1::WebhooksController < ApplicationController
|
||||||
|
|
||||||
before_action :login_from_basic_auth, only: [:chargebee]
|
before_action :login_from_basic_auth, only: [:chargebee]
|
||||||
before_action :check_billing_enabled, only: [:chargebee]
|
before_action :check_billing_enabled, only: [:chargebee]
|
||||||
|
|
||||||
def chargebee
|
def chargebee
|
||||||
chargebee_consumer.consume
|
chargebee_consumer.consume
|
||||||
head :ok
|
head :ok
|
||||||
|
|
|
@ -2,9 +2,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= @contact_inbox.conversations.find_by(
|
@conversation ||= @contact_inbox.conversations.where(
|
||||||
inbox_id: auth_token_params[:inbox_id]
|
inbox_id: auth_token_params[:inbox_id]
|
||||||
)
|
).last
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth_token_params
|
def auth_token_params
|
||||||
|
|
18
app/controllers/api/v1/widget/contacts_controller.rb
Normal file
18
app/controllers/api/v1/widget/contacts_controller.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||||
|
before_action :set_web_widget
|
||||||
|
before_action :set_contact
|
||||||
|
|
||||||
|
def update
|
||||||
|
contact_identify_action = ContactIdentifyAction.new(
|
||||||
|
contact: @contact,
|
||||||
|
params: permitted_params.to_h.deep_symbolize_keys
|
||||||
|
)
|
||||||
|
render json: contact_identify_action.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:website_token, :identifier, :email, :name, :avatar_url)
|
||||||
|
end
|
||||||
|
end
|
16
app/controllers/api/v1/widget/events_controller.rb
Normal file
16
app/controllers/api/v1/widget/events_controller.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
||||||
|
include Events::Types
|
||||||
|
before_action :set_web_widget
|
||||||
|
before_action :set_contact
|
||||||
|
|
||||||
|
def create
|
||||||
|
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox)
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:name, :website_token)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,48 +0,0 @@
|
||||||
class Api::V1::Widget::InboxesController < Api::BaseController
|
|
||||||
before_action :authorize_request
|
|
||||||
before_action :set_web_widget_channel, only: [:update]
|
|
||||||
before_action :set_inbox, only: [:update]
|
|
||||||
|
|
||||||
def create
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
channel = web_widgets.create!(
|
|
||||||
website_name: permitted_params[:website][:website_name],
|
|
||||||
website_url: permitted_params[:website][:website_url],
|
|
||||||
widget_color: permitted_params[:website][:widget_color]
|
|
||||||
)
|
|
||||||
@inbox = inboxes.create!(name: permitted_params[:website][:website_name], channel: channel)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@channel.update!(
|
|
||||||
widget_color: permitted_params[:website][:widget_color]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def authorize_request
|
|
||||||
authorize ::Inbox
|
|
||||||
end
|
|
||||||
|
|
||||||
def inboxes
|
|
||||||
current_account.inboxes
|
|
||||||
end
|
|
||||||
|
|
||||||
def web_widgets
|
|
||||||
current_account.web_widgets
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_web_widget_channel
|
|
||||||
@channel = web_widgets.find_by(id: permitted_params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_inbox
|
|
||||||
@inbox = @channel.inbox
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(:id, website: [:website_name, :website_url, :widget_color])
|
|
||||||
end
|
|
||||||
end
|
|
24
app/controllers/api/v1/widget/labels_controller.rb
Normal file
24
app/controllers/api/v1/widget/labels_controller.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController
|
||||||
|
before_action :set_web_widget
|
||||||
|
before_action :set_contact
|
||||||
|
|
||||||
|
def create
|
||||||
|
conversation.label_list.add(permitted_params[:label])
|
||||||
|
conversation.save!
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
conversation.label_list.remove(permitted_params[:id])
|
||||||
|
conversation.save!
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:id, :label, :website_token)
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,20 +10,36 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@message = conversation.messages.new(message_params)
|
@message = conversation.messages.new(message_params)
|
||||||
@message.save!
|
@message.save
|
||||||
render json: @message
|
build_attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@message.update!(input_submitted_email: contact_email)
|
if @message.content_type == 'input_email'
|
||||||
|
@message.update!(submitted_email: contact_email)
|
||||||
update_contact(contact_email)
|
update_contact(contact_email)
|
||||||
head :no_content
|
else
|
||||||
|
@message.update!(message_update_params[:message])
|
||||||
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
|
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def build_attachment
|
||||||
|
return if params[:message][:attachments].blank?
|
||||||
|
|
||||||
|
params[:message][:attachments].each do |uploaded_attachment|
|
||||||
|
attachment = @message.attachments.new(
|
||||||
|
account_id: @message.account_id,
|
||||||
|
file_type: helpers.file_type(uploaded_attachment&.content_type)
|
||||||
|
)
|
||||||
|
attachment.file.attach(uploaded_attachment)
|
||||||
|
end
|
||||||
|
@message.save!
|
||||||
|
end
|
||||||
|
|
||||||
def set_conversation
|
def set_conversation
|
||||||
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
|
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
|
||||||
end
|
end
|
||||||
|
@ -31,9 +47,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
def message_params
|
def message_params
|
||||||
{
|
{
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
|
contact_id: @contact.id,
|
||||||
|
content: permitted_params[:message][:content],
|
||||||
inbox_id: conversation.inbox_id,
|
inbox_id: conversation.inbox_id,
|
||||||
message_type: :incoming,
|
message_type: :incoming
|
||||||
content: permitted_params[:message][:content]
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -85,7 +102,11 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
def update_contact(email)
|
def update_contact(email)
|
||||||
contact_with_email = @account.contacts.find_by(email: email)
|
contact_with_email = @account.contacts.find_by(email: email)
|
||||||
if contact_with_email
|
if contact_with_email
|
||||||
::ContactMergeAction.new(account: @account, base_contact: contact_with_email, mergee_contact: @contact).perform
|
@contact = ::ContactMergeAction.new(
|
||||||
|
account: @account,
|
||||||
|
base_contact: contact_with_email,
|
||||||
|
mergee_contact: @contact
|
||||||
|
).perform
|
||||||
else
|
else
|
||||||
@contact.update!(
|
@contact.update!(
|
||||||
email: email,
|
email: email,
|
||||||
|
@ -102,6 +123,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
contact_email.split('@')[0]
|
contact_email.split('@')[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message_update_params
|
||||||
|
params.permit(message: [submitted_values: [:name, :title, :value]])
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp])
|
params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp])
|
||||||
end
|
end
|
||||||
|
|
39
app/controllers/api/v2/accounts/reports_controller.rb
Normal file
39
app/controllers/api/v2/accounts/reports_controller.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
class Api::V2::Accounts::ReportsController < Api::BaseController
|
||||||
|
def account
|
||||||
|
builder = V2::ReportBuilder.new(current_account, account_report_params)
|
||||||
|
data = builder.build
|
||||||
|
render json: data
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_summary
|
||||||
|
render json: account_summary_metrics
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def current_account
|
||||||
|
current_user.account
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_summary_params
|
||||||
|
{
|
||||||
|
type: :account,
|
||||||
|
since: params[:since],
|
||||||
|
until: params[:until]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_report_params
|
||||||
|
{
|
||||||
|
metric: params[:metric],
|
||||||
|
type: :account,
|
||||||
|
since: params[:since],
|
||||||
|
until: params[:until]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_summary_metrics
|
||||||
|
builder = V2::ReportBuilder.new(current_account, account_summary_params)
|
||||||
|
builder.summary
|
||||||
|
end
|
||||||
|
end
|
8
app/controllers/api_controller.rb
Normal file
8
app/controllers/api_controller.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
class ApiController < ApplicationController
|
||||||
|
skip_before_action :set_current_user, only: [:index]
|
||||||
|
skip_before_action :check_subscription, only: [:index]
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,7 +14,25 @@ class ApplicationController < ActionController::Base
|
||||||
private
|
private
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
@_ ||= current_user.account
|
@_ ||= find_current_account
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_current_account
|
||||||
|
account = Account.find(params[:account_id])
|
||||||
|
if current_user
|
||||||
|
account_accessible_for_user?(account)
|
||||||
|
elsif @resource&.is_a?(AgentBot)
|
||||||
|
account_accessible_for_bot?(account)
|
||||||
|
end
|
||||||
|
account
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_accessible_for_user?(account)
|
||||||
|
render_unauthorized('You are not authorized to access this account') unless account.account_users.find_by(user_id: current_user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_accessible_for_bot?(account)
|
||||||
|
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_with_exception
|
def handle_with_exception
|
||||||
|
|
26
app/controllers/concerns/access_token_auth_helper.rb
Normal file
26
app/controllers/concerns/access_token_auth_helper.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
module AccessTokenAuthHelper
|
||||||
|
BOT_ACCESSIBLE_ENDPOINTS = {
|
||||||
|
'api/v1/accounts/conversations' => %w[toggle_status create],
|
||||||
|
'api/v1/accounts/conversations/messages' => ['create']
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def authenticate_access_token!
|
||||||
|
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
|
||||||
|
access_token = AccessToken.find_by(token: token)
|
||||||
|
render_unauthorized('Invalid Access Token') && return unless access_token
|
||||||
|
|
||||||
|
token_owner = access_token.owner
|
||||||
|
@resource = token_owner
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_bot_access_token!
|
||||||
|
return if current_user.is_a?(User)
|
||||||
|
return if agent_bot_accessible?
|
||||||
|
|
||||||
|
render_unauthorized('Access to this endpoint is not authorized for bots')
|
||||||
|
end
|
||||||
|
|
||||||
|
def agent_bot_accessible?
|
||||||
|
BOT_ACCESSIBLE_ENDPOINTS.fetch(params[:controller], []).include?(params[:action])
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,9 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
||||||
@recoverable = User.find_by(reset_password_token: reset_password_token)
|
@recoverable = User.find_by(reset_password_token: reset_password_token)
|
||||||
if @recoverable && reset_password_and_confirmation(@recoverable)
|
if @recoverable && reset_password_and_confirmation(@recoverable)
|
||||||
send_auth_headers(@recoverable)
|
send_auth_headers(@recoverable)
|
||||||
render json: {
|
render 'devise/auth.json', locals: { resource: @recoverable }
|
||||||
data: @recoverable.token_validation_response
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422
|
render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,6 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
|
||||||
wrap_parameters format: []
|
wrap_parameters format: []
|
||||||
|
|
||||||
def render_create_success
|
def render_create_success
|
||||||
render 'devise/auth.json'
|
render 'devise/auth.json', locals: { resource: @resource }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
18
app/controllers/swagger_controller.rb
Normal file
18
app/controllers/swagger_controller.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
class SwaggerController < ApplicationController
|
||||||
|
def respond
|
||||||
|
if Rails.env.development? || Rails.env.test?
|
||||||
|
render inline: File.read(Rails.root.join('swagger', derived_path))
|
||||||
|
else
|
||||||
|
head 404
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def derived_path
|
||||||
|
params[:path] ||= 'index.html'
|
||||||
|
path = params[:path]
|
||||||
|
path << ".#{params[:format]}" unless path.ends_with?(params[:format].to_s)
|
||||||
|
path
|
||||||
|
end
|
||||||
|
end
|
31
app/controllers/twilio/callback_controller.rb
Normal file
31
app/controllers/twilio/callback_controller.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
class Twilio::CallbackController < ApplicationController
|
||||||
|
def create
|
||||||
|
::Twilio::IncomingMessageService.new(params: permitted_params).perform
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(
|
||||||
|
:ApiVersion,
|
||||||
|
:SmsSid,
|
||||||
|
:From,
|
||||||
|
:ToState,
|
||||||
|
:ToZip,
|
||||||
|
:AccountSid,
|
||||||
|
:MessageSid,
|
||||||
|
:FromCountry,
|
||||||
|
:ToCity,
|
||||||
|
:FromCity,
|
||||||
|
:To,
|
||||||
|
:FromZip,
|
||||||
|
:Body,
|
||||||
|
:ToCountry,
|
||||||
|
:FromState,
|
||||||
|
:MediaUrl0,
|
||||||
|
:MediaContentType0
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,7 +6,7 @@ class Twitter::AuthorizationsController < Twitter::BaseController
|
||||||
::Redis::Alfred.setex(oauth_token, account.id)
|
::Redis::Alfred.setex(oauth_token, account.id)
|
||||||
redirect_to oauth_authorize_endpoint(oauth_token)
|
redirect_to oauth_authorize_endpoint(oauth_token)
|
||||||
else
|
else
|
||||||
redirect_to app_new_twitter_inbox_url
|
redirect_to app_new_twitter_inbox_url(account_id: account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
class Twitter::CallbacksController < Twitter::BaseController
|
class Twitter::CallbacksController < Twitter::BaseController
|
||||||
def show
|
def show
|
||||||
|
return redirect_to twitter_app_redirect_url if permitted_params[:denied]
|
||||||
|
|
||||||
@response = twitter_client.access_token(
|
@response = twitter_client.access_token(
|
||||||
oauth_token: permitted_params[:oauth_token],
|
oauth_token: permitted_params[:oauth_token],
|
||||||
oauth_verifier: permitted_params[:oauth_verifier]
|
oauth_verifier: permitted_params[:oauth_verifier]
|
||||||
|
@ -8,9 +10,9 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||||
inbox = build_inbox
|
inbox = build_inbox
|
||||||
::Redis::Alfred.delete(permitted_params[:oauth_token])
|
::Redis::Alfred.delete(permitted_params[:oauth_token])
|
||||||
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
|
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
|
||||||
redirect_to app_twitter_inbox_agents_url(inbox_id: inbox.id)
|
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||||
else
|
else
|
||||||
redirect_to app_new_twitter_inbox_url
|
redirect_to twitter_app_redirect_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -28,13 +30,16 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||||
@account ||= Account.find_by!(id: account_id)
|
@account ||= Account.find_by!(id: account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def twitter_app_redirect_url
|
||||||
|
app_new_twitter_inbox_url(account_id: account.id)
|
||||||
|
end
|
||||||
|
|
||||||
def build_inbox
|
def build_inbox
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
twitter_profile = account.twitter_profiles.create(
|
twitter_profile = account.twitter_profiles.create(
|
||||||
twitter_access_token: parsed_body['oauth_token'],
|
twitter_access_token: parsed_body['oauth_token'],
|
||||||
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
||||||
profile_id: parsed_body['user_id'],
|
profile_id: parsed_body['user_id']
|
||||||
name: parsed_body['screen_name']
|
|
||||||
)
|
)
|
||||||
account.inboxes.create(
|
account.inboxes.create(
|
||||||
name: parsed_body['screen_name'],
|
name: parsed_body['screen_name'],
|
||||||
|
@ -46,6 +51,6 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:oauth_token, :oauth_verifier)
|
params.permit(:oauth_token, :oauth_verifier, :denied)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
class AsyncDispatcher < BaseDispatcher
|
class AsyncDispatcher < BaseDispatcher
|
||||||
def dispatch(event_name, timestamp, data)
|
def dispatch(event_name, timestamp, data)
|
||||||
|
EventDispatcherJob.perform_later(event_name, timestamp, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish_event(event_name, timestamp, data)
|
||||||
event_object = Events::Base.new(event_name, timestamp, data)
|
event_object = Events::Base.new(event_name, timestamp, data)
|
||||||
publish(event_object.method_name, event_object)
|
publish(event_object.method_name, event_object)
|
||||||
end
|
end
|
||||||
|
|
||||||
def listeners
|
def listeners
|
||||||
listeners = [ReportingListener.instance]
|
listeners = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
|
||||||
|
listeners << EventListener.instance
|
||||||
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
|
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
|
||||||
listeners
|
listeners
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
|
||||||
end
|
end
|
||||||
|
|
||||||
def listeners
|
def listeners
|
||||||
[ActionCableListener.instance]
|
[ActionCableListener.instance, AgentBotListener.instance]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
class ConversationFinder
|
class ConversationFinder
|
||||||
attr_reader :current_user, :current_account, :params
|
attr_reader :current_user, :current_account, :params
|
||||||
|
|
||||||
ASSIGNEE_TYPES = { me: 0, unassigned: 1, all: 2 }.freeze
|
|
||||||
|
|
||||||
ASSIGNEE_TYPES_BY_ID = ASSIGNEE_TYPES.invert
|
|
||||||
ASSIGNEE_TYPES_BY_ID.default = :me
|
|
||||||
|
|
||||||
DEFAULT_STATUS = 'open'.freeze
|
DEFAULT_STATUS = 'open'.freeze
|
||||||
|
|
||||||
# assumptions
|
# assumptions
|
||||||
# inbox_id if not given, take from all conversations, else specific to inbox
|
# inbox_id if not given, take from all conversations, else specific to inbox
|
||||||
# assignee_type if not given, take 'me'
|
# assignee_type if not given, take 'all'
|
||||||
# conversation_status if not given, take 'open'
|
# conversation_status if not given, take 'open'
|
||||||
|
|
||||||
# response of this class will be of type
|
# response of this class will be of type
|
||||||
# {conversations: [array of conversations], count: {open: count, resolved: count}}
|
# {conversations: [array of conversations], count: {open: count, resolved: count}}
|
||||||
|
|
||||||
# params
|
# params
|
||||||
# assignee_type_id, inbox_id, :status
|
# assignee_type, inbox_id, :status
|
||||||
|
|
||||||
def initialize(current_user, params)
|
def initialize(current_user, params)
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
|
@ -62,7 +57,7 @@ class ConversationFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_assignee_type
|
def set_assignee_type
|
||||||
@assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]]
|
@assignee_type = params[:assignee_type]
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
|
@ -72,12 +67,10 @@ class ConversationFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_assignee_type
|
def filter_by_assignee_type
|
||||||
if @assignee_type_id == ASSIGNEE_TYPES[:me]
|
if @assignee_type == 'me'
|
||||||
@conversations = @conversations.assigned_to(current_user)
|
@conversations = @conversations.assigned_to(current_user)
|
||||||
elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned]
|
elsif @assignee_type == 'unassigned'
|
||||||
@conversations = @conversations.unassigned
|
@conversations = @conversations.unassigned
|
||||||
elsif @assignee_type_id == ASSIGNEE_TYPES[:all]
|
|
||||||
@conversations
|
|
||||||
end
|
end
|
||||||
@conversations
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ class MessageFinder
|
||||||
private
|
private
|
||||||
|
|
||||||
def conversation_messages
|
def conversation_messages
|
||||||
@conversation.messages.includes(:attachment, user: { avatar_attachment: :blob })
|
@conversation.messages.includes(:attachments, user: { avatar_attachment: :blob })
|
||||||
end
|
end
|
||||||
|
|
||||||
def messages
|
def messages
|
||||||
|
|
14
app/helpers/file_type_helper.rb
Normal file
14
app/helpers/file_type_helper.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module FileTypeHelper
|
||||||
|
def file_type(content_type)
|
||||||
|
return :image if [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/gif',
|
||||||
|
'image/tiff',
|
||||||
|
'image/bmp'
|
||||||
|
].include?(content_type)
|
||||||
|
|
||||||
|
:file
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,7 +8,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import WootSnackbarBox from './components/SnackbarContainer';
|
import WootSnackbarBox from './components/SnackbarContainer';
|
||||||
|
import { accountIdFromPathname } from './helper/URLHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
@ -17,8 +20,28 @@ export default {
|
||||||
WootSnackbarBox,
|
WootSnackbarBox,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
getAccount: 'accounts/getAccount',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('setUser');
|
this.$store.dispatch('setUser');
|
||||||
|
this.initializeAccount();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async initializeAccount() {
|
||||||
|
const { pathname } = window.location;
|
||||||
|
const accountId = accountIdFromPathname(pathname);
|
||||||
|
|
||||||
|
if (accountId) {
|
||||||
|
await this.$store.dispatch('accounts/get');
|
||||||
|
const { locale } = this.getAccount(accountId);
|
||||||
|
Vue.config.lang = locale;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,9 +3,25 @@
|
||||||
const API_VERSION = `/api/v1`;
|
const API_VERSION = `/api/v1`;
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
constructor(url) {
|
constructor(resource, options = {}) {
|
||||||
this.apiVersion = API_VERSION;
|
this.apiVersion = API_VERSION;
|
||||||
this.url = `${this.apiVersion}/${url}`;
|
this.options = options;
|
||||||
|
this.resource = resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
let url = this.apiVersion;
|
||||||
|
if (this.options.accountScoped) {
|
||||||
|
const isInsideAccountScopedURLs = window.location.pathname.includes(
|
||||||
|
'/app/accounts'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isInsideAccountScopedURLs) {
|
||||||
|
const accountId = window.location.pathname.split('/')[3];
|
||||||
|
url = `${url}/accounts/${accountId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${url}/${this.resource}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get() {
|
get() {
|
||||||
|
|
9
app/javascript/dashboard/api/account.js
Normal file
9
app/javascript/dashboard/api/account.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class AccountAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AccountAPI();
|
|
@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
|
||||||
|
|
||||||
class Agents extends ApiClient {
|
class Agents extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('agents');
|
super('agents', { accountScoped: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import ApiClient from './ApiClient';
|
||||||
|
|
||||||
class CannedResponse extends ApiClient {
|
class CannedResponse extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('canned_responses');
|
super('canned_responses', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ searchKey }) {
|
get({ searchKey }) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
class FBChannel extends ApiClient {
|
class FBChannel extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('facebook_indicators');
|
super('facebook_indicators', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
markSeen({ inboxId, contactId }) {
|
markSeen({ inboxId, contactId }) {
|
||||||
|
@ -22,7 +22,7 @@ class FBChannel extends ApiClient {
|
||||||
|
|
||||||
create(params) {
|
create(params) {
|
||||||
return axios.post(
|
return axios.post(
|
||||||
`${this.apiVersion}/callbacks/register_facebook_page`,
|
`${this.url.replace(this.resource, '')}callbacks/register_facebook_page`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
9
app/javascript/dashboard/api/channel/twilioChannel.js
Normal file
9
app/javascript/dashboard/api/channel/twilioChannel.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
class TwilioChannel extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('channels/twilio_channel', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TwilioChannel();
|
|
@ -2,7 +2,7 @@ import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
class WebChannel extends ApiClient {
|
class WebChannel extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('widget/inboxes');
|
super('inboxes', { accountScoped: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
import endPoints from './endPoints';
|
import endPoints from './endPoints';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchFacebookPages(token) {
|
fetchFacebookPages(token, accountId) {
|
||||||
const urlData = endPoints('fetchFacebookPages');
|
const urlData = endPoints('fetchFacebookPages');
|
||||||
urlData.params.omniauth_token = token;
|
urlData.params.omniauth_token = token;
|
||||||
return axios.post(urlData.url, urlData.params);
|
return axios.post(urlData.url(accountId), urlData.params);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
|
||||||
|
|
||||||
class ContactAPI extends ApiClient {
|
class ContactAPI extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('contacts');
|
super('contacts', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
getConversations(contactId) {
|
getConversations(contactId) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
|
||||||
|
|
||||||
class ConversationApi extends ApiClient {
|
class ConversationApi extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('conversations');
|
super('conversations', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabels(conversationID) {
|
getLabels(conversationID) {
|
||||||
|
|
|
@ -28,23 +28,12 @@ const endPoints = {
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchFacebookPages: {
|
fetchFacebookPages: {
|
||||||
url: 'api/v1/callbacks/get_facebook_pages.json',
|
url(accountId) {
|
||||||
|
return `api/v1/accounts/${accountId}/callbacks/facebook_pages.json`;
|
||||||
|
},
|
||||||
params: { omniauth_token: '' },
|
params: { omniauth_token: '' },
|
||||||
},
|
},
|
||||||
|
|
||||||
reports: {
|
|
||||||
account(metric, from, to) {
|
|
||||||
return {
|
|
||||||
url: `/api/v1/reports/account?metric=${metric}&since=${from}&to=${to}`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
accountSummary(accountId, from, to) {
|
|
||||||
return {
|
|
||||||
url: `/api/v1/reports/${accountId}/account_summary?since=${from}&to=${to}`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
get() {
|
get() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,15 +3,16 @@ import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
class ConversationApi extends ApiClient {
|
class ConversationApi extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('conversations');
|
super('conversations', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ inboxId, status, assigneeType }) {
|
get({ inboxId, status, assigneeType, page }) {
|
||||||
return axios.get(this.url, {
|
return axios.get(this.url, {
|
||||||
params: {
|
params: {
|
||||||
inbox_id: inboxId,
|
inbox_id: inboxId,
|
||||||
status,
|
status,
|
||||||
assignee_type_id: assigneeType,
|
assignee_type: assigneeType,
|
||||||
|
page,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,21 +4,31 @@ import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
class MessageApi extends ApiClient {
|
class MessageApi extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('conversations');
|
super('conversations', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
create({ conversationId, message, private: isPrivate }) {
|
create({ conversationId, message, private: isPrivate }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/messages`, {
|
return axios.post(`${this.url}/${conversationId}/messages`, {
|
||||||
message,
|
content: message,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreviousMessages({ conversationId, before }) {
|
getPreviousMessages({ conversationId, before }) {
|
||||||
return axios.get(`${this.url}/${conversationId}`, {
|
return axios.get(`${this.url}/${conversationId}/messages`, {
|
||||||
params: { before },
|
params: { before },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendAttachment([conversationId, { file }]) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('attachments[]', file, file.name);
|
||||||
|
return axios({
|
||||||
|
method: 'post',
|
||||||
|
url: `${this.url}/${conversationId}/messages`,
|
||||||
|
data: formData,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new MessageApi();
|
export default new MessageApi();
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
|
||||||
|
|
||||||
class InboxMembers extends ApiClient {
|
class InboxMembers extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('inbox_members');
|
super('inbox_members', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
create({ inboxId, agentList }) {
|
create({ inboxId, agentList }) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
|
||||||
|
|
||||||
class Inboxes extends ApiClient {
|
class Inboxes extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('inboxes');
|
super('inboxes', { accountScoped: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
/* global axios */
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
import endPoints from './endPoints';
|
class ReportsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('reports', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
getAccountReports(metric, since, until) {
|
||||||
getAccountReports(metric, from, to) {
|
return axios.get(`${this.url}/account`, {
|
||||||
const { url } = endPoints('reports').account(metric, from, to);
|
params: { metric, since, until },
|
||||||
return axios.get(url);
|
});
|
||||||
},
|
}
|
||||||
getAccountSummary(accountId, from, to) {
|
|
||||||
const urlData = endPoints('reports').accountSummary(accountId, from, to);
|
getAccountSummary(accountId, since, until) {
|
||||||
return axios.get(urlData.url);
|
return axios.get(`${this.url}/${accountId}/account_summary`, {
|
||||||
},
|
params: { since, until },
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ReportsAPI();
|
||||||
|
|
15
app/javascript/dashboard/api/specs/channel/fbChannel.spec.js
Normal file
15
app/javascript/dashboard/api/specs/channel/fbChannel.spec.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import fbChannel from '../../channel/fbChannel';
|
||||||
|
import ApiClient from '../../ApiClient';
|
||||||
|
|
||||||
|
describe('#FBChannel', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(fbChannel).toBeInstanceOf(ApiClient);
|
||||||
|
expect(fbChannel).toHaveProperty('get');
|
||||||
|
expect(fbChannel).toHaveProperty('show');
|
||||||
|
expect(fbChannel).toHaveProperty('create');
|
||||||
|
expect(fbChannel).toHaveProperty('update');
|
||||||
|
expect(fbChannel).toHaveProperty('delete');
|
||||||
|
expect(fbChannel).toHaveProperty('markSeen');
|
||||||
|
expect(fbChannel).toHaveProperty('toggleTyping');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import userNotificationSettings from '../userNotificationSettings';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#AgentAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(userNotificationSettings).toBeInstanceOf(ApiClient);
|
||||||
|
expect(userNotificationSettings).toHaveProperty('get');
|
||||||
|
expect(userNotificationSettings).toHaveProperty('show');
|
||||||
|
expect(userNotificationSettings).toHaveProperty('create');
|
||||||
|
expect(userNotificationSettings).toHaveProperty('update');
|
||||||
|
expect(userNotificationSettings).toHaveProperty('delete');
|
||||||
|
});
|
||||||
|
});
|
14
app/javascript/dashboard/api/userNotificationSettings.js
Normal file
14
app/javascript/dashboard/api/userNotificationSettings.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class UserNotificationSettings extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('notification_settings', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
update(params) {
|
||||||
|
return axios.patch(`${this.url}`, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new UserNotificationSettings();
|
9
app/javascript/dashboard/api/webhooks.js
Normal file
9
app/javascript/dashboard/api/webhooks.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class WebHooks extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('webhooks', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WebHooks();
|
Binary file not shown.
BIN
app/javascript/dashboard/assets/images/channels/twilio.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/twilio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
app/javascript/dashboard/assets/images/channels/whatsapp.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/whatsapp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -0,0 +1,64 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<path style="fill:#4D4D4D;" d="M188.287,512c-41.473,0-75.213-33.74-75.213-75.213V246.75c0-4.142,3.358-7.5,7.5-7.5
|
||||||
|
s7.5,3.358,7.5,7.5v190.037c0,33.202,27.011,60.213,60.213,60.213c16.082,0,31.204-6.266,42.582-17.644
|
||||||
|
c11.37-11.37,17.631-26.488,17.631-42.569V75.213C248.5,33.74,282.24,0,323.713,0c20.088,0,38.978,7.826,53.189,22.037
|
||||||
|
c14.203,14.202,22.024,33.087,22.024,53.176V256c0,4.142-3.358,7.5-7.5,7.5s-7.5-3.358-7.5-7.5V75.213
|
||||||
|
c0-16.082-6.261-31.2-17.63-42.569C354.918,21.266,339.794,15,323.713,15C290.511,15,263.5,42.011,263.5,75.213v361.574
|
||||||
|
c0,20.088-7.822,38.973-22.024,53.176C227.265,504.174,208.376,512,188.287,512z"/>
|
||||||
|
<g>
|
||||||
|
<rect x="113.07" y="246.75" style="fill:#3B3B3B;" width="15" height="26.875"/>
|
||||||
|
<rect x="383.93" y="235.31" style="fill:#3B3B3B;" width="15" height="26.875"/>
|
||||||
|
</g>
|
||||||
|
<rect x="361.9" y="385" style="fill:#CCCCCC;" width="57.983" height="39.944"/>
|
||||||
|
<rect x="361.9" y="385" style="fill:#ADADAD;" width="57.983" height="22.19"/>
|
||||||
|
<path style="fill:#A6E2E3;" d="M432.802,298.678v86.977c0,3.616-2.932,6.548-6.548,6.548h-70.721c-3.617,0-6.548-2.932-6.548-6.548
|
||||||
|
v-87.746c0-23.439,19.239-42.39,42.803-41.899C414.709,256.486,432.802,275.751,432.802,298.678z"/>
|
||||||
|
<rect x="92.11" y="87.06" style="fill:#CCCCCC;" width="57.983" height="36.28"/>
|
||||||
|
<rect x="92.11" y="105.43" style="fill:#ADADAD;" width="57.983" height="17.907"/>
|
||||||
|
<path style="fill:#FFA638;" d="M163.015,126.345v86.977c0,22.927-18.093,42.191-41.015,42.668
|
||||||
|
c-23.564,0.49-42.803-18.461-42.803-41.899v-87.746c0-3.616,2.932-6.548,6.548-6.548l0,0h70.721l0,0
|
||||||
|
C160.083,119.797,163.015,122.729,163.015,126.345z"/>
|
||||||
|
<path style="fill:#7CCBCC;" d="M391.787,256.009c-5.066-0.105-9.93,0.693-14.447,2.236c0.396-0.081,0.781-0.166,1.142-0.257
|
||||||
|
c2.982-0.755,5.201-0.896,7.513-0.85c18.954,0.395,34.375,16.494,34.375,35.888v86.981c0,3.614-2.93,6.544-6.544,6.544H355.53
|
||||||
|
c-3.614,0-6.544-2.93-6.544-6.544l0,0v5.648c0,3.616,2.932,6.548,6.548,6.548h70.721c3.617,0,6.548-2.932,6.548-6.548v-86.977
|
||||||
|
C432.802,275.751,414.709,256.486,391.787,256.009z"/>
|
||||||
|
<path style="fill:#EB7100;" d="M79.527,217.153l-0.23-0.322c0.081,1.261,0.209,2.509,0.399,3.737
|
||||||
|
C79.586,219.444,79.527,218.305,79.527,217.153z"/>
|
||||||
|
<path style="fill:#ED8300;" d="M156.467,119.797h-11.188c2.613,0.858,4.502,3.314,4.502,6.215v90.372
|
||||||
|
c0,19.395-15.42,35.494-34.375,35.888c-0.251,0.005-0.503,0.008-0.753,0.008c-9.651,0-18.405-3.914-24.76-10.236
|
||||||
|
c7.856,8.766,19.342,14.212,32.106,13.947c22.922-0.477,41.015-19.741,41.015-42.668v-86.977
|
||||||
|
C163.015,122.728,160.083,119.797,156.467,119.797z"/>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3 KiB |
|
@ -3,8 +3,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-center {
|
.flex-center {
|
||||||
display: flex;
|
|
||||||
@include flex-align(center, middle);
|
@include flex-align(center, middle);
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-space-fix {
|
.bottom-space-fix {
|
||||||
|
@ -17,42 +17,43 @@
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
@include color-spinner();
|
@include color-spinner();
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: $space-medium;
|
|
||||||
height: $space-medium;
|
height: $space-medium;
|
||||||
padding: $zero $space-medium;
|
padding: $zero $space-medium;
|
||||||
|
position: relative;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
width: $space-medium;
|
||||||
|
|
||||||
&.message {
|
&.message {
|
||||||
padding: $space-normal;
|
@include elegent-shadow;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
margin-top: $space-slab;
|
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
border-radius: $space-large;
|
border-radius: $space-large;
|
||||||
@include elegent-shadow;
|
left: 0;
|
||||||
|
margin: $space-slab 0 auto;
|
||||||
|
padding: $space-normal;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
margin-top: -$space-slab;
|
|
||||||
margin-left: -$space-slab;
|
margin-left: -$space-slab;
|
||||||
|
margin-top: -$space-slab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
width: $space-normal;
|
|
||||||
height: $space-normal;
|
height: $space-normal;
|
||||||
|
|
||||||
&:before {
|
|
||||||
width: $space-normal;
|
width: $space-normal;
|
||||||
|
|
||||||
|
&::before {
|
||||||
height: $space-normal;
|
height: $space-normal;
|
||||||
margin-top: -$space-small;
|
margin-top: -$space-small;
|
||||||
|
width: $space-normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
input, textarea {
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,11 +35,11 @@ body {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@include margin($zero);
|
@include margin($zero);
|
||||||
@include padding($space-normal);
|
@include padding($space-normal);
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-box {
|
.content-box {
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
@include padding($space-normal);
|
@include padding($space-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -129,17 +129,16 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin scroll-on-hover() {
|
@mixin scroll-on-hover() {
|
||||||
transition: all .4s $ease-in-out-cubic;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@mixin horizontal-scroll() {
|
@mixin horizontal-scroll() {
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin elegent-shadow() {
|
@mixin elegent-shadow() {
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: $color-gray;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
@import 'widgets/conv-header';
|
@import 'widgets/conv-header';
|
||||||
@import 'widgets/conversation-card';
|
@import 'widgets/conversation-card';
|
||||||
@import 'widgets/conversation-view';
|
@import 'widgets/conversation-view';
|
||||||
@import 'widgets/emojiinput';
|
|
||||||
@import 'widgets/forms';
|
@import 'widgets/forms';
|
||||||
@import 'widgets/login';
|
@import 'widgets/login';
|
||||||
@import 'widgets/modal';
|
@import 'widgets/modal';
|
||||||
|
@ -25,6 +24,7 @@
|
||||||
|
|
||||||
@import 'views/settings/inbox';
|
@import 'views/settings/inbox';
|
||||||
@import 'views/settings/channel';
|
@import 'views/settings/channel';
|
||||||
|
@import 'views/settings/integrations';
|
||||||
@import 'views/signup';
|
@import 'views/signup';
|
||||||
|
|
||||||
@import 'plugins/multiselect';
|
@import 'plugins/multiselect';
|
||||||
|
|
|
@ -31,42 +31,39 @@
|
||||||
.wizard-box {
|
.wizard-box {
|
||||||
.item {
|
.item {
|
||||||
@include padding($space-normal $space-normal $space-normal $space-medium);
|
@include padding($space-normal $space-normal $space-normal $space-medium);
|
||||||
position: relative;
|
|
||||||
@include background-light;
|
@include background-light;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:before,
|
cursor: pointer;
|
||||||
&:after {
|
position: relative;
|
||||||
content: '';
|
|
||||||
position: absolute;
|
&::before,
|
||||||
width: 2px;
|
&::after {
|
||||||
height: 100%;
|
|
||||||
background: $color-border;
|
background: $color-border;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
top: $space-normal;
|
top: $space-normal;
|
||||||
|
width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
top: $zero;
|
|
||||||
height: $space-normal;
|
height: $space-normal;
|
||||||
|
top: $zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
&:before {
|
&::before {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
&:after {
|
&::after {
|
||||||
height: $zero;
|
height: $zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
// left: 1px;
|
|
||||||
// @include background-white;
|
|
||||||
// @include border-light;
|
|
||||||
// border-right: 0;
|
|
||||||
h3 {
|
h3 {
|
||||||
color: $color-woot;
|
color: $color-woot;
|
||||||
}
|
}
|
||||||
|
@ -78,7 +75,7 @@
|
||||||
|
|
||||||
&.over {
|
&.over {
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
background: $color-woot;
|
background: $color-woot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,17 +84,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
& + .item {
|
& + .item {
|
||||||
&:before {
|
&::before {
|
||||||
background: $color-woot;
|
background: $color-woot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: $font-size-default;
|
|
||||||
padding-left: $space-medium;
|
|
||||||
line-height: 1;
|
|
||||||
color: $color-body;
|
color: $color-body;
|
||||||
|
font-size: $font-size-default;
|
||||||
|
line-height: 1;
|
||||||
|
padding-left: $space-medium;
|
||||||
|
|
||||||
.completed {
|
.completed {
|
||||||
color: $success-color;
|
color: $success-color;
|
||||||
|
@ -105,25 +102,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: $font-size-small;
|
|
||||||
color: $color-light-gray;
|
color: $color-light-gray;
|
||||||
padding-left: $space-medium;
|
font-size: $font-size-small;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding-left: $space-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step {
|
.step {
|
||||||
position: absolute;
|
|
||||||
left: $space-normal;
|
|
||||||
top: $space-normal;
|
|
||||||
font-size: $font-size-small;
|
|
||||||
font-weight: $font-weight-medium;
|
|
||||||
background: $color-border;
|
background: $color-border;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
width: $space-normal;
|
color: $color-white;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
height: $space-normal;
|
height: $space-normal;
|
||||||
text-align: center;
|
left: $space-normal;
|
||||||
line-height: $space-normal;
|
line-height: $space-normal;
|
||||||
color: #fff;
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
top: $space-normal;
|
||||||
|
width: $space-normal;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
|
@ -141,10 +138,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.inoboxes-list {
|
.inoboxes-list {
|
||||||
// @include margin(auto);
|
|
||||||
// @include background-white;
|
|
||||||
// @include border-light;
|
|
||||||
// width: 50%;
|
|
||||||
|
|
||||||
.inbox-item {
|
.inbox-item {
|
||||||
@include margin($space-normal);
|
@include margin($space-normal);
|
||||||
|
@ -152,16 +145,18 @@
|
||||||
@include flex-shrink;
|
@include flex-shrink;
|
||||||
@include padding($space-normal $space-normal);
|
@include padding($space-normal $space-normal);
|
||||||
@include border-light-bottom();
|
@include border-light-bottom();
|
||||||
flex-direction: column;
|
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 20%;
|
flex-direction: column;
|
||||||
float: left;
|
float: left;
|
||||||
min-height: 10rem;
|
min-height: 10rem;
|
||||||
|
width: 20%;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: $zero;
|
|
||||||
@include border-nil;
|
@include border-nil;
|
||||||
|
|
||||||
|
margin-bottom: $zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -174,8 +169,8 @@
|
||||||
|
|
||||||
.switch {
|
.switch {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-right: $space-normal;
|
|
||||||
margin-bottom: $zero;
|
margin-bottom: $zero;
|
||||||
|
margin-right: $space-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item--details {
|
.item--details {
|
||||||
|
@ -187,15 +182,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.item--sub {
|
.item--sub {
|
||||||
margin-bottom: 0;
|
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: $font-size-small;
|
|
||||||
color: $medium-gray;
|
color: $medium-gray;
|
||||||
|
font-size: $font-size-small;
|
||||||
opacity: .7;
|
opacity: .7;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s;
|
transition: opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s;
|
||||||
|
@ -204,18 +199,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings--content {
|
.settings--content {
|
||||||
@include margin($space-small $space-medium);
|
@include margin($space-small $space-larger);
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
max-height: $space-mega;
|
|
||||||
overflow: scroll;
|
|
||||||
white-space: nowrap;
|
|
||||||
@include padding($space-one);
|
@include padding($space-one);
|
||||||
|
|
||||||
background: $color-background;
|
background: $color-background;
|
||||||
|
max-height: $space-mega;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
@ -225,8 +221,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-init {
|
.login-init {
|
||||||
text-align: center;
|
|
||||||
padding-top: 30%;
|
padding-top: 30%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@include padding($space-medium);
|
@include padding($space-medium);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue