Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7015609577 | ||
|
|
58ad0e6fd6 | ||
|
|
b8ea8e5b5e | ||
|
|
eada8c9af2 | ||
|
|
6de964a0bd | ||
|
|
9f9dd09985 | ||
|
|
a9849e402e | ||
|
|
a2ffcc4502 | ||
|
|
c831bee0e3 | ||
|
|
3b53ca157c | ||
|
|
de0702b679 | ||
|
|
cd5c2c51d9 | ||
|
|
cebd34053b | ||
|
|
ed25435ac2 | ||
|
|
0c65db925d | ||
|
|
590001cca2 | ||
|
|
e669f6766f | ||
|
|
99abbb8158 | ||
|
|
5c30bc3e2b | ||
|
|
5799b9fa26 | ||
|
|
b33701a666 | ||
|
|
6c3e2a0bd3 | ||
|
|
a494afd0e7 | ||
|
|
6998e9aa2d | ||
|
|
6bfa551c85 | ||
|
|
7c21cf2255 | ||
|
|
5749d25ff8 | ||
|
|
c1d68cc8ae | ||
|
|
5237e51f2c | ||
|
|
2c8bf00d38 | ||
|
|
3d3f0ff6ee | ||
|
|
2c3e37b157 | ||
|
|
a35269d275 | ||
|
|
68e697c379 | ||
|
|
0e0632be22 | ||
|
|
1c4afb10df | ||
|
|
ec2dc1b61b | ||
|
|
b9e85a628b | ||
|
|
5d3cce12d5 | ||
|
|
8c192559fe | ||
|
|
700721ea6d | ||
|
|
c54aae21ff | ||
|
|
1c6a539c0a | ||
|
|
3e99088fe3 | ||
|
|
ec9903ae82 | ||
|
|
219a8c4bc5 | ||
|
|
271ca53c58 | ||
|
|
f874925f0e | ||
|
|
bd7aeba484 | ||
|
|
40d0b2faf3 | ||
|
|
30244f79a6 | ||
|
|
1d94e65ca8 | ||
|
|
be9a055a3f | ||
|
|
e6bcf24864 | ||
|
|
1083a24a26 | ||
|
|
a1563917ba | ||
|
|
57abdc4d5f | ||
|
|
49ac4a4400 | ||
|
|
cc244833d0 | ||
|
|
d55a48037a | ||
|
|
6f6bb91823 | ||
|
|
a8f6eebd66 | ||
|
|
bba2750975 | ||
|
|
6129edce08 | ||
|
|
ed22e7cad4 | ||
|
|
edd0e2329f | ||
|
|
1761bec615 | ||
|
|
15aaa8883c | ||
|
|
8b7f6c691a | ||
|
|
1cb2226dbc | ||
|
|
1f4685ae4b | ||
|
|
54bdb2957f | ||
|
|
4f51a46c2b | ||
|
|
0c24df96a8 | ||
|
|
dddab0bbce | ||
|
|
c504067e2b | ||
|
|
b59e73b10b | ||
|
|
aaadd61e09 | ||
|
|
dcbbb09ebd | ||
|
|
fa2db5a729 | ||
|
|
b1b0268705 | ||
|
|
794a56d4cc | ||
|
|
6ad5a7452c | ||
|
|
0c3e8b6dbd | ||
|
|
a0ffefad71 | ||
|
|
799e0ed4f5 | ||
|
|
928766f07b | ||
|
|
b474e39feb | ||
|
|
3abcadb5cb | ||
|
|
a14f4ede87 | ||
|
|
2396b59f11 | ||
|
|
946a09928e | ||
|
|
2c381d726d | ||
|
|
11cbc4d952 | ||
|
|
d76455e840 | ||
|
|
aa5d01b572 | ||
|
|
22d1c8baf2 | ||
|
|
ccd0dc39ad | ||
|
|
84b96526db | ||
|
|
f2e2a0b7ed | ||
|
|
328edd24de | ||
|
|
26a3150fd8 | ||
|
|
571fefd7cd | ||
|
|
b74261205d | ||
|
|
f79c52b8c3 | ||
|
|
0a38632f14 | ||
|
|
671c5c931f | ||
|
|
9c4ce9af35 | ||
|
|
2821777e93 | ||
|
|
41314157c2 | ||
|
|
c80289e661 | ||
|
|
39c4fa111a | ||
|
|
4759730022 | ||
|
|
8de4ce0037 | ||
|
|
7245e66b31 | ||
|
|
772d4ae726 | ||
|
|
118c1458ab | ||
|
|
dace97b7b0 | ||
|
|
7b77bab18e | ||
|
|
7fc575a474 | ||
|
|
9acac38635 | ||
|
|
2ebd38c3b7 | ||
|
|
b866c54ad5 | ||
|
|
ff1625708e | ||
|
|
a03ed4eea0 | ||
|
|
ad83d1bb71 | ||
|
|
a60a33679f | ||
|
|
af1d8c0ee5 | ||
|
|
2ddd508aee | ||
|
|
8b841596a5 | ||
|
|
afdf1c70c1 | ||
|
|
6fdd4a2996 | ||
|
|
06d8916341 | ||
|
|
6563cccf34 | ||
|
|
5319af3dfc | ||
|
|
1ff9939a80 | ||
|
|
fdcc322660 | ||
|
|
75329e5de1 | ||
|
|
588f49cc34 | ||
|
|
65f3e83afd | ||
|
|
f94abaef5f | ||
|
|
9a8cbee470 | ||
|
|
78646b44b8 | ||
|
|
0a08e1ffc0 | ||
|
|
5853531d9a | ||
|
|
d13c4ff8c5 | ||
|
|
69b0434bb7 | ||
|
|
1d5150e3a7 | ||
|
|
7dd42f3fd7 | ||
|
|
4a23a393f7 | ||
|
|
8d6b3efa2e | ||
|
|
a27ccc5866 | ||
|
|
a9ca76d9df | ||
|
|
21e8a41206 | ||
|
|
1257f86f22 | ||
|
|
09e3413d10 | ||
|
|
6515b69560 | ||
|
|
5debe9e8ee | ||
|
|
7cd3cef0a2 | ||
|
|
eb80324eaa | ||
|
|
20b96c7c60 | ||
|
|
6a9c885926 | ||
|
|
ed317c5bb3 | ||
|
|
319412f6dc | ||
|
|
0e4196c91a | ||
|
|
70d41ffcdd | ||
|
|
e09941db1c | ||
|
|
37bf4b7dd8 | ||
|
|
9e052fd5b2 | ||
|
|
dfcc33cbdd | ||
|
|
0f148f7700 | ||
|
|
acb39cbc8f | ||
|
|
d492a65c24 | ||
|
|
3d807684bb | ||
|
|
21d2b4d918 | ||
|
|
1f59205f20 | ||
|
|
a065165bcb | ||
|
|
4907489ea8 | ||
|
|
4d668d8db3 | ||
|
|
8daf1fe033 | ||
|
|
29f54c1f26 | ||
|
|
1a4faab381 | ||
|
|
c3314dd186 | ||
|
|
bdc4ecffc1 | ||
|
|
462214aced | ||
|
|
c523a953f7 | ||
|
|
c7482696d4 | ||
|
|
2035d24cb8 | ||
|
|
9c257578b0 | ||
|
|
b58ca21f0b | ||
|
|
edec74c389 | ||
|
|
f934a5eb04 | ||
|
|
f477c5281f | ||
|
|
6265c1c907 | ||
|
|
e834b545ef | ||
|
|
c37966744f | ||
|
|
ab54d9c629 | ||
|
|
7e960b7c72 | ||
|
|
92c14fa87d | ||
|
|
9b01b82cc7 | ||
|
|
0475060245 | ||
|
|
faf104c1fe | ||
|
|
d88e3e3596 | ||
|
|
9f3f238cb5 | ||
|
|
5b3339cd33 | ||
|
|
bfa00cc12d | ||
|
|
e46aa1aa64 | ||
|
|
2890339734 | ||
|
|
915366c472 | ||
|
|
f2b5e328bb | ||
|
|
672e5874fb | ||
|
|
8f30abb98b | ||
|
|
223385d134 | ||
|
|
7662fdce47 | ||
|
|
a47ca9cf4b | ||
|
|
b44f9b792b | ||
|
|
359c3c8ccb | ||
|
|
d7da6fbfb5 | ||
|
|
7664006625 | ||
|
|
6e1493501a | ||
|
|
7e0937f3ed | ||
|
|
d955d8e7dc | ||
|
|
4d45ac3bfc | ||
|
|
ecdf977de7 | ||
|
|
6b6df7a70d | ||
|
|
a7ca55c080 | ||
|
|
a0886d37bc |
@@ -0,0 +1,3 @@
|
||||
---
|
||||
ignore:
|
||||
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
|
||||
@@ -7,7 +7,7 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: circleci/ruby:2.7.3-node-browsers
|
||||
- image: cimg/ruby:3.0.2-node
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
@@ -40,20 +40,20 @@ jobs:
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-bundle-{{ checksum "Gemfile.lock" }}
|
||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
||||
- chatwoot-bundle
|
||||
|
||||
- run: bundle install --frozen --path ~/.bundle
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.bundle
|
||||
key: chatwoot-bundle-{{ checksum "Gemfile.lock" }}
|
||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
|
||||
# Only necessary if app uses webpacker or yarn in some other way
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-yarn-{{ checksum "yarn.lock" }}
|
||||
- chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
- chatwoot-yarn-
|
||||
|
||||
- run:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
# Store yarn / webpacker cache
|
||||
- save_cache:
|
||||
key: chatwoot-yarn-{{ checksum "yarn.lock" }}
|
||||
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ plugins:
|
||||
checks:
|
||||
similar-code:
|
||||
enabled: false
|
||||
method-count:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 25
|
||||
exclude_patterns:
|
||||
- "spec/"
|
||||
- "**/specs/"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# pre-build stage
|
||||
ARG VARIANT=2.7
|
||||
ARG VARIANT=3
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||
|
||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||
|
||||
@@ -10,8 +10,8 @@ services:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
args:
|
||||
# Update 'VARIANT' to pick a Ruby version: 2, 2.7, 2.6, 2.5
|
||||
VARIANT: 2.7
|
||||
# Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
|
||||
VARIANT: 3
|
||||
# [Choice] Install Node.js
|
||||
INSTALL_NODE: "true"
|
||||
NODE_VERSION: "lts/*"
|
||||
|
||||
+32
-4
@@ -41,7 +41,7 @@ RAILS_MAX_THREADS=5
|
||||
|
||||
# The email from which all outgoing emails are sent
|
||||
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
||||
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||
MAILER_SENDER_EMAIL="Chatwoot <accounts@chatwoot.com>"
|
||||
|
||||
|
||||
#SMTP domain key is set up for HELO checking
|
||||
@@ -85,8 +85,6 @@ AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_REGION=
|
||||
|
||||
# Sentry
|
||||
SENTRY_DSN=
|
||||
|
||||
# Log settings
|
||||
# Disable if you want to write logs to a file
|
||||
@@ -102,6 +100,9 @@ FB_VERIFY_TOKEN=
|
||||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
||||
IG_VERIFY_TOKEN=
|
||||
|
||||
# Twitter
|
||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||
TWITTER_APP_ID=
|
||||
@@ -115,7 +116,7 @@ SLACK_CLIENT_SECRET=
|
||||
|
||||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
|
||||
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
||||
ANDROID_BUNDLE_ID=com.chatwoot.app
|
||||
|
||||
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
|
||||
@@ -139,6 +140,25 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
USE_INBOX_AVATAR_FOR_BOT=true
|
||||
|
||||
|
||||
### APM and Error Monitoring configurations
|
||||
## Sentry
|
||||
# SENTRY_DSN=
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
# SCOUT_NAME=YOURAPPNAME (Production)
|
||||
# SCOUT_MONITOR=true
|
||||
|
||||
## NewRelic
|
||||
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
|
||||
# NEW_RELIC_LICENSE_KEY=
|
||||
|
||||
## Datadog
|
||||
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
||||
# DD_TRACE_AGENT_URL=
|
||||
|
||||
|
||||
|
||||
## IP look up configuration
|
||||
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
|
||||
@@ -147,6 +167,10 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||
# maxmindb api key to use geoip2 service
|
||||
# IP_LOOKUP_API_KEY=
|
||||
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=true
|
||||
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
## setting this value to true will disable the frontend dashboard endpoints
|
||||
@@ -157,3 +181,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
||||
# LETTER_OPENER=true
|
||||
# meant to be used in github codespaces
|
||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||
|
||||
# If you want to use official mobile app,
|
||||
# the notifications would be relayed via a Chatwoot server
|
||||
ENABLE_PUSH_RELAY_SERVER=true
|
||||
|
||||
+37
-5
@@ -13,6 +13,7 @@ Metrics/ClassLength:
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/mailers/conversation_reply_mailer.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -24,12 +25,13 @@ Style/FrozenStringLiteralComment:
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
- 'app/dispatchers/dispatcher.rb'
|
||||
Style/GlobalVars:
|
||||
Exclude:
|
||||
- 'config/initializers/redis.rb'
|
||||
- 'config/initializers/01_redis.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'lib/redis/alfred.rb'
|
||||
- 'lib/global_config.rb'
|
||||
Style/ClassVars:
|
||||
@@ -38,6 +40,14 @@ Style/ClassVars:
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'app/drops/base_drop.rb'
|
||||
Lint/SymbolConversion:
|
||||
Enabled: false
|
||||
Lint/EmptyBlock:
|
||||
Exclude:
|
||||
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
|
||||
Lint/OrAssignmentToConstant:
|
||||
Exclude:
|
||||
- 'lib/redis/config.rb'
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
@@ -55,6 +65,11 @@ Rails/ApplicationController:
|
||||
- 'app/controllers/widgets_controller.rb'
|
||||
- 'app/controllers/platform_controller.rb'
|
||||
- 'app/controllers/public_controller.rb'
|
||||
- 'app/controllers/survey/responses_controller.rb'
|
||||
Rails/EnvironmentVariableAccess:
|
||||
Enabled: false
|
||||
Rails/TimeZoneAssignment:
|
||||
Enabled: false
|
||||
Style/ClassAndModuleChildren:
|
||||
EnforcedStyle: compact
|
||||
Exclude:
|
||||
@@ -64,6 +79,10 @@ RSpec/NestedGroups:
|
||||
Max: 4
|
||||
RSpec/MessageSpies:
|
||||
Enabled: false
|
||||
RSpec/StubbedMock:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
@@ -77,10 +96,11 @@ Style/GuardClause:
|
||||
- 'app/models/message.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Metrics/AbcSize:
|
||||
Exclude:
|
||||
Exclude:
|
||||
- 'app/controllers/concerns/auth_helper.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
Exclude:
|
||||
@@ -106,19 +126,24 @@ Rails/BulkChangeTable:
|
||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/models/integrations/hook.rb'
|
||||
Rails/RenderInline:
|
||||
Exclude:
|
||||
- 'app/controllers/swagger_controller.rb'
|
||||
Performance/CollectionLiteralInLoop:
|
||||
Exclude:
|
||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
# we should bring this down
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 12
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
@@ -133,4 +158,11 @@ AllCops:
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- 'db/migrate/20200225162150_init_schema.rb'
|
||||
- 'config/initializers/azure_storage_service_patch.rb'
|
||||
- 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.rb'
|
||||
- 'db/migrate/20210611180221_add_service_name_to_active_storage_blobs.active_storage.rb'
|
||||
- db/migrate/20200309213132_add_account_id_to_agent_bot_inboxes.rb
|
||||
- db/migrate/20200331095710_add_identifier_to_contact.rb
|
||||
- db/migrate/20200429082655_add_medium_to_twilio_sms.rb
|
||||
- db/migrate/20200503151130_add_account_feature_flag.rb
|
||||
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
|
||||
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.7.3
|
||||
3.0.2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '2.7.3'
|
||||
ruby '3.0.2'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
@@ -33,19 +33,22 @@ gem 'liquid'
|
||||
gem 'commonmarker'
|
||||
# Validate Data against JSON Schema
|
||||
gem 'json_schemer'
|
||||
# Rack middleware for blocking & throttling abusive requests
|
||||
gem 'rack-attack'
|
||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||
gem 'down', '~> 5.0'
|
||||
|
||||
##-- for active storage --##
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'mini_magick'
|
||||
gem 'image_processing'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
gem 'pg'
|
||||
gem 'redis'
|
||||
gem 'redis-namespace'
|
||||
gem 'redis-rack-cache'
|
||||
# super fast record imports in bulk
|
||||
gem 'activerecord-import'
|
||||
|
||||
@@ -53,14 +56,13 @@ gem 'activerecord-import'
|
||||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'rack-timeout'
|
||||
gem 'webpacker', '~> 5.x'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
##--- gems for authentication & authorization ---##
|
||||
gem 'devise'
|
||||
gem 'devise-secure_password', '~> 2.0'
|
||||
gem 'devise-secure_password', '~> 2.0', git: 'https://github.com/chatwoot/devise-secure_password'
|
||||
gem 'devise_token_auth'
|
||||
# authorization
|
||||
gem 'jwt'
|
||||
@@ -75,7 +77,7 @@ gem 'wisper', '2.0.0'
|
||||
##--- gems for channels ---##
|
||||
# TODO: bump up gem to 2.0
|
||||
gem 'facebook-messenger'
|
||||
gem 'telegram-bot-ruby'
|
||||
gem 'line-bot-api'
|
||||
gem 'twilio-ruby', '~> 5.32.0'
|
||||
# twitty will handle subscription of twitter account events
|
||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||
@@ -90,8 +92,14 @@ gem 'google-cloud-dialogflow'
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
gem 'brakeman'
|
||||
|
||||
##-- apm and error monitoring ---#
|
||||
gem 'ddtrace'
|
||||
gem 'newrelic_rpm'
|
||||
gem 'scout_apm'
|
||||
gem 'sentry-raven'
|
||||
gem 'sentry-rails'
|
||||
gem 'sentry-ruby'
|
||||
gem 'sentry-sidekiq'
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
@@ -113,6 +121,11 @@ gem 'hairtrigger'
|
||||
|
||||
gem 'procore-sift'
|
||||
|
||||
group :production, :staging do
|
||||
# we dont want request timing out in development while using byebug
|
||||
gem 'rack-timeout'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'annotate'
|
||||
gem 'bullet'
|
||||
@@ -120,7 +133,7 @@ group :development do
|
||||
gem 'web-console'
|
||||
|
||||
# used in swagger build
|
||||
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: '131b11294fd6af9c428171f38516e6222a58c874'
|
||||
gem 'json_refs'
|
||||
|
||||
# When we want to squash migrations
|
||||
gem 'squasher'
|
||||
@@ -134,19 +147,24 @@ group :test do
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
# TODO: is this needed ?
|
||||
# errors thrown by devise password gem
|
||||
gem 'flay'
|
||||
gem 'rspec'
|
||||
# for error thrown by devise password gem
|
||||
gem 'active_record_query_trace'
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'listen'
|
||||
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec-rails', '~> 4.0.0.beta2'
|
||||
gem 'rspec-rails', '~> 5.0.0'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
gem 'rubocop-rspec', require: false
|
||||
gem 'scss_lint', require: false
|
||||
gem 'seed_dump'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'simplecov', '0.17.1', require: false
|
||||
|
||||
+320
-286
@@ -1,80 +1,78 @@
|
||||
GIT
|
||||
remote: https://github.com/sds/mock_redis
|
||||
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
ref: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
remote: https://github.com/chatwoot/devise-secure_password
|
||||
revision: de11e8765654b8242d42101ee9c8ffc8126f7975
|
||||
specs:
|
||||
mock_redis (0.22.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/tzmfreedom/json_refs
|
||||
revision: 131b11294fd6af9c428171f38516e6222a58c874
|
||||
ref: 131b11294fd6af9c428171f38516e6222a58c874
|
||||
specs:
|
||||
json_refs (0.1.6)
|
||||
hana
|
||||
devise-secure_password (2.0.1)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
railties (>= 5.0.0, < 7.0.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actioncable (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actionmailbox (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activestorage (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
actionmailer (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
actionview (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
actionpack (6.1.4.1)
|
||||
actionview (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actiontext (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activestorage (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
actionview (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
activerecord (6.0.3.7)
|
||||
activemodel (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
activerecord-import (1.0.7)
|
||||
activemodel (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
activerecord (6.1.4.1)
|
||||
activemodel (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
activerecord-import (1.2.0)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
marcel (~> 1.0.0)
|
||||
activesupport (6.0.3.7)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.2, >= 2.2.2)
|
||||
acts-as-taggable-on (6.5.0)
|
||||
activerecord (>= 5.0, < 6.1)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
acts-as-taggable-on (8.1.0)
|
||||
activerecord (>= 5.0, < 6.2)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.16.0)
|
||||
@@ -90,28 +88,24 @@ GEM
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.1)
|
||||
ast (2.4.2)
|
||||
attr_extras (6.2.4)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.360.0)
|
||||
aws-sdk-core (3.105.0)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.513.0)
|
||||
aws-sdk-core (3.121.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.37.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sdk-kms (1.49.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.79.1)
|
||||
aws-sdk-core (~> 3, >= 3.104.3)
|
||||
aws-sdk-s3 (1.103.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.2)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axiom-types (0.1.1)
|
||||
descendants_tracker (~> 0.0.4)
|
||||
ice_nine (~> 0.11.0)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
azure-storage-blob (2.0.1)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
@@ -120,95 +114,100 @@ GEM
|
||||
faraday_middleware (~> 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
barnes (0.0.8)
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.4.8)
|
||||
bootsnap (1.9.1)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.9.0)
|
||||
brakeman (5.1.1)
|
||||
browser (5.3.1)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
bullet (6.1.5)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
bundler-audit
|
||||
bundler-audit (0.7.0.1)
|
||||
bundler-audit (0.9.0.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (>= 0.18, < 2)
|
||||
thor (~> 1.0)
|
||||
byebug (11.1.3)
|
||||
coderay (1.1.3)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
commonmarker (0.21.1)
|
||||
ruby-enum (~> 0.5)
|
||||
concurrent-ruby (1.1.8)
|
||||
connection_pool (2.2.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
commonmarker (0.23.2)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.8.0)
|
||||
cypress-on-rails (1.11.0)
|
||||
rack
|
||||
database_cleaner (1.8.5)
|
||||
database_cleaner (2.0.1)
|
||||
database_cleaner-active_record (~> 2.0.0)
|
||||
database_cleaner-active_record (2.0.1)
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
ddtrace (0.53.0)
|
||||
ffi (~> 1.0)
|
||||
msgpack
|
||||
declarative (0.0.20)
|
||||
declarative-option (0.1.0)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (4.8.0)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-secure_password (2.0.1)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
railties (>= 5.0.0, < 7.0.0)
|
||||
devise_token_auth (1.1.4)
|
||||
devise_token_auth (1.2.0)
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
rails (>= 4.2.0, < 6.1)
|
||||
sprockets (= 3.7.2)
|
||||
rails (>= 4.2.0, < 6.2)
|
||||
diff-lcs (1.4.4)
|
||||
digest-crc (0.6.1)
|
||||
rake (~> 13.0)
|
||||
docile (1.3.2)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
ecma-re-validator (0.2.1)
|
||||
regexp_parser (~> 1.2)
|
||||
equalizer (0.0.11)
|
||||
down (5.2.4)
|
||||
addressable (~> 2.8)
|
||||
ecma-re-validator (0.3.0)
|
||||
regexp_parser (~> 2.0)
|
||||
erubi (1.10.0)
|
||||
et-orbi (1.2.4)
|
||||
erubis (2.7.0)
|
||||
et-orbi (1.2.5)
|
||||
tzinfo
|
||||
execjs (2.7.0)
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
rack (>= 1.4.5)
|
||||
factory_bot (6.1.0)
|
||||
factory_bot (6.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.1.0)
|
||||
factory_bot (~> 6.1.0)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (2.13.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday (~> 1.0)
|
||||
fcm (1.0.2)
|
||||
faraday (~> 1.0.0)
|
||||
ffi (1.15.0)
|
||||
fcm (1.0.3)
|
||||
faraday (~> 1)
|
||||
ffi (1.15.4)
|
||||
flag_shih_tzu (0.3.23)
|
||||
flay (2.12.1)
|
||||
erubis (~> 2.7.0)
|
||||
path_expander (~> 1.0)
|
||||
ruby_parser (~> 3.0)
|
||||
sexp_processor (~> 4.0)
|
||||
foreman (0.87.2)
|
||||
fugit (1.4.1)
|
||||
fugit (1.5.2)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.3.4)
|
||||
@@ -217,18 +216,23 @@ GEM
|
||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.6.3)
|
||||
gli (2.19.2)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.43.0)
|
||||
geocoder (1.7.0)
|
||||
gli (2.20.1)
|
||||
globalid (0.5.2)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.4.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.8.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
@@ -240,35 +244,44 @@ GEM
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.1.0)
|
||||
google-cloud-storage (1.28.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.34.1)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-protobuf (3.15.8)
|
||||
googleapis-common-protos (1.3.11)
|
||||
google-protobuf (3.18.1)
|
||||
google-protobuf (3.18.1-universal-darwin)
|
||||
google-protobuf (3.18.1-x86_64-linux)
|
||||
googleapis-common-protos (1.3.12)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.0.6)
|
||||
googleapis-common-protos-types (1.2.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.16.2)
|
||||
googleauth (0.17.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.14)
|
||||
signet (~> 0.15)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.37.1)
|
||||
google-protobuf (~> 3.15)
|
||||
grpc (1.41.0)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.0)
|
||||
hairtrigger (0.2.23)
|
||||
grpc (1.41.0-universal-darwin)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.41.0-x86_64-linux)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (0.2.24)
|
||||
activerecord (>= 5.0, < 7)
|
||||
ruby2ruby (~> 2.4)
|
||||
ruby_parser (~> 3.10)
|
||||
@@ -277,30 +290,33 @@ GEM
|
||||
hashie (4.1.0)
|
||||
hkdf (0.3.0)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.3)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
httparty (0.18.1)
|
||||
httparty (0.20.0)
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
inflecto (0.0.2)
|
||||
jbuilder (2.10.0)
|
||||
image_processing (1.12.1)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
jbuilder (2.11.2)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.4.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.3.1)
|
||||
json_schemer (0.2.16)
|
||||
ecma-re-validator (~> 0.2)
|
||||
json (2.5.1)
|
||||
json_refs (0.1.6)
|
||||
hana
|
||||
json_schemer (0.2.18)
|
||||
ecma-re-validator (~> 0.3)
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 1.5)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.2.3)
|
||||
jwt (2.3.0)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
@@ -321,113 +337,119 @@ GEM
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
liquid (4.0.3)
|
||||
listen (3.3.3)
|
||||
line-bot-api (1.22.0)
|
||||
liquid (5.1.0)
|
||||
listen (3.7.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.9.1)
|
||||
loofah (2.12.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (1.0.1)
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0225)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.1.0)
|
||||
mini_portile2 (2.5.1)
|
||||
mime-types-data (3.2021.0901)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.5.3)
|
||||
minitest (5.14.4)
|
||||
mock_redis (0.29.0)
|
||||
ruby2_keywords
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.3.3)
|
||||
msgpack (1.4.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
net-http-persistent (4.0.0)
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.7)
|
||||
nokogiri (1.11.6)
|
||||
newrelic_rpm (8.0.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.11.7)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.1)
|
||||
parallel (1.19.2)
|
||||
parser (2.7.1.4)
|
||||
parallel (1.21.0)
|
||||
parser (3.0.2.0)
|
||||
ast (~> 2.4.1)
|
||||
path_expander (1.1.0)
|
||||
pg (1.2.3)
|
||||
procore-sift (0.15.0)
|
||||
procore-sift (0.16.0)
|
||||
rails (> 4.2.0)
|
||||
pry (0.13.1)
|
||||
pry (0.14.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (4.3.8)
|
||||
puma (5.5.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
pundit (2.1.1)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.5.2)
|
||||
rack (2.2.3)
|
||||
rack-cache (1.12.0)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.5.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-proxy (0.6.5)
|
||||
rack-proxy (0.7.0)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
rails (6.0.3.7)
|
||||
actioncable (= 6.0.3.7)
|
||||
actionmailbox (= 6.0.3.7)
|
||||
actionmailer (= 6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
actiontext (= 6.0.3.7)
|
||||
actionview (= 6.0.3.7)
|
||||
activejob (= 6.0.3.7)
|
||||
activemodel (= 6.0.3.7)
|
||||
activerecord (= 6.0.3.7)
|
||||
activestorage (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.3.7)
|
||||
rails (6.1.4.1)
|
||||
actioncable (= 6.1.4.1)
|
||||
actionmailbox (= 6.1.4.1)
|
||||
actionmailer (= 6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
actiontext (= 6.1.4.1)
|
||||
actionview (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activemodel (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activestorage (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
loofah (~> 2.3)
|
||||
railties (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
activesupport (= 6.0.3.7)
|
||||
railties (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.3)
|
||||
rb-fsevent (0.10.4)
|
||||
rake (13.0.6)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.2.1)
|
||||
redis-namespace (1.8.0)
|
||||
redis (4.4.0)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
redis-rack-cache (2.2.1)
|
||||
rack-cache (>= 1.10, < 2)
|
||||
redis-store (>= 1.6, < 2)
|
||||
redis-store (1.9.0)
|
||||
redis (>= 4, < 5)
|
||||
regexp_parser (1.7.1)
|
||||
representable (3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
representable (3.1.1)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
responders (3.0.1)
|
||||
actionpack (>= 5.0)
|
||||
@@ -439,57 +461,56 @@ GEM
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
rspec (3.10.0)
|
||||
rspec-core (~> 3.10.0)
|
||||
rspec-expectations (~> 3.10.0)
|
||||
rspec-mocks (~> 3.10.0)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-mocks (3.10.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-rails (4.0.1)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
railties (>= 4.2)
|
||||
rspec-core (~> 3.9)
|
||||
rspec-expectations (~> 3.9)
|
||||
rspec-mocks (~> 3.9)
|
||||
rspec-support (~> 3.9)
|
||||
rspec-support (3.9.3)
|
||||
rubocop (0.89.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rspec-core (~> 3.10)
|
||||
rspec-expectations (~> 3.10)
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rubocop (1.22.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.1.1)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 0.3.0, < 1.0)
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (0.3.0)
|
||||
parser (>= 2.7.1.4)
|
||||
rubocop-performance (1.7.1)
|
||||
rubocop (>= 0.82.0)
|
||||
rubocop-rails (2.8.1)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.12.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-performance (1.11.5)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.12.3)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.87.0)
|
||||
rubocop-rspec (1.43.2)
|
||||
rubocop (~> 0.87)
|
||||
ruby-enum (0.9.0)
|
||||
i18n
|
||||
ruby-progressbar (1.10.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.5.0)
|
||||
rubocop (~> 1.19)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-vips (2.1.3)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_parser (3.15.0)
|
||||
rubocop (>= 0.87.0)
|
||||
sexp_processor (~> 4.9)
|
||||
safe_yaml (1.0.5)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
ruby_parser (3.17.0)
|
||||
sexp_processor (~> 4.15, >= 4.15.1)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
@@ -498,29 +519,38 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (2.6.9)
|
||||
scout_apm (4.1.2)
|
||||
parser
|
||||
scss_lint (0.59.0)
|
||||
sass (~> 3.5, >= 3.5.5)
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (2.3.0)
|
||||
sentry-raven (3.0.3)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (4.7.3)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sentry-ruby (4.7.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
faraday (>= 1.0)
|
||||
sexp_processor (4.15.1)
|
||||
shoulda-matchers (4.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
sidekiq (6.1.1)
|
||||
sentry-ruby-core (= 4.7.3)
|
||||
sentry-ruby-core (4.7.3)
|
||||
concurrent-ruby
|
||||
faraday
|
||||
sentry-sidekiq (4.7.3)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.15.3)
|
||||
shoulda-matchers (5.0.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.2.2)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
sidekiq-cron (1.2.0)
|
||||
fugit (~> 1.1)
|
||||
sidekiq (>= 4.2.1)
|
||||
signet (0.15.0)
|
||||
addressable (~> 2.3)
|
||||
signet (0.16.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
@@ -529,7 +559,7 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (0.15.0)
|
||||
slack-ruby-client (0.17.0)
|
||||
faraday (>= 1.0)
|
||||
faraday_middleware
|
||||
gli
|
||||
@@ -539,7 +569,7 @@ GEM
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (3.7.2)
|
||||
sprockets (4.0.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.2)
|
||||
@@ -547,74 +577,72 @@ GEM
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.4.0)
|
||||
telegram-bot-ruby (0.12.0)
|
||||
faraday
|
||||
inflecto
|
||||
virtus
|
||||
telephone_number (1.4.9)
|
||||
statsd-ruby (1.5.0)
|
||||
telephone_number (1.4.12)
|
||||
thor (1.1.0)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
trailblazer-option (0.1.1)
|
||||
twilio-ruby (5.32.0)
|
||||
faraday (~> 1.0.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitty (0.1.1)
|
||||
twitty (0.1.4)
|
||||
oauth
|
||||
tzinfo (1.2.9)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (2.1.0)
|
||||
uniform_notifier (1.14.2)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (3.3.1)
|
||||
valid_email2 (4.0.0)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
virtus (1.0.5)
|
||||
axiom-types (~> 0.1)
|
||||
coercible (~> 1.0)
|
||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.0.4)
|
||||
web-console (4.1.0)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.8.3)
|
||||
addressable (>= 2.3.6)
|
||||
webmock (3.14.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.2.1)
|
||||
webpacker (5.4.3)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (1.0.0)
|
||||
webpush (1.1.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.3)
|
||||
webrick (1.7.0)
|
||||
websocket-driver (0.7.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.4.2)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
ruby
|
||||
x86_64-darwin-18
|
||||
x86_64-darwin-20
|
||||
x86_64-darwin-21
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
active_record_query_trace
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
@@ -632,15 +660,18 @@ DEPENDENCIES
|
||||
commonmarker
|
||||
cypress-on-rails (~> 1.0)
|
||||
database_cleaner
|
||||
ddtrace
|
||||
devise
|
||||
devise-secure_password (~> 2.0)
|
||||
devise-secure_password (~> 2.0)!
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
down (~> 5.0)
|
||||
facebook-messenger
|
||||
factory_bot_rails
|
||||
faker
|
||||
fcm
|
||||
flag_shih_tzu
|
||||
flay
|
||||
foreman
|
||||
geocoder
|
||||
google-cloud-dialogflow
|
||||
@@ -649,40 +680,44 @@ DEPENDENCIES
|
||||
haikunator
|
||||
hairtrigger
|
||||
hashie
|
||||
image_processing
|
||||
jbuilder
|
||||
json_refs!
|
||||
json_refs
|
||||
json_schemer
|
||||
jwt
|
||||
kaminari
|
||||
koala
|
||||
letter_opener
|
||||
line-bot-api
|
||||
liquid
|
||||
listen
|
||||
maxminddb
|
||||
mini_magick
|
||||
mock_redis!
|
||||
mock_redis
|
||||
newrelic_rpm
|
||||
pg
|
||||
procore-sift
|
||||
pry-rails
|
||||
puma
|
||||
pundit
|
||||
rack-attack
|
||||
rack-cors
|
||||
rack-timeout
|
||||
rails
|
||||
redis
|
||||
redis-namespace
|
||||
redis-rack-cache
|
||||
responders
|
||||
rest-client
|
||||
rspec-rails (~> 4.0.0.beta2)
|
||||
rspec
|
||||
rspec-rails (~> 5.0.0)
|
||||
rubocop
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-raven
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq-cron
|
||||
@@ -691,7 +726,6 @@ DEPENDENCIES
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
telegram-bot-ruby
|
||||
telephone_number
|
||||
time_diff
|
||||
twilio-ruby (~> 5.32.0)
|
||||
@@ -706,7 +740,7 @@ DEPENDENCIES
|
||||
wisper (= 2.0.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.3p183
|
||||
ruby 3.0.2p107
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
2.2.25
|
||||
|
||||
@@ -22,6 +22,8 @@ ___
|
||||
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
|
||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
|
||||
<a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a>
|
||||
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a>
|
||||
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a>
|
||||
</p>
|
||||
|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png" width="100%" alt="Chat dashboard"/>
|
||||
@@ -29,9 +31,8 @@ ___
|
||||
Chatwoot is an open-source omnichannel customer support software. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it open-source, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
|
||||
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community.
|
||||
|
||||
---
|
||||
|
||||
### Features
|
||||
## Features
|
||||
|
||||
Chatwoot gives an integrated view of conversations happening in different communication channels.
|
||||
|
||||
@@ -57,28 +58,22 @@ Other features include:
|
||||
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
||||
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
||||
|
||||
---
|
||||
## Documentation
|
||||
|
||||
### Documentation
|
||||
Detailed documentation is available at [chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
||||
|
||||
Detailed documentation is available at [www.chatwoot.com/help-center](https://www.chatwoot.com/help-center).
|
||||
|
||||
### Translation process
|
||||
## Translation process
|
||||
|
||||
The translation process for Chatwoot web and mobile app is managed at [https://translate.chatwoot.com](https://translate.chatwoot.com) using Crowdin. Please read the [translation guide](https://www.chatwoot.com/docs/contributing/translating-chatwoot-to-your-language) for contributing to Chatwoot.
|
||||
|
||||
---
|
||||
|
||||
### Branching model
|
||||
## Branching model
|
||||
|
||||
We use the [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`.
|
||||
If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`.
|
||||
|
||||
---
|
||||
## Deployment
|
||||
|
||||
### Deployment
|
||||
|
||||
#### Heroku one-click deploy
|
||||
### Heroku one-click deploy
|
||||
|
||||
Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button:
|
||||
|
||||
@@ -86,13 +81,21 @@ Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button
|
||||
|
||||
Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables.
|
||||
|
||||
#### Other deployment options
|
||||
### Other deployment options
|
||||
|
||||
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
|
||||
|
||||
---
|
||||
## Security
|
||||
|
||||
### Contributors ✨
|
||||
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
|
||||
|
||||
|
||||
## Community? Questions? Support ?
|
||||
|
||||
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
|
||||
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||
|
||||
|
||||
+24
-1
@@ -1,8 +1,31 @@
|
||||
# Security Policy
|
||||
Chatwoot is looking forward to working with security researchers across the world to keep Chatwoot and our users safe. If you have found an issue in our systems/applications, please reach out to us.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose).
|
||||
|
||||
This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
|
||||
|
||||
If you have any questions about the process, feel free to reach out to hello@chatwoot.com.
|
||||
If you have any questions about the process, feel free to reach out to security@chatwoot.com.
|
||||
|
||||
|
||||
## Out of scope
|
||||
|
||||
Please do not perform testing against Chatwoot production services. Use a self hosted instance to perform tests.
|
||||
|
||||
We consider the following to be out of scope, though there may be exceptions.
|
||||
|
||||
- Missing HTTP security headers
|
||||
- Self XSS
|
||||
- HTTP Host Header XSS without working proof-of-concept
|
||||
- Incomplete/Missing SPF/DKIM
|
||||
- Denial of Service attacks
|
||||
- DNSSEC
|
||||
- Social Engineering attacks
|
||||
|
||||
If you are not sure about the scope, please create a report.
|
||||
|
||||
## Thanks
|
||||
|
||||
Thank you for keeping Chatwoot and our users safe. 🙇
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
"FRONTEND_URL": {
|
||||
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
||||
"value": "https://CHANGE.herokuapp.com"
|
||||
},
|
||||
"INSTALLATION_ENV": {
|
||||
"description": "Installation method used for Chatwoot.",
|
||||
"value": "heroku"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
||||
@@ -3,8 +3,9 @@ class ContactIdentifyAction
|
||||
|
||||
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)
|
||||
merge_if_existing_identified_contact
|
||||
merge_if_existing_email_contact
|
||||
merge_if_existing_phone_number_contact
|
||||
update_contact
|
||||
end
|
||||
@contact
|
||||
@@ -16,6 +17,18 @@ class ContactIdentifyAction
|
||||
@account ||= @contact.account
|
||||
end
|
||||
|
||||
def merge_if_existing_identified_contact
|
||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||
end
|
||||
|
||||
def merge_if_existing_email_contact
|
||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||
end
|
||||
|
||||
def merge_if_existing_phone_number_contact
|
||||
@contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact)
|
||||
end
|
||||
|
||||
def existing_identified_contact
|
||||
return if params[:identifier].blank?
|
||||
|
||||
@@ -28,6 +41,12 @@ class ContactIdentifyAction
|
||||
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
||||
end
|
||||
|
||||
def existing_phone_number_contact
|
||||
return if params[:phone_number].blank?
|
||||
|
||||
@existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number])
|
||||
end
|
||||
|
||||
def merge_contacts?(existing_contact, _contact)
|
||||
existing_contact && existing_contact.id != @contact.id
|
||||
end
|
||||
@@ -36,7 +55,9 @@ class ContactIdentifyAction
|
||||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||
# blank identifier or email will throw unique index error
|
||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||
@contact.update!(params.slice(:name, :email, :identifier).reject { |_k, v| v.blank? }.merge({ custom_attributes: custom_attributes }))
|
||||
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
|
||||
v.blank?
|
||||
end.merge({ custom_attributes: custom_attributes }))
|
||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class ContactMergeAction
|
||||
include Events::Types
|
||||
pattr_initialize [:account!, :base_contact!, :mergee_contact!]
|
||||
|
||||
def perform
|
||||
@@ -11,7 +12,7 @@ class ContactMergeAction
|
||||
merge_conversations
|
||||
merge_messages
|
||||
merge_contact_inboxes
|
||||
remove_mergee_contact
|
||||
merge_and_remove_mergee_contact
|
||||
end
|
||||
@base_contact
|
||||
end
|
||||
@@ -40,7 +41,18 @@ class ContactMergeAction
|
||||
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||
end
|
||||
|
||||
def remove_mergee_contact
|
||||
def merge_and_remove_mergee_contact
|
||||
mergable_attribute_keys = %w[identifier name email phone_number additional_attributes custom_attributes]
|
||||
base_contact_attributes = base_contact.attributes.slice(*mergable_attribute_keys).compact_blank
|
||||
mergee_contact_attributes = mergee_contact.attributes.slice(*mergable_attribute_keys).compact_blank
|
||||
|
||||
# attributes in base contact are given preference
|
||||
merged_attributes = mergee_contact_attributes.deep_merge(base_contact_attributes)
|
||||
# retaining old pubsub token to notify the contacts that are listening
|
||||
mergee_pubsub_token = mergee_contact.pubsub_token
|
||||
|
||||
@mergee_contact.destroy!
|
||||
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact, tokens: [mergee_pubsub_token])
|
||||
@base_contact.update!(merged_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,7 +43,7 @@ $woot-logo-padding: $space-large $space-two;
|
||||
// Colors
|
||||
$color-woot: #1f93ff;
|
||||
$color-gray: #6e6f73;
|
||||
$color-light-gray: #999a9b;
|
||||
$color-light-gray: #747677;
|
||||
$color-border: #e0e6ed;
|
||||
$color-border-light: #f0f4f5;
|
||||
$color-background: #f4f6fb;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
require 'facebook/messenger'
|
||||
|
||||
class FacebookBot
|
||||
Facebook::Messenger::Bot.on :message do |message|
|
||||
Rails.logger.info "MESSAGE_RECIEVED #{message}"
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
end
|
||||
|
||||
Facebook::Messenger::Bot.on :delivery do |delivery|
|
||||
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
|
||||
# delivery.sender # => { 'id' => '1008372609250235' }
|
||||
# delivery.recipient # => { 'id' => '2015573629214912' }
|
||||
# delivery.at # => 2016-04-22 21:30:36 +0200
|
||||
# delivery.seq # => 37
|
||||
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
|
||||
updater.perform
|
||||
Rails.logger.info "Human was online at #{delivery.at}"
|
||||
end
|
||||
|
||||
Facebook::Messenger::Bot.on :message_echo do |message|
|
||||
Rails.logger.info "MESSAGE_ECHO #{message}"
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
end
|
||||
end
|
||||
@@ -38,17 +38,30 @@ class ContactBuilder
|
||||
end
|
||||
|
||||
def find_contact
|
||||
contact = nil
|
||||
|
||||
contact = account.contacts.find_by(identifier: contact_attributes[:identifier]) if contact_attributes[:identifier].present?
|
||||
|
||||
contact ||= account.contacts.find_by(email: contact_attributes[:email]) if contact_attributes[:email].present?
|
||||
|
||||
contact ||= account.contacts.find_by(phone_number: contact_attributes[:phone_number]) if contact_attributes[:phone_number].present?
|
||||
|
||||
contact = find_contact_by_identifier(contact_attributes[:identifier])
|
||||
contact ||= find_contact_by_email(contact_attributes[:email])
|
||||
contact ||= find_contact_by_phone_number(contact_attributes[:phone_number])
|
||||
contact
|
||||
end
|
||||
|
||||
def find_contact_by_identifier(identifier)
|
||||
return if identifier.blank?
|
||||
|
||||
account.contacts.find_by(identifier: identifier)
|
||||
end
|
||||
|
||||
def find_contact_by_email(email)
|
||||
return if email.blank?
|
||||
|
||||
account.contacts.find_by(email: email.downcase)
|
||||
end
|
||||
|
||||
def find_contact_by_phone_number(phone_number)
|
||||
return if phone_number.blank?
|
||||
|
||||
account.contacts.find_by(phone_number: phone_number)
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
contact = find_contact || create_contact
|
||||
@@ -57,6 +70,7 @@ class ContactBuilder
|
||||
contact_inbox
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::Facebook::MessageBuilder
|
||||
class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, inbox, outgoing_echo: false)
|
||||
super()
|
||||
@response = response
|
||||
@inbox = inbox
|
||||
@outgoing_echo = outgoing_echo
|
||||
@@ -17,12 +18,18 @@ class Messages::Facebook::MessageBuilder
|
||||
end
|
||||
|
||||
def perform
|
||||
# This channel might require reauthorization, may be owner might have changed the fb password
|
||||
return if @inbox.channel.reauthorization_required?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
build_contact
|
||||
build_message
|
||||
end
|
||||
ensure_contact_avatar
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
@@ -36,30 +43,22 @@ class Messages::Facebook::MessageBuilder
|
||||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
|
||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
end
|
||||
|
||||
def build_message
|
||||
@message = conversation.messages.create!(message_params)
|
||||
|
||||
@attachments.each do |attachment|
|
||||
process_attachment(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
def ensure_contact_avatar
|
||||
return if contact_params[:remote_avatar_url].blank?
|
||||
return if @contact.avatar.attached?
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
file_resource = LocalResource.new(file_url)
|
||||
attachment.file.attach(io: file_resource.file, filename: file_resource.filename, content_type: file_resource.encoding)
|
||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url])
|
||||
end
|
||||
|
||||
def conversation
|
||||
@@ -73,28 +72,6 @@ class Messages::Facebook::MessageBuilder
|
||||
))
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def location_params(attachment)
|
||||
lat = attachment['payload']['coordinates']['lat']
|
||||
long = attachment['payload']['coordinates']['long']
|
||||
@@ -132,18 +109,30 @@ class Messages::Facebook::MessageBuilder
|
||||
}
|
||||
end
|
||||
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Raven.capture_exception(e)
|
||||
end
|
||||
def process_contact_params_result(result)
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
}
|
||||
end
|
||||
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue Koala::Facebook::ClientError => e
|
||||
result = {}
|
||||
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
||||
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
||||
Sentry.capture_exception(e) unless @outgoing_echo
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
process_contact_params_result(result)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||
# Assumptions
|
||||
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
attr_reader :messaging
|
||||
|
||||
def initialize(messaging, inbox, outgoing_echo: false)
|
||||
super()
|
||||
@messaging = messaging
|
||||
@inbox = inbox
|
||||
@outgoing_echo = outgoing_echo
|
||||
end
|
||||
|
||||
def perform
|
||||
return if @inbox.channel.reauthorization_required?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
build_message
|
||||
end
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attachments
|
||||
@messaging[:message][:attachments] || {}
|
||||
end
|
||||
|
||||
def message_type
|
||||
@outgoing_echo ? :outgoing : :incoming
|
||||
end
|
||||
|
||||
def message_source_id
|
||||
@outgoing_echo ? recipient_id : sender_id
|
||||
end
|
||||
|
||||
def sender_id
|
||||
@messaging[:sender][:id]
|
||||
end
|
||||
|
||||
def recipient_id
|
||||
@messaging[:recipient][:id]
|
||||
end
|
||||
|
||||
def message
|
||||
@messaging[:message]
|
||||
end
|
||||
|
||||
def contact
|
||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: message_source_id)&.contact
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||
end
|
||||
|
||||
def message_content
|
||||
@messaging[:message][:text]
|
||||
end
|
||||
|
||||
def content_attributes
|
||||
{ message_id: @messaging[:message][:mid] }
|
||||
end
|
||||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
|
||||
@message = conversation.messages.create!(message_params)
|
||||
|
||||
attachments.each do |attachment|
|
||||
process_attachment(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def build_conversation
|
||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
|
||||
Conversation.create!(conversation_params.merge(
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
))
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id,
|
||||
additional_attributes: {
|
||||
type: 'instagram_direct_message'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: message_type,
|
||||
source_id: message_source_id,
|
||||
content: message_content,
|
||||
content_attributes: content_attributes,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
}
|
||||
end
|
||||
|
||||
def already_sent_from_chatwoot?
|
||||
cw_message = conversation.messages.where(
|
||||
source_id: nil,
|
||||
message_type: 'outgoing',
|
||||
content: message_content,
|
||||
private: false,
|
||||
status: :sent
|
||||
).first
|
||||
cw_message.update(content_attributes: content_attributes) if cw_message.present?
|
||||
cw_message.present?
|
||||
end
|
||||
|
||||
### Sample response
|
||||
# {
|
||||
# "object": "instagram",
|
||||
# "entry": [
|
||||
# {
|
||||
# "id": "<IGID>",// ig id of the business
|
||||
# "time": 1569262486134,
|
||||
# "messaging": [
|
||||
# {
|
||||
# "sender": {
|
||||
# "id": "<IGSID>"
|
||||
# },
|
||||
# "recipient": {
|
||||
# "id": "<IGID>"
|
||||
# },
|
||||
# "timestamp": 1569262485349,
|
||||
# "message": {
|
||||
# "mid": "<MESSAGE_ID>",
|
||||
# "text": "<MESSAGE_CONTENT>"
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ],
|
||||
# }
|
||||
end
|
||||
@@ -15,21 +15,36 @@ class Messages::MessageBuilder
|
||||
|
||||
def perform
|
||||
@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
|
||||
process_attachments
|
||||
process_emails
|
||||
@message.save!
|
||||
@message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_attachments
|
||||
return if @attachments.blank?
|
||||
|
||||
@attachments.each do |uploaded_attachment|
|
||||
@message.attachments.build(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def process_emails
|
||||
return unless @conversation.inbox&.inbox_type == 'Email'
|
||||
|
||||
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails]
|
||||
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails]
|
||||
|
||||
@message.content_attributes[:cc_emails] = cc_emails
|
||||
@message.content_attributes[:bcc_emails] = bcc_emails
|
||||
end
|
||||
|
||||
def message_type
|
||||
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
||||
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
class Messages::Messenger::MessageBuilder
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
attachment_file = Down.download(
|
||||
file_url
|
||||
)
|
||||
attachment.file.attach(
|
||||
io: attachment_file,
|
||||
filename: attachment_file.original_filename,
|
||||
content_type: attachment_file.content_type
|
||||
)
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@ class NotificationSubscriptionBuilder
|
||||
def perform
|
||||
# if multiple accounts were used to login in same browser
|
||||
move_subscription_to_user if identifier_subscription && identifier_subscription.user_id != user.id
|
||||
build_identifier_subscription if identifier_subscription.blank?
|
||||
identifier_subscription.blank? ? build_identifier_subscription : update_identifier_subscription
|
||||
identifier_subscription
|
||||
end
|
||||
|
||||
@@ -25,6 +25,10 @@ class NotificationSubscriptionBuilder
|
||||
end
|
||||
|
||||
def build_identifier_subscription
|
||||
user.notification_subscriptions.create(params.merge(identifier: identifier))
|
||||
@identifier_subscription = user.notification_subscriptions.create(params.merge(identifier: identifier))
|
||||
end
|
||||
|
||||
def update_identifier_subscription
|
||||
identifier_subscription.update(params.merge(identifier: identifier))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,17 +32,34 @@ class V2::ReportBuilder
|
||||
private
|
||||
|
||||
def scope
|
||||
return account if params[:type].match?('account')
|
||||
return inbox if params[:type].match?('inbox')
|
||||
return user if params[:type].match?('agent')
|
||||
case params[:type]
|
||||
when :account
|
||||
account
|
||||
when :inbox
|
||||
inbox
|
||||
when :agent
|
||||
user
|
||||
when :label
|
||||
label
|
||||
when :team
|
||||
team
|
||||
end
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.where(id: params[:id]).first
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= account.users.where(id: params[:id]).first
|
||||
@user ||= account.users.find(params[:id])
|
||||
end
|
||||
|
||||
def label
|
||||
@label ||= account.labels.find(params[:id])
|
||||
end
|
||||
|
||||
def team
|
||||
@team ||= account.teams.find(params[:id])
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
@@ -51,15 +68,14 @@ class V2::ReportBuilder
|
||||
.count
|
||||
end
|
||||
|
||||
# unscoped removes all scopes added to a model previously
|
||||
def incoming_messages_count
|
||||
scope.messages.unscoped.where(account_id: account.id).incoming
|
||||
scope.messages.incoming.unscope(:order)
|
||||
.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
|
||||
scope.messages.outgoing.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
@@ -9,21 +9,18 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
@agents = agents
|
||||
end
|
||||
|
||||
def create; end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.slice(:name).compact)
|
||||
@agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent.current_account_user.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.except(:role))
|
||||
@agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role]
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent }
|
||||
end
|
||||
|
||||
def create
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
@@ -47,22 +44,25 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def save_account_user
|
||||
AccountUser.create!(
|
||||
AccountUser.create!({
|
||||
account_id: Current.account.id,
|
||||
user_id: @user.id,
|
||||
role: new_agent_params[:role],
|
||||
inviter_id: current_user.id
|
||||
)
|
||||
}.merge({
|
||||
role: new_agent_params[:role],
|
||||
availability: new_agent_params[:availability],
|
||||
auto_offline: new_agent_params[:auto_offline]
|
||||
}.compact))
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
# intial string ensures the password requirements are met
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
||||
end
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class Api::V1::Accounts::BaseController < Api::BaseController
|
||||
account = Account.find(params[:account_id])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource&.is_a?(AgentBot)
|
||||
elsif @resource.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
|
||||
@@ -12,9 +12,10 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
page_access_token: page_access_token
|
||||
)
|
||||
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
set_instagram_id(page_access_token, facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +23,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||
end
|
||||
|
||||
def set_instagram_id(page_access_token, facebook_channel)
|
||||
fb_object = Koala::Facebook::API.new(page_access_token)
|
||||
response = fb_object.get_connections('me', '', { fields: 'instagram_business_account' })
|
||||
return if response['instagram_business_account'].blank?
|
||||
|
||||
instagram_id = response['instagram_business_account']['id']
|
||||
facebook_channel.update(instagram_id: instagram_id)
|
||||
end
|
||||
|
||||
# get params[:inbox_id], current_account. params[:omniauth_token]
|
||||
def reauthorize_page
|
||||
if @inbox&.facebook?
|
||||
@@ -45,8 +55,13 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
|
||||
def update_fb_page(fb_page_id, access_token)
|
||||
fb_page = get_fb_page(fb_page_id)
|
||||
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
||||
fb_page&.reauthorized!
|
||||
ActiveRecord::Base.transaction do
|
||||
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
||||
set_instagram_id(access_token, fb_page)
|
||||
fb_page&.reauthorized!
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
def get_fb_page(fb_page_id)
|
||||
@@ -69,39 +84,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
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
|
||||
page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id'])
|
||||
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)
|
||||
rescue *ExceptionList::URI_EXCEPTIONS => e
|
||||
Rails.logger.info "invalid url #{file_url} : #{e.message}"
|
||||
end
|
||||
|
||||
def get_avatar_url(page_id)
|
||||
begin
|
||||
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
|
||||
uri = URI.parse(url)
|
||||
tries = 3
|
||||
begin
|
||||
response = uri.open(redirect: false)
|
||||
rescue OpenURI::HTTPRedirect => e
|
||||
uri = e.uri # assigned from the "Location" response header
|
||||
retry if (tries -= 1).positive?
|
||||
raise
|
||||
end
|
||||
pic_url = response.base_uri.to_s
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
pic_url = nil
|
||||
end
|
||||
pic_url
|
||||
avatar_file = Down.download(
|
||||
"http://graph.facebook.com/#{page_id}/picture?type=large"
|
||||
)
|
||||
facebook_inbox.avatar.attach(io: avatar_file, filename: avatar_file.original_filename, content_type: avatar_file.content_type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def campaign_params
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id,
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
|
||||
:scheduled_at, audience: [:type, :id], trigger_rules: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,9 +6,8 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
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
|
||||
Sentry.capture_exception(e)
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search]
|
||||
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
||||
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||
|
||||
def index
|
||||
@contacts_count = resolved_contacts.count
|
||||
@@ -21,7 +22,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||
|
||||
contacts = resolved_contacts.where(
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search',
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
|
||||
search: "%#{params[:q]}%"
|
||||
)
|
||||
@contacts_count = contacts.count
|
||||
@@ -29,10 +30,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def import
|
||||
render json: { error: I18n.t('errors.contacts.import.failed') }, status: :unprocessable_entity and return if params[:import_file].blank?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
import = Current.account.data_imports.create!(data_type: 'contacts')
|
||||
import.import_file.attach(params[:import_file])
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -69,6 +73,18 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
if ::OnlineStatusTracker.get_presence(
|
||||
@contact.account.id, 'Contact', @contact.id
|
||||
)
|
||||
return render_error({ message: I18n.t('contacts.online.delete', contact_name: @contact.name.capitalize) },
|
||||
:unprocessable_entity)
|
||||
end
|
||||
|
||||
@contact.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: Move this to a finder class
|
||||
@@ -78,6 +94,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
@resolved_contacts = Current.account.contacts
|
||||
.where.not(email: [nil, ''])
|
||||
.or(Current.account.contacts.where.not(phone_number: [nil, '']))
|
||||
.or(Current.account.contacts.where.not(identifier: [nil, '']))
|
||||
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
|
||||
@resolved_contacts
|
||||
end
|
||||
@@ -87,11 +104,15 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def fetch_contacts_with_conversation_count(contacts)
|
||||
filtrate(contacts).left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }, { contact_inboxes: [:inbox] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
contacts_with_conversation_count = filtrate(contacts).left_outer_joins(:conversations)
|
||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
||||
.group('contacts.id')
|
||||
.includes([{ avatar_attachment: [:blob] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
|
||||
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||
|
||||
contacts_with_conversation_count
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
@@ -103,7 +124,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||
params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||
end
|
||||
|
||||
def contact_custom_attributes
|
||||
@@ -117,7 +138,19 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes })
|
||||
end
|
||||
|
||||
def set_include_contact_inboxes
|
||||
@include_contact_inboxes = if params[:include_contact_inboxes].present?
|
||||
params[:include_contact_inboxes] == 'true'
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_contact
|
||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||
end
|
||||
|
||||
def render_error(error, error_status)
|
||||
render json: error, status: error_status
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
|
||||
# assigns agent/team to a conversation
|
||||
def create
|
||||
set_assignee
|
||||
render json: @assignee
|
||||
if params.key?(:assignee_id)
|
||||
set_agent
|
||||
elsif params.key?(:team_id)
|
||||
set_team
|
||||
else
|
||||
render json: nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_assignee
|
||||
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
|
||||
if params.key?(:assignee_id)
|
||||
@assignee = Current.account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(@assignee)
|
||||
elsif params.key?(:team_id)
|
||||
@assignee = Current.account.teams.find_by(id: params[:team_id])
|
||||
@conversation.update!(team: @assignee)
|
||||
def set_agent
|
||||
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(@agent)
|
||||
render_agent
|
||||
end
|
||||
|
||||
def render_agent
|
||||
if @agent.nil?
|
||||
render json: nil
|
||||
else
|
||||
render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @agent }
|
||||
end
|
||||
end
|
||||
|
||||
def set_team
|
||||
@team = Current.account.teams.find_by(id: params[:team_id])
|
||||
@conversation.update!(team: @team)
|
||||
render json: @team
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
include Events::Types
|
||||
include DateRangeHelper
|
||||
|
||||
before_action :conversation, except: [:index, :meta, :search, :create]
|
||||
before_action :contact_inbox, only: [:create]
|
||||
@@ -49,8 +50,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def toggle_status
|
||||
if params[:status]
|
||||
@conversation.status = params[:status]
|
||||
@status = @conversation.save
|
||||
set_conversation_status
|
||||
@status = @conversation.save!
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
@@ -68,11 +69,23 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def update_last_seen
|
||||
@conversation.agent_last_seen_at = DateTime.now.utc
|
||||
@conversation.assignee_last_seen_at = DateTime.now.utc if assignee?
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def custom_attributes
|
||||
@conversation.custom_attributes = params.permit(custom_attributes: {})[:custom_attributes]
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_conversation_status
|
||||
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||
@conversation.status = status
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
|
||||
def trigger_typing_event(event)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||
@@ -105,17 +118,29 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def conversation_params
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
||||
status = params[:status].present? ? { status: params[:status] } : {}
|
||||
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
status = { status: 'pending' } if status[:status] == 'bot'
|
||||
{
|
||||
account_id: Current.account.id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes
|
||||
additional_attributes: additional_attributes,
|
||||
custom_attributes: custom_attributes,
|
||||
snoozed_until: params[:snoozed_until],
|
||||
assignee_id: params[:assignee_id],
|
||||
team_id: params[:team_id]
|
||||
}.merge(status)
|
||||
end
|
||||
|
||||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||
end
|
||||
|
||||
def assignee?
|
||||
@conversation.assignee_id? && current_user == @conversation.assignee
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_custom_attributes_definitions, except: [:create]
|
||||
before_action :fetch_custom_attribute_definition, only: [:show, :update, :destroy]
|
||||
DEFAULT_ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
|
||||
|
||||
def index; end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@custom_attribute_definition = Current.account.custom_attribute_definitions.create!(
|
||||
permitted_payload
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
@custom_attribute_definition.update!(permitted_payload)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_attribute_definition.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_custom_attributes_definitions
|
||||
@custom_attribute_definitions = Current.account.custom_attribute_definitions.where(
|
||||
attribute_model: permitted_params[:attribute_model] || DEFAULT_ATTRIBUTE_MODEL
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_custom_attribute_definition
|
||||
@custom_attribute_definition = Current.account.custom_attribute_definitions.find(permitted_params[:id])
|
||||
end
|
||||
|
||||
def permitted_payload
|
||||
params.require(:custom_attribute_definition).permit(
|
||||
:attribute_display_name,
|
||||
:attribute_description,
|
||||
:attribute_display_type,
|
||||
:attribute_key,
|
||||
:attribute_model,
|
||||
:default_value
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :filter_type, :attribute_model)
|
||||
end
|
||||
end
|
||||
@@ -1,26 +1,40 @@
|
||||
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox, only: [:create, :show]
|
||||
before_action :current_agents_ids, only: [:create]
|
||||
before_action :fetch_inbox
|
||||
before_action :current_agents_ids, only: [:update]
|
||||
|
||||
def create
|
||||
authorize @inbox, :create?
|
||||
begin
|
||||
# update also done via same action
|
||||
update_agents_list
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
render_could_not_create_error('Could not add agents to inbox')
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:user_ids].map { |user_id| @inbox.add_member(user_id) }
|
||||
end
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @inbox, :show?
|
||||
@agents = Current.account.users.where(id: @inbox.members.select(:user_id))
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @inbox, :update?
|
||||
update_agents_list
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @inbox, :destroy?
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:user_ids].map { |user_id| @inbox.remove_member(user_id) }
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_updated_agents
|
||||
@agents = Current.account.users.where(id: @inbox.members.select(:user_id))
|
||||
end
|
||||
|
||||
def update_agents_list
|
||||
# get all the user_ids which the inbox currently has as members.
|
||||
# get the list of user_ids from params
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
before_action :check_authorization
|
||||
# we are already handling the authorization in fetch inbox
|
||||
before_action :check_authorization, except: [:show]
|
||||
|
||||
def index
|
||||
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def assignable_agents
|
||||
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
||||
end
|
||||
@@ -15,26 +18,32 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
@campaigns = @inbox.campaigns
|
||||
end
|
||||
|
||||
def avatar
|
||||
@inbox.avatar.attachment.destroy! if @inbox.avatar.attached?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = create_channel
|
||||
@inbox = Current.account.inboxes.build(
|
||||
name: permitted_params[:name],
|
||||
greeting_message: permitted_params[:greeting_message],
|
||||
greeting_enabled: permitted_params[:greeting_enabled],
|
||||
channel: channel
|
||||
{
|
||||
name: inbox_name(channel),
|
||||
channel: channel
|
||||
}.merge(
|
||||
permitted_params.except(:channel)
|
||||
)
|
||||
)
|
||||
@inbox.avatar.attach(permitted_params[:avatar])
|
||||
@inbox.save!
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@inbox.update(inbox_update_params.except(:channel))
|
||||
@inbox.update(permitted_params.except(:channel))
|
||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||
return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
|
||||
|
||||
@inbox.channel.update!(inbox_update_params[:channel])
|
||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel]) if permitted_params(channel_attributes)[:channel].present?
|
||||
update_channel_feature_flags
|
||||
end
|
||||
|
||||
@@ -69,43 +78,59 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||
end
|
||||
|
||||
def inbox_name(channel)
|
||||
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
|
||||
|
||||
permitted_params[:name]
|
||||
end
|
||||
|
||||
def create_channel
|
||||
case permitted_params[:channel][:type]
|
||||
when 'web_widget'
|
||||
Current.account.web_widgets.create!(permitted_params[:channel].except(:type))
|
||||
Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'api'
|
||||
Current.account.api_channels.create!(permitted_params[:channel].except(:type))
|
||||
Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'email'
|
||||
Current.account.email_channels.create!(permitted_params[:channel].except(:type))
|
||||
Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'line'
|
||||
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'telegram'
|
||||
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'whatsapp'
|
||||
Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
end
|
||||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
return unless inbox_update_params[:channel].key? :selected_feature_flags
|
||||
return unless @inbox.web_widget?
|
||||
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
||||
|
||||
@inbox.channel.selected_feature_flags = inbox_update_params[:channel][:selected_feature_flags]
|
||||
@inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags]
|
||||
@inbox.channel.save!
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, :enable_email_collect, :csat_survey_enabled, channel:
|
||||
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time])
|
||||
def permitted_params(channel_attributes = [])
|
||||
params.permit(
|
||||
:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone,
|
||||
channel: [:type, *channel_attributes]
|
||||
)
|
||||
end
|
||||
|
||||
def inbox_update_params
|
||||
params.permit(:enable_auto_assignment, :enable_email_collect, :name, :avatar, :greeting_message, :greeting_enabled, :csat_survey_enabled,
|
||||
:working_hours_enabled, :out_of_office_message, :timezone,
|
||||
channel: [
|
||||
:website_url,
|
||||
:widget_color,
|
||||
:welcome_title,
|
||||
:welcome_tagline,
|
||||
:webhook_url,
|
||||
:email,
|
||||
:reply_time,
|
||||
:pre_chat_form_enabled,
|
||||
{ pre_chat_form_options: [:pre_chat_message, :require_email] },
|
||||
{ selected_feature_flags: [] }
|
||||
])
|
||||
def get_channel_attributes(channel_type)
|
||||
case channel_type
|
||||
when 'Channel::WebWidget'
|
||||
Channel::WebWidget::EDITABLE_ATTRS
|
||||
when 'Channel::Api'
|
||||
Channel::Api::EDITABLE_ATTRS
|
||||
when 'Channel::Email'
|
||||
Channel::Email::EDITABLE_ATTRS
|
||||
when 'Channel::Telegram'
|
||||
Channel::Telegram::EDITABLE_ATTRS
|
||||
when 'Channel::Line'
|
||||
Channel::Line::EDITABLE_ATTRS
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
class Api::V1::ProfilesController < Api::BaseController
|
||||
before_action :set_user
|
||||
|
||||
def show
|
||||
render partial: 'api/v1/models/user.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
def show; end
|
||||
|
||||
def update
|
||||
if password_params[:password].present?
|
||||
@@ -15,19 +13,26 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
@user.update!(profile_params)
|
||||
end
|
||||
|
||||
def availability
|
||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def availability_params
|
||||
params.require(:profile).permit(:account_id, :availability)
|
||||
end
|
||||
|
||||
def profile_params
|
||||
params.require(:profile).permit(
|
||||
:email,
|
||||
:name,
|
||||
:display_name,
|
||||
:avatar,
|
||||
:availability,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::WebhooksController < ApplicationController
|
||||
twitter_consumer.consume
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -94,6 +94,10 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
{ timestamp: permitted_params[:message][:timestamp] }
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
|
||||
@@ -4,10 +4,4 @@ class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController
|
||||
def index
|
||||
@campaigns = @web_widget.inbox.campaigns.where(enabled: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
||||
before_action :set_global_config
|
||||
|
||||
def create
|
||||
build_contact
|
||||
set_token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if @contact.present?
|
||||
|
||||
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
|
||||
@contact = @contact_inbox.contact
|
||||
end
|
||||
|
||||
def set_token
|
||||
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
||||
@token = ::Widget::TokenService.new(payload: payload).generate_token
|
||||
end
|
||||
|
||||
def additional_attributes
|
||||
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
|
||||
{ created_at_ip: request.remote_ip }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,6 +29,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,10 +4,4 @@ class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController
|
||||
def index
|
||||
@inbox_members = @web_widget.inbox.inbox_members.includes(:user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,8 +8,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def create
|
||||
@message = conversation.messages.new(message_params)
|
||||
@message.save
|
||||
build_attachment
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -29,13 +29,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
return if params[:message][:attachments].blank?
|
||||
|
||||
params[:message][:attachments].each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type)
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
|
||||
def account
|
||||
builder = V2::ReportBuilder.new(Current.account, account_report_params)
|
||||
def index
|
||||
builder = V2::ReportBuilder.new(Current.account, report_params)
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def account_summary
|
||||
render json: account_summary_metrics
|
||||
def summary
|
||||
render json: summary_metrics
|
||||
end
|
||||
|
||||
def agents
|
||||
@@ -23,31 +23,45 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
render layout: false, template: 'api/v2/accounts/reports/inboxes.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
def labels
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=labels_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
def teams
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=teams_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def account_summary_params
|
||||
def summary_params
|
||||
{
|
||||
type: :account,
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
}
|
||||
end
|
||||
|
||||
def account_report_params
|
||||
def report_params
|
||||
{
|
||||
metric: params[:metric],
|
||||
type: :account,
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
}
|
||||
end
|
||||
|
||||
def account_summary_metrics
|
||||
builder = V2::ReportBuilder.new(Current.account, account_summary_params)
|
||||
def summary_metrics
|
||||
builder = V2::ReportBuilder.new(Current.account, summary_params)
|
||||
builder.summary
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include DeviseTokenAuth::Concerns::SetUserByToken
|
||||
include RequestExceptionHandler
|
||||
include Pundit
|
||||
include SwitchLocale
|
||||
|
||||
@@ -9,22 +10,8 @@ class ApplicationController < ActionController::Base
|
||||
around_action :switch_locale
|
||||
around_action :handle_with_exception, unless: :devise_controller?
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
|
||||
private
|
||||
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Raven.capture_exception(e)
|
||||
render_not_found_error('Resource could not be found')
|
||||
rescue Pundit::NotAuthorizedError
|
||||
render_unauthorized('You are not authorized to do this action')
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
Current.reset
|
||||
end
|
||||
|
||||
def set_current_user
|
||||
@user ||= current_user
|
||||
Current.user = @user
|
||||
@@ -34,32 +21,6 @@ class ApplicationController < ActionController::Base
|
||||
@subscription ||= Current.account.subscription
|
||||
end
|
||||
|
||||
def render_unauthorized(message)
|
||||
render json: { error: message }, status: :unauthorized
|
||||
end
|
||||
|
||||
def render_not_found_error(message)
|
||||
render json: { error: message }, status: :not_found
|
||||
end
|
||||
|
||||
def render_could_not_create_error(message)
|
||||
render json: { error: message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_internal_server_error(message)
|
||||
render json: { error: message }, status: :internal_server_error
|
||||
end
|
||||
|
||||
def render_record_invalid(exception)
|
||||
render json: {
|
||||
message: exception.record.errors.full_messages.join(', ')
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_error_response(exception)
|
||||
render json: exception.to_hash, status: exception.http_status
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
{
|
||||
user: Current.user,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module AuthHelper
|
||||
def send_auth_headers(user)
|
||||
data = user.create_new_auth_token
|
||||
response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data['access-token']
|
||||
response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = 'Bearer'
|
||||
response.headers[DeviseTokenAuth.headers_names[:'access-token']] = data['access-token']
|
||||
response.headers[DeviseTokenAuth.headers_names[:'token-type']] = 'Bearer'
|
||||
response.headers[DeviseTokenAuth.headers_names[:client]] = data['client']
|
||||
response.headers[DeviseTokenAuth.headers_names[:expiry]] = data['expiry']
|
||||
response.headers[DeviseTokenAuth.headers_names[:uid]] = data['uid']
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
module RequestExceptionHandler
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Sentry.capture_exception(e)
|
||||
render_not_found_error('Resource could not be found')
|
||||
rescue Pundit::NotAuthorizedError
|
||||
render_unauthorized('You are not authorized to do this action')
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
Current.reset
|
||||
end
|
||||
|
||||
def render_unauthorized(message)
|
||||
render json: { error: message }, status: :unauthorized
|
||||
end
|
||||
|
||||
def render_not_found_error(message)
|
||||
render json: { error: message }, status: :not_found
|
||||
end
|
||||
|
||||
def render_could_not_create_error(message)
|
||||
render json: { error: message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_internal_server_error(message)
|
||||
render json: { error: message }, status: :internal_server_error
|
||||
end
|
||||
|
||||
def render_record_invalid(exception)
|
||||
render json: {
|
||||
message: exception.record.errors.full_messages.join(', ')
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_error_response(exception)
|
||||
render json: exception.to_hash, status: exception.http_status
|
||||
end
|
||||
end
|
||||
@@ -23,7 +23,9 @@ class DashboardController < ActionController::Base
|
||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
||||
'CHATWOOT_INBOX_TOKEN',
|
||||
'API_CHANNEL_NAME',
|
||||
'API_CHANNEL_THUMBNAIL'
|
||||
'API_CHANNEL_THUMBNAIL',
|
||||
'ANALYTICS_TOKEN',
|
||||
'ANALYTICS_HOST'
|
||||
).merge(
|
||||
APP_VERSION: Chatwoot.config[:version]
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ class Platform::Api::V1::AccountsController < PlatformController
|
||||
end
|
||||
|
||||
def destroy
|
||||
# TODO: obfusicate account
|
||||
DeleteObjectJob.perform_later(@resource)
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
|
||||
def create
|
||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||
@resource.confirm
|
||||
@resource.save!
|
||||
@resource.confirm
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
@@ -19,21 +19,33 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.update!(user_params)
|
||||
@resource.assign_attributes(user_update_params)
|
||||
@resource.save!
|
||||
end
|
||||
|
||||
def destroy
|
||||
# TODO: obfusicate user
|
||||
DeleteObjectJob.perform_later(@resource)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_custom_attributes
|
||||
return @resource.custom_attributes.merge(user_params[:custom_attributes]) if user_params[:custom_attributes]
|
||||
|
||||
@resource.custom_attributes
|
||||
end
|
||||
|
||||
def user_update_params
|
||||
# we want the merged custom attributes not the original one
|
||||
user_params.except(:custom_attributes).merge({ custom_attributes: user_custom_attributes })
|
||||
end
|
||||
|
||||
def set_resource
|
||||
@resource = User.find(params[:id])
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.permit(:name, :email, :password)
|
||||
params.permit(:name, :email, :password, custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class PlatformController < ActionController::API
|
||||
include RequestExceptionHandler
|
||||
|
||||
before_action :ensure_access_token
|
||||
before_action :set_platform_app
|
||||
before_action :set_resource, only: [:update, :show, :destroy]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
class Public::Api::V1::CsatSurveyController < PublicController
|
||||
before_action :set_conversation
|
||||
before_action :set_message
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
render json: { error: 'You cannot update the CSAT survey after 14 days' }, status: :unprocessable_entity and return if check_csat_locked
|
||||
|
||||
@message.update!(message_update_params[:message])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_conversation
|
||||
return if params[:id].blank?
|
||||
|
||||
@conversation = Conversation.find_by!(uuid: params[:id])
|
||||
end
|
||||
|
||||
def set_message
|
||||
@message = @conversation.messages.find_by!(content_type: 'input_csat')
|
||||
end
|
||||
|
||||
def message_update_params
|
||||
params.permit(message: [{ submitted_values: [:name, :title, :value, { csat_survey_response: [:feedback_message, :rating] }] }])
|
||||
end
|
||||
|
||||
def check_csat_locked
|
||||
(Time.zone.now.to_date - @message.created_at.to_date).to_i > 14
|
||||
end
|
||||
end
|
||||
@@ -7,14 +7,14 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
||||
|
||||
def create
|
||||
@message = @conversation.messages.new(message_params)
|
||||
@message.save
|
||||
build_attachment
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@message.update!(message_update_params)
|
||||
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: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
@@ -23,13 +23,12 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
||||
return if params[:attachments].blank?
|
||||
|
||||
params[:attachments].each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type)
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def message_finder_params
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# TODO: we should switch to ActionController::API for the base classes
|
||||
# One of the specs is failing when I tried doing that, lets revisit in future
|
||||
class PublicController < ActionController::Base
|
||||
include RequestExceptionHandler
|
||||
skip_before_action :verify_authenticity_token
|
||||
end
|
||||
|
||||
@@ -3,10 +3,9 @@ class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
|
||||
|
||||
def index
|
||||
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
|
||||
@accounts_count = number_with_delimiter(Account.all.length)
|
||||
@users_count = number_with_delimiter(User.all.length)
|
||||
@inboxes_count = number_with_delimiter(Inbox.all.length)
|
||||
@conversations_count = number_with_delimiter(Conversation.all.length)
|
||||
@messages_count = number_with_delimiter(Message.all.length)
|
||||
@accounts_count = number_with_delimiter(Account.count)
|
||||
@users_count = number_with_delimiter(User.count)
|
||||
@inboxes_count = number_with_delimiter(Inbox.count)
|
||||
@conversations_count = number_with_delimiter(Conversation.count)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
class Survey::ResponsesController < ActionController::Base
|
||||
before_action :set_global_config
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@ class SwaggerController < ApplicationController
|
||||
if Rails.env.development? || Rails.env.test?
|
||||
render inline: File.read(Rails.root.join('swagger', derived_path))
|
||||
else
|
||||
head 404
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class Webhooks::InstagramController < ApplicationController
|
||||
skip_before_action :authenticate_user!, raise: false
|
||||
skip_before_action :set_current_user
|
||||
|
||||
def verify
|
||||
if valid_instagram_token?(params['hub.verify_token'])
|
||||
Rails.logger.info('Instagram webhook verified')
|
||||
render json: params['hub.challenge']
|
||||
else
|
||||
render json: { error: 'Error; wrong verify token', status: 403 }
|
||||
end
|
||||
end
|
||||
|
||||
def events
|
||||
Rails.logger.info('Instagram webhook received events')
|
||||
if params['object'].casecmp('instagram').zero?
|
||||
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
||||
render json: :ok
|
||||
else
|
||||
Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_instagram_token?(token)
|
||||
token == ENV['IG_VERIFY_TOKEN']
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class Webhooks::LineController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::LineEventsJob.perform_later(params: params.to_unsafe_hash, signature: request.headers['x-line-signature'], post_body: request.raw_post)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class Webhooks::TelegramController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::TelegramEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class Webhooks::WhatsappController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,4 @@
|
||||
# TODO : Delete this and associated spec once 'api/widget/config' end point is merged
|
||||
class WidgetsController < ActionController::Base
|
||||
before_action :set_global_config
|
||||
before_action :set_web_widget
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
class MessageDrop < BaseDrop
|
||||
include MessageFormatHelper
|
||||
|
||||
def sender_display_name
|
||||
@obj.sender.try(:available_name)
|
||||
end
|
||||
|
||||
def text_content
|
||||
content = @obj.try(:content)
|
||||
transform_user_mention_content content
|
||||
end
|
||||
end
|
||||
@@ -94,6 +94,8 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def filter_by_status
|
||||
return if params[:status] == 'all'
|
||||
|
||||
@conversations = @conversations.where(status: params[:status] || DEFAULT_STATUS)
|
||||
end
|
||||
|
||||
@@ -114,13 +116,13 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page]
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def conversations
|
||||
@conversations = @conversations.includes(
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
|
||||
)
|
||||
current_page ? @conversations.latest.page(current_page) : @conversations.latest
|
||||
@conversations.latest.page(current_page)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,12 +3,12 @@ module FileTypeHelper
|
||||
return :image if [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/gif',
|
||||
'image/tiff',
|
||||
'image/bmp'
|
||||
].include?(content_type)
|
||||
|
||||
return :video if content_type.include?('video/')
|
||||
return :audio if content_type.include?('audio/')
|
||||
|
||||
:file
|
||||
|
||||
@@ -3,13 +3,20 @@
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
<add-account-modal
|
||||
:show="showAddAccountModal"
|
||||
:has-accounts="hasAccounts"
|
||||
/>
|
||||
<woot-snackbar-box />
|
||||
<network-notification />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import NetworkNotification from './components/NetworkNotification';
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
|
||||
export default {
|
||||
@@ -17,14 +24,37 @@ export default {
|
||||
|
||||
components: {
|
||||
WootSnackbarBox,
|
||||
AddAccountModal,
|
||||
NetworkNotification,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentUser: 'getCurrentUser',
|
||||
}),
|
||||
hasAccounts() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.accounts &&
|
||||
this.currentUser.accounts.length !== 0
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
currentUser() {
|
||||
if (!this.hasAccounts) {
|
||||
this.showAddAccountModal = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setUser');
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class AccountActions extends ApiClient {
|
||||
constructor() {
|
||||
super('actions', { accountScoped: true });
|
||||
}
|
||||
|
||||
merge(parentId, childId) {
|
||||
return axios.post(`${this.url}/contact_merge`, {
|
||||
base_contact_id: parentId,
|
||||
mergee_contact_id: childId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccountActions();
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class AttributeAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('custom_attribute_definitions', { accountScoped: true });
|
||||
}
|
||||
|
||||
getAttributesByModel(modelId) {
|
||||
return axios.get(`${this.url}?attribute_model=${modelId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AttributeAPI();
|
||||
@@ -161,9 +161,9 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
updateAvailability({ availability }) {
|
||||
return axios.put(endPoints('profileUpdate').url, {
|
||||
profile: { availability },
|
||||
updateAvailability(availabilityData) {
|
||||
return axios.post(endPoints('availabilityUpdate').url, {
|
||||
profile: { ...availabilityData },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
export const buildContactParams = (page, sortAttr, label, search) => {
|
||||
let params = `page=${page}&sort=${sortAttr}`;
|
||||
let params = `include_contact_inboxes=false&page=${page}&sort=${sortAttr}`;
|
||||
if (search) {
|
||||
params = `${params}&q=${search}`;
|
||||
}
|
||||
@@ -52,6 +52,14 @@ class ContactAPI extends ApiClient {
|
||||
)}`;
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
importContacts(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('import_file', file);
|
||||
return axios.post(`${this.url}/import`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactAPI();
|
||||
|
||||
@@ -13,6 +13,9 @@ const endPoints = {
|
||||
profileUpdate: {
|
||||
url: '/api/v1/profile',
|
||||
},
|
||||
availabilityUpdate: {
|
||||
url: '/api/v1/profile/availability',
|
||||
},
|
||||
logout: {
|
||||
url: 'auth/sign_out',
|
||||
},
|
||||
|
||||
@@ -28,9 +28,10 @@ class ConversationApi extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
toggleStatus({ conversationId, status }) {
|
||||
toggleStatus({ conversationId, status, snoozedUntil = null }) {
|
||||
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
||||
status,
|
||||
snoozed_until: snoozedUntil,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,39 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
export const buildCreatePayload = ({
|
||||
message,
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}) => {
|
||||
let payload;
|
||||
if (file) {
|
||||
payload = new FormData();
|
||||
payload.append('attachments[]', file, file.name);
|
||||
if (message) {
|
||||
payload.append('content', message);
|
||||
}
|
||||
payload.append('private', isPrivate);
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
payload.append('bcc_emails', bccEmails);
|
||||
} else {
|
||||
payload = {
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
echo_id: echoId,
|
||||
content_attributes: contentAttributes,
|
||||
cc_emails: ccEmails,
|
||||
bcc_emails: bccEmails,
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
class MessageApi extends ApiClient {
|
||||
constructor() {
|
||||
super('conversations', { accountScoped: true });
|
||||
@@ -14,19 +47,21 @@ class MessageApi extends ApiClient {
|
||||
contentAttributes,
|
||||
echo_id: echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
if (file) formData.append('attachments[]', file, file.name);
|
||||
if (message) formData.append('content', message);
|
||||
if (contentAttributes)
|
||||
formData.append('content_attributes', JSON.stringify(contentAttributes));
|
||||
|
||||
formData.append('private', isPrivate);
|
||||
formData.append('echo_id', echoId);
|
||||
return axios({
|
||||
method: 'post',
|
||||
url: `${this.url}/${conversationId}/messages`,
|
||||
data: formData,
|
||||
data: buildCreatePayload({
|
||||
message,
|
||||
isPrivate,
|
||||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ class InboxMembers extends ApiClient {
|
||||
super('inbox_members', { accountScoped: true });
|
||||
}
|
||||
|
||||
create({ inboxId, agentList }) {
|
||||
return axios.post(this.url, {
|
||||
update({ inboxId, agentList }) {
|
||||
return axios.patch(this.url, {
|
||||
inbox_id: inboxId,
|
||||
user_ids: agentList,
|
||||
});
|
||||
|
||||
@@ -13,6 +13,10 @@ class Inboxes extends ApiClient {
|
||||
getCampaigns(inboxId) {
|
||||
return axios.get(`${this.url}/${inboxId}/campaigns`);
|
||||
}
|
||||
|
||||
deleteInboxAvatar(inboxId) {
|
||||
return axios.delete(`${this.url}/${inboxId}/avatar`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
||||
@@ -6,15 +6,15 @@ class ReportsAPI extends ApiClient {
|
||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getAccountReports(metric, since, until) {
|
||||
return axios.get(`${this.url}/account`, {
|
||||
params: { metric, since, until },
|
||||
getReports(metric, since, until, type = 'account', id) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { metric, since, until, type, id },
|
||||
});
|
||||
}
|
||||
|
||||
getAccountSummary(since, until) {
|
||||
return axios.get(`${this.url}/account_summary`, {
|
||||
params: { since, until },
|
||||
getSummary(since, until, type = 'account', id) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: { since, until, type, id },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,24 @@ class ReportsAPI extends ApiClient {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getLabelReports(since, until) {
|
||||
return axios.get(`${this.url}/labels`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getInboxReports(since, until) {
|
||||
return axios.get(`${this.url}/inboxes`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getTeamReports(since, until) {
|
||||
return axios.get(`${this.url}/teams`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportsAPI();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import accountActionsAPI from '../accountActions';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ContactsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(accountActionsAPI).toBeInstanceOf(ApiClient);
|
||||
expect(accountActionsAPI).toHaveProperty('merge');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#merge', () => {
|
||||
accountActionsAPI.merge(1, 2);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/actions/contact_merge',
|
||||
{
|
||||
base_contact_id: 1,
|
||||
mergee_contact_id: 2,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ describe('#ContactsAPI', () => {
|
||||
it('#get', () => {
|
||||
contactAPI.get(1, 'name', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts?page=1&sort=name&labels[]=customer-support'
|
||||
'/api/v1/contacts?include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -56,7 +56,19 @@ describe('#ContactsAPI', () => {
|
||||
it('#search', () => {
|
||||
contactAPI.search('leads', 1, 'date', 'customer-support');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/search?page=1&sort=date&q=leads&labels[]=customer-support'
|
||||
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
it('#importContacts', () => {
|
||||
const file = 'file';
|
||||
contactAPI.importContacts(file);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/import',
|
||||
expect.any(FormData),
|
||||
{
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -64,12 +76,16 @@ describe('#ContactsAPI', () => {
|
||||
|
||||
describe('#buildContactParams', () => {
|
||||
it('returns correct string', () => {
|
||||
expect(buildContactParams(1, 'name', '', '')).toBe('page=1&sort=name');
|
||||
expect(buildContactParams(1, 'name', '', '')).toBe(
|
||||
'include_contact_inboxes=false&page=1&sort=name'
|
||||
);
|
||||
expect(buildContactParams(1, 'name', 'customer-support', '')).toBe(
|
||||
'page=1&sort=name&labels[]=customer-support'
|
||||
'include_contact_inboxes=false&page=1&sort=name&labels[]=customer-support'
|
||||
);
|
||||
expect(
|
||||
buildContactParams(1, 'name', 'customer-support', 'message-content')
|
||||
).toBe('page=1&sort=name&q=message-content&labels[]=customer-support');
|
||||
).toBe(
|
||||
'include_contact_inboxes=false&page=1&sort=name&q=message-content&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('#ConversationAPI', () => {
|
||||
`/api/v1/conversations/12/toggle_status`,
|
||||
{
|
||||
status: 'online',
|
||||
snoozed_until: null,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import messageAPI from '../../inbox/message';
|
||||
import messageAPI, { buildCreatePayload } from '../../inbox/message';
|
||||
import ApiClient from '../../ApiClient';
|
||||
import describeWithAPIMock from '../apiSpecHelper';
|
||||
|
||||
@@ -29,4 +29,34 @@ describe('#ConversationAPI', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('#buildCreatePayload', () => {
|
||||
it('builds form payload if file is available', () => {
|
||||
const formPayload = buildCreatePayload({
|
||||
message: 'test content',
|
||||
echoId: 12,
|
||||
isPrivate: true,
|
||||
file: new Blob(['test-content'], { type: 'application/pdf' }),
|
||||
});
|
||||
expect(formPayload).toBeInstanceOf(FormData);
|
||||
expect(formPayload.get('content')).toEqual('test content');
|
||||
expect(formPayload.get('echo_id')).toEqual('12');
|
||||
expect(formPayload.get('private')).toEqual('true');
|
||||
});
|
||||
|
||||
it('builds object payload if file is not available', () => {
|
||||
expect(
|
||||
buildCreatePayload({
|
||||
message: 'test content',
|
||||
isPrivate: false,
|
||||
echoId: 12,
|
||||
contentAttributes: { in_reply_to: 12 },
|
||||
})
|
||||
).toEqual({
|
||||
content: 'test content',
|
||||
private: false,
|
||||
echo_id: 12,
|
||||
content_attributes: { in_reply_to: 12 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,5 +27,12 @@ describe('#InboxesAPI', () => {
|
||||
'/api/v1/inboxes/2/campaigns'
|
||||
);
|
||||
});
|
||||
|
||||
it('#deleteInboxAvatar', () => {
|
||||
inboxesAPI.deleteInboxAvatar(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/2/avatar'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,37 +11,35 @@ describe('#Reports API', () => {
|
||||
expect(reportsAPI).toHaveProperty('create');
|
||||
expect(reportsAPI).toHaveProperty('update');
|
||||
expect(reportsAPI).toHaveProperty('delete');
|
||||
expect(reportsAPI).toHaveProperty('getAccountReports');
|
||||
expect(reportsAPI).toHaveProperty('getAccountSummary');
|
||||
expect(reportsAPI).toHaveProperty('getReports');
|
||||
expect(reportsAPI).toHaveProperty('getSummary');
|
||||
expect(reportsAPI).toHaveProperty('getAgentReports');
|
||||
expect(reportsAPI).toHaveProperty('getLabelReports');
|
||||
expect(reportsAPI).toHaveProperty('getInboxReports');
|
||||
expect(reportsAPI).toHaveProperty('getTeamReports');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#getAccountReports', () => {
|
||||
reportsAPI.getAccountReports(
|
||||
'conversations_count',
|
||||
1621103400,
|
||||
1621621800
|
||||
);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/account',
|
||||
{
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
reportsAPI.getReports('conversations_count', 1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getAccountSummary', () => {
|
||||
reportsAPI.getAccountSummary(1621103400, 1621621800);
|
||||
reportsAPI.getSummary(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/account_summary',
|
||||
'/api/v2/reports/summary',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -59,5 +57,44 @@ describe('#Reports API', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getLabelReports', () => {
|
||||
reportsAPI.getLabelReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/labels',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getInboxReports', () => {
|
||||
reportsAPI.getInboxReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/inboxes',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getTeamReports', () => {
|
||||
reportsAPI.getTeamReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/teams',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB |
@@ -93,3 +93,17 @@
|
||||
/* .slide-fade-leave-active for <2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.network-notification-fade-enter-active {
|
||||
transition: all .1s $ease-in-sine;
|
||||
}
|
||||
|
||||
.network-notification-fade-leave-active {
|
||||
transition: all .1s $ease-out-sine;
|
||||
}
|
||||
|
||||
.network-notification-fade-enter,
|
||||
.network-notification-fade-leave-to {
|
||||
transform: translateY(-$space-small);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
@import 'shared/assets/stylesheets/font-weights';
|
||||
@import 'shared/assets/stylesheets/shadows';
|
||||
@import 'shared/assets/stylesheets/border-radius';
|
||||
@import 'shared/assets/stylesheets/z-index';
|
||||
|
||||
@import 'variables';
|
||||
|
||||
@@ -13,6 +14,7 @@
|
||||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
@import 'date-picker';
|
||||
@import 'utility-helpers';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
@import '~bourbon/core/bourbon';
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
.multiselect {
|
||||
margin-bottom: var(--space-normal);
|
||||
|
||||
&.multiselect--disabled {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.multiselect--active {
|
||||
>.multiselect__tags {
|
||||
border-color: $color-woot;
|
||||
@@ -204,4 +208,58 @@
|
||||
.multiselect--disabled .multiselect__select {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-wrap--medium {
|
||||
$multiselect-height: 4.8rem;
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
background: var(--white);
|
||||
font-size: var(--font-size-small);
|
||||
height: $multiselect-height;
|
||||
min-height: $multiselect-height;
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
height: $multiselect-height - $space-micro;
|
||||
min-height: $multiselect-height - $space-micro;
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__select {
|
||||
min-height: $multiselect-height;
|
||||
}
|
||||
|
||||
.multiselect--disabled .multiselect__current,
|
||||
.multiselect--disabled .multiselect__select {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
font-weight: var(--font-weight-light);
|
||||
margin-top: var(--space-large);
|
||||
}
|
||||
|
||||
.update-subscription--checkbox {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
line-height: 1.5;
|
||||
margin-right: var(--space-one);
|
||||
margin-top: var(--space-smaller);
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
@@ -20,17 +36,4 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.update-subscription--checkbox {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
line-height: 1.5;
|
||||
margin-right: var(--space-one);
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
}
|
||||
|
||||
&.over {
|
||||
|
||||
&::after {
|
||||
background: $color-woot;
|
||||
}
|
||||
@@ -80,7 +79,7 @@
|
||||
background: $color-woot;
|
||||
}
|
||||
|
||||
&+.item {
|
||||
& + .item {
|
||||
&::before {
|
||||
background: $color-woot;
|
||||
}
|
||||
@@ -132,10 +131,13 @@
|
||||
@include padding($space-medium);
|
||||
@include border-light;
|
||||
@include full-height();
|
||||
|
||||
&.height-auto {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.inoboxes-list {
|
||||
|
||||
.inbox-item {
|
||||
@include margin($space-normal);
|
||||
@include flex;
|
||||
@@ -189,9 +191,9 @@
|
||||
align-self: center;
|
||||
color: $medium-gray;
|
||||
font-size: $font-size-small;
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
transform: translateX(0);
|
||||
transition: opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s;
|
||||
transition: opacity 0.1s ease-in 0s, transform 0.2s ease-in 0.03s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,7 +228,7 @@
|
||||
@include padding($space-medium);
|
||||
}
|
||||
|
||||
>a>img {
|
||||
> a > img {
|
||||
width: $space-larger * 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,6 @@ $resolve-button-width: 13.2rem;
|
||||
margin-right: var(--space-normal);
|
||||
min-width: 0;
|
||||
|
||||
.user--name {
|
||||
@include margin(0);
|
||||
display: inline-block;
|
||||
font-size: $font-size-medium;
|
||||
line-height: 1.3;
|
||||
text-transform: capitalize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user--profile__meta {
|
||||
align-items: flex-start;
|
||||
@@ -59,12 +51,6 @@ $resolve-button-width: 13.2rem;
|
||||
margin-left: $space-slab;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user--profile__button {
|
||||
font-size: $font-size-mini;
|
||||
margin-top: $space-micro;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,11 +90,6 @@
|
||||
font-size: $font-size-mini;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.message-from-agent {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-mini;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation--meta {
|
||||
@@ -149,7 +144,10 @@
|
||||
padding-left: 0;
|
||||
|
||||
.conversation--details {
|
||||
border-radius: var(--border-radius-small);
|
||||
margin-left: 0;
|
||||
padding-left: var(--space-two);
|
||||
padding-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
.conversation-panel {
|
||||
@include flex;
|
||||
@include flex-weight(1);
|
||||
@include flex-weight(1 1 1px);
|
||||
@include margin($zero);
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -113,10 +113,18 @@
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $space-small;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.unread--toast {
|
||||
+.right {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
+.left {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
@include elegant-card;
|
||||
@include round-corner;
|
||||
@@ -140,6 +148,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.left {
|
||||
|
||||
.bubble {
|
||||
@@ -183,6 +192,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
+.unread--toast {
|
||||
+.right {
|
||||
margin-top: $space-one;
|
||||
|
||||
.bubble {
|
||||
border-top-right-radius: $space-one;
|
||||
}
|
||||
}
|
||||
|
||||
+.left {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
@@ -226,6 +248,21 @@
|
||||
border-top-left-radius: $space-one;
|
||||
}
|
||||
}
|
||||
|
||||
+.unread--toast {
|
||||
+.left {
|
||||
margin-top: $space-one;
|
||||
|
||||
.bubble {
|
||||
border-top-left-radius: $space-one;
|
||||
}
|
||||
}
|
||||
|
||||
+.right {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.center {
|
||||
@@ -259,11 +296,11 @@
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
justify-content: center;
|
||||
margin: var(--space-small) var(--space-normal);
|
||||
padding: var(--space-small) var(--space-normal);
|
||||
margin: var(--space-smaller) 0;
|
||||
padding: var(--space-smaller) var(--space-micro) var(--space-smaller) var(--space-one);
|
||||
|
||||
.is-text {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
text-align: start;
|
||||
|
||||
@include breakpoint(xxxlarge up) {
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
@include padding($space-large);
|
||||
}
|
||||
|
||||
form {
|
||||
form,
|
||||
.modal-content {
|
||||
@include padding($space-large);
|
||||
align-self: center;
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.report-bar {
|
||||
@include margin(-1px $zero);
|
||||
@include background-white;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.date-picker {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.margin-left-small {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.reports-option__rounded--item {
|
||||
border-radius: 100%;
|
||||
height: var(--space-two);
|
||||
width: var(--space-two);
|
||||
}
|
||||
|
||||
.reports-option__item {
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.reports-option__label--swatch {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.display-flex {
|
||||
display: flex;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user