Merge branch 'develop' into feature/conversation-refactor

This commit is contained in:
Pranav Raj Sreepuram 2020-05-01 13:02:47 +05:30
commit b07bdfda1d
764 changed files with 25854 additions and 5499 deletions

View file

@ -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

View file

@ -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"

View file

@ -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=

View file

@ -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
View file

@ -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
View file

@ -0,0 +1 @@
12.16.1

View file

@ -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/**/*'

View file

@ -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:

View file

@ -1 +1 @@
2.6.5 2.7.0

View file

@ -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
View file

@ -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

View file

@ -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

View file

@ -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):

View 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

View file

@ -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

View file

@ -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(' ')

View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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]

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View 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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View 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

View file

@ -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

View 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

View 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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
end end
def listeners def listeners
[ActionCableListener.instance] [ActionCableListener.instance, AgentBotListener.instance]
end end
end end

View file

@ -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

View file

@ -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

View 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

View file

@ -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>

View file

@ -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() {

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class AccountAPI extends ApiClient {
constructor() {
super('', { accountScoped: true });
}
}
export default new AccountAPI();

View file

@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
class Agents extends ApiClient { class Agents extends ApiClient {
constructor() { constructor() {
super('agents'); super('agents', { accountScoped: true });
} }
} }

View file

@ -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 }) {

View file

@ -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
); );
} }

View file

@ -0,0 +1,9 @@
import ApiClient from '../ApiClient';
class TwilioChannel extends ApiClient {
constructor() {
super('channels/twilio_channel', { accountScoped: true });
}
}
export default new TwilioChannel();

View file

@ -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 });
} }
} }

View file

@ -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);
}, },
}; };

View file

@ -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) {

View file

@ -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) {

View file

@ -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 {

View file

@ -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,
}, },
}); });
} }

View file

@ -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();

View file

@ -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 }) {

View file

@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
class Inboxes extends ApiClient { class Inboxes extends ApiClient {
constructor() { constructor() {
super('inboxes'); super('inboxes', { accountScoped: true });
} }
} }

View file

@ -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();

View 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');
});
});

View file

@ -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');
});
});

View 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();

View 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.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -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

View file

@ -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;
} }

View file

@ -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);
} }

View file

@ -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() {

View file

@ -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;
} }

View file

@ -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';

View file

@ -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