Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
480eb3043c | ||
|
|
569ae24d8a | ||
|
|
615a575bdd | ||
|
|
17fb6b8d55 | ||
|
|
e010f0c6f0 | ||
|
|
04e890070d | ||
|
|
0319b78eac | ||
|
|
80e5d6d7a0 | ||
|
|
337a74a10c | ||
|
|
d4be268cc3 | ||
|
|
a6b119d187 | ||
|
|
923b4637db | ||
|
|
b6ad468eb4 | ||
|
|
14503b5fe0 | ||
|
|
c64e2e3bc5 | ||
|
|
31cdc63e18 | ||
|
|
3d164271a8 | ||
|
|
1194188f13 | ||
|
|
9b5eb98c59 | ||
|
|
8622740161 | ||
|
|
fb3ed29c90 | ||
|
|
f1ae09d21b | ||
|
|
b1efcde495 | ||
|
|
bc7bcc20b8 | ||
|
|
7e5ec7925c | ||
|
|
727993aa19 | ||
|
|
d5536d65f7 | ||
|
|
57359be37e | ||
|
|
14e6a5d6b0 | ||
|
|
5ea0436051 | ||
|
|
dfb56f6bb8 | ||
|
|
d4a046a21e | ||
|
|
8482ecc1b1 | ||
|
|
5b490bc8cc | ||
|
|
c2afdd5c93 | ||
|
|
75ce5345a9 | ||
|
|
9329f08e14 | ||
|
|
0b03e4b296 | ||
|
|
821b953ee9 | ||
|
|
7a8aa4ca2e | ||
|
|
2595e774e5 | ||
|
|
9a8a0bd865 | ||
|
|
cd5646b4b9 | ||
|
|
76f53d9a11 | ||
|
|
591d186d66 | ||
|
|
73d8073530 | ||
|
|
1dd0c7249c | ||
|
|
3509692055 | ||
|
|
5d85a21287 | ||
|
|
caee9535f1 | ||
|
|
3813b3b372 | ||
|
|
c397fe1964 | ||
|
|
cb23ff53bf | ||
|
|
3cd1616df6 | ||
|
|
eff3a50316 | ||
|
|
0477123f92 | ||
|
|
c9f821cc5b | ||
|
|
baf697f064 | ||
|
|
5be9380547 | ||
|
|
24b20c10ce | ||
|
|
bfe6324d9a | ||
|
|
15fd37b124 | ||
|
|
3f2ac2042f | ||
|
|
3158966241 | ||
|
|
c674393c02 | ||
|
|
cffc984ef6 | ||
|
|
5e8fd689c9 | ||
|
|
ccf52a620b | ||
|
|
ec0ea0b1dc | ||
|
|
8155024b6a | ||
|
|
823c0ab6a7 | ||
|
|
e8bc30e3c6 | ||
|
|
30ac8054ba | ||
|
|
0ba6e772a4 | ||
|
|
ba0188aefc | ||
|
|
e0f29b9d81 | ||
|
|
971755b845 | ||
|
|
0667d9f016 | ||
|
|
48389d69d8 | ||
|
|
ff881fcad9 | ||
|
|
5da0b0667f | ||
|
|
a8cfcbc168 | ||
|
|
6bfe0f2fc1 | ||
|
|
131c0a8668 | ||
|
|
8e153d6350 | ||
|
|
60a0709de6 | ||
|
|
14c53b8b54 | ||
|
|
7577c9c888 | ||
|
|
0cf970dafd | ||
|
|
c2647a1f27 | ||
|
|
8b9aea231c | ||
|
|
b735135431 | ||
|
|
160e8a8ab4 | ||
|
|
2605f24539 | ||
|
|
dd76dd4204 | ||
|
|
86b4183bde | ||
|
|
4a21633a2b | ||
|
|
f8c46341f6 | ||
|
|
771cb5b9fc | ||
|
|
f43018e017 | ||
|
|
467f3b9191 | ||
|
|
715400f7ab | ||
|
|
7b4cac746a | ||
|
|
6169d172d8 | ||
|
|
e06a6a7c55 | ||
|
|
c608ca37e0 | ||
|
|
6097f4c122 | ||
|
|
8372ae418e | ||
|
|
71391d982b | ||
|
|
b77fad52fb | ||
|
|
40f31cb0a3 | ||
|
|
7f8a199b29 | ||
|
|
23fe8847d8 | ||
|
|
430547e026 | ||
|
|
d0702363f4 | ||
|
|
d6d0c79618 | ||
|
|
9f8e442333 | ||
|
|
4a3ddad02f | ||
|
|
207a03155e | ||
|
|
796a7805db | ||
|
|
a50ea9c88a | ||
|
|
711251560b | ||
|
|
c4837cd7ac | ||
|
|
4b748e2c8c | ||
|
|
8c8c5a77c8 | ||
|
|
02dd5ecfab | ||
|
|
f405572e5c | ||
|
|
25876993ff | ||
|
|
b3ba8b9513 | ||
|
|
4a2452173e | ||
|
|
c62d74a01d | ||
|
|
5edf0f2bbe | ||
|
|
dd1fe4f93a | ||
|
|
e730804b48 | ||
|
|
578414d788 | ||
|
|
619441ce1c | ||
|
|
3c3b6f90c7 | ||
|
|
647efa12e7 | ||
|
|
b3545f42f1 | ||
|
|
fb03b6f434 | ||
|
|
1aa7d6f7d8 | ||
|
|
96b719017b | ||
|
|
b94e67f5d7 | ||
|
|
c76b588850 | ||
|
|
a28b911132 | ||
|
|
6e4ae1a1bc | ||
|
|
9583a2dbad | ||
|
|
11adfd2384 | ||
|
|
28d102f526 | ||
|
|
3d7ca61481 | ||
|
|
7b9e4982cf | ||
|
|
4d458c2184 | ||
|
|
0582d15eb5 | ||
|
|
915fe3897c | ||
|
|
f08d1b35d0 | ||
|
|
a3cb26a317 | ||
|
|
eee89bf0d8 | ||
|
|
1de18391b4 | ||
|
|
bb16780278 | ||
|
|
4260441f8c | ||
|
|
12c0be002e | ||
|
|
aff14b697f | ||
|
|
87a6266ddc | ||
|
|
cebb1bf871 | ||
|
|
ed2772dd59 | ||
|
|
4f19fa763c | ||
|
|
46c1734ba1 | ||
|
|
9b615f11f1 | ||
|
|
4ca66c1195 | ||
|
|
cc74bebc0f | ||
|
|
2c8a3ef3b2 | ||
|
|
6c94768bdb | ||
|
|
dcecbf4b80 | ||
|
|
d590b0ebc4 | ||
|
|
85f19fa25a | ||
|
|
1ca1b4d36b | ||
|
|
9059f5906a | ||
|
|
eb0da2026f | ||
|
|
e348db1e37 | ||
|
|
7ba24b90c4 | ||
|
|
3b015d4dd8 | ||
|
|
ea44a32758 | ||
|
|
80d83b401c | ||
|
|
ea3e011d3f | ||
|
|
2586c8e7e5 | ||
|
|
b9025a98d3 | ||
|
|
b6c3d61163 | ||
|
|
991f291c69 | ||
|
|
5ad6db07b4 | ||
|
|
e345a4486d | ||
|
|
5ad7e93059 | ||
|
|
505c93c467 | ||
|
|
464e12ceb7 | ||
|
|
e6f8895c1b | ||
|
|
a703ef2de6 | ||
|
|
94a473c9f8 | ||
|
|
2591a04c0b | ||
|
|
351a3dc372 | ||
|
|
d5c9193d1a | ||
|
|
de4e4c6f65 | ||
|
|
f34a2e6115 | ||
|
|
9c7044c81b | ||
|
|
04148b95a6 | ||
|
|
3d73d8935a | ||
|
|
edd93ca4a3 | ||
|
|
2c15d5b4f3 | ||
|
|
74d9203868 | ||
|
|
25001689ec | ||
|
|
2da618bd5e | ||
|
|
ba7f387dde | ||
|
|
1115bd18ed | ||
|
|
e9a162e71a | ||
|
|
4201b57315 | ||
|
|
0ec3a56041 | ||
|
|
4e416b4d51 | ||
|
|
4ae9ed8f94 | ||
|
|
7b2ff2f112 | ||
|
|
e2ff1b1a15 | ||
|
|
d891c1e7b5 | ||
|
|
2a07eae69f | ||
|
|
375d943a48 | ||
|
|
4c0f6c1333 | ||
|
|
97675c61ea | ||
|
|
f4c0ce5dfa | ||
|
|
9f37a6e2ba | ||
|
|
047070ad87 | ||
|
|
1a3d39a7f3 | ||
|
|
a737f89c47 | ||
|
|
a7987d4d1c | ||
|
|
32673ea8b4 | ||
|
|
9454c6b14f | ||
|
|
cf10f3d03b | ||
|
|
fba7f40bee | ||
|
|
5c6958482f | ||
|
|
8dcb4a5ed4 | ||
|
|
dbb50e5923 | ||
|
|
903072ef14 | ||
|
|
fc1f257793 | ||
|
|
8821106da9 | ||
|
|
e99ea0b582 | ||
|
|
193a531e49 | ||
|
|
91b9168fae | ||
|
|
1f3c5002b3 | ||
|
|
c0276d252a | ||
|
|
52d1821cd3 | ||
|
|
e0d24e0a73 | ||
|
|
b1e1d7e423 | ||
|
|
a95d249ec1 | ||
|
|
a1034a70c4 | ||
|
|
357a374478 | ||
|
|
1467a8fa33 | ||
|
|
13eaff156a | ||
|
|
e7e46590a4 | ||
|
|
17018d392d | ||
|
|
69eaf3ff7f | ||
|
|
cd6c9a8fe9 | ||
|
|
5710b7559e | ||
|
|
8e6d8e7654 | ||
|
|
59deffc7e3 | ||
|
|
34e8ad9dc5 | ||
|
|
23965fbaa3 | ||
|
|
8cff690640 | ||
|
|
b304f5a826 | ||
|
|
e90d064648 | ||
|
|
504fc24fb3 | ||
|
|
693f2531ab | ||
|
|
e997aaceb7 | ||
|
|
a7c947aeae | ||
|
|
ab864a86fd | ||
|
|
9617137688 | ||
|
|
1b36143d27 | ||
|
|
241e7d0c29 | ||
|
|
185f916b2a | ||
|
|
4a68d13310 | ||
|
|
ceb2b9673c | ||
|
|
1749b4eb61 | ||
|
|
346d89ecc0 | ||
|
|
c64cffb294 | ||
|
|
6deb3764dc | ||
|
|
4398734bdf | ||
|
|
290196d43b | ||
|
|
fcd2b892bf | ||
|
|
cab5a3e53b | ||
|
|
359caf754c | ||
|
|
7fcac532df | ||
|
|
d57be3ffae | ||
|
|
5046dcaf61 | ||
|
|
7df68c6388 | ||
|
|
905fd62974 | ||
|
|
1c99294c8c | ||
|
|
94209d29cb | ||
|
|
9c31d7c672 | ||
|
|
991a42c417 | ||
|
|
4a902fce6d | ||
|
|
5475746a87 | ||
|
|
acba07cf6e | ||
|
|
f44be0b1e6 | ||
|
|
bec1266242 | ||
|
|
ebb04487a8 | ||
|
|
de37975be4 | ||
|
|
8a26d78623 | ||
|
|
7ee7062843 | ||
|
|
a0884310f4 | ||
|
|
9a9462f5cb | ||
|
|
9b6f1df871 | ||
|
|
ab4d63a006 | ||
|
|
0745ceb430 | ||
|
|
f7ed7756d9 | ||
|
|
e1fe4f09bc | ||
|
|
a61dae0bb2 | ||
|
|
fade94658a | ||
|
|
e775d22b98 | ||
|
|
e304ad27cb | ||
|
|
3e8ae3865f | ||
|
|
223fb2b7e4 | ||
|
|
666028a443 | ||
|
|
8bdd7ba3e4 | ||
|
|
a1c77c8c4c | ||
|
|
577e9905c6 | ||
|
|
9606abe251 | ||
|
|
28da30f7e4 | ||
|
|
9f663ed4e5 | ||
|
|
acc55ea79c | ||
|
|
13d6734977 | ||
|
|
c57c975a0d | ||
|
|
44486fc8e1 | ||
|
|
009abc1948 | ||
|
|
262474166f | ||
|
|
307118b235 | ||
|
|
0130e08016 | ||
|
|
83655f4ca4 | ||
|
|
6fe5484119 | ||
|
|
76e8acd3c6 | ||
|
|
ad09c5dc5f | ||
|
|
f8cf0cd7c5 | ||
|
|
46d2a8f218 | ||
|
|
6069cd9eba | ||
|
|
8d65608666 | ||
|
|
5ee209c079 | ||
|
|
e0c9687f5e | ||
|
|
93f384a3ec | ||
|
|
9984edd3ef | ||
|
|
e2e459a1ac | ||
|
|
b7336eca65 | ||
|
|
6ae06f2c7a | ||
|
|
7986d61b6f | ||
|
|
c2191e48b1 | ||
|
|
ac372f188d | ||
|
|
178cb34319 | ||
|
|
c2519ea1ea | ||
|
|
dcdeaa7954 | ||
|
|
0b7eb2e2c1 | ||
|
|
2defe4a8fe | ||
|
|
b0af3f2498 | ||
|
|
5feb5aba72 | ||
|
|
a7623041bf | ||
|
|
d268f155e4 | ||
|
|
b1eea7f7d1 | ||
|
|
2f63ebb8a6 | ||
|
|
2be71e73dc | ||
|
|
1db82f235d | ||
|
|
9306b725d8 | ||
|
|
3ee6b7d6e9 | ||
|
|
35130bc83e | ||
|
|
f609a28c6b | ||
|
|
714c10dc66 | ||
|
|
f5d79ffe58 | ||
|
|
824101bc30 | ||
|
|
46afcd9348 | ||
|
|
6b025816f5 | ||
|
|
3c72a2759f | ||
|
|
a98f10130b | ||
|
|
b46ee5e580 | ||
|
|
6d378eb206 | ||
|
|
d7cfe6858e | ||
|
|
1c29f5bbe4 | ||
|
|
8b4134c790 | ||
|
|
b826319776 | ||
|
|
025bbf1bfb | ||
|
|
2a5932cf1b | ||
|
|
0a18993b70 | ||
|
|
b01d032d0d | ||
|
|
c792cfc0be | ||
|
|
98be3b33c1 | ||
|
|
0115b4ddc4 | ||
|
|
d5c30760a7 | ||
|
|
0899f62912 | ||
|
|
86dfdfb9ab | ||
|
|
bfcde9b022 | ||
|
|
6a98a812e7 | ||
|
|
a4ee8ee239 | ||
|
|
89f9f39e6e | ||
|
|
b326da7d94 | ||
|
|
add004a56e | ||
|
|
55843c5bad | ||
|
|
2c3c1888f1 | ||
|
|
b0370ce44d | ||
|
|
3a48e08fe0 | ||
|
|
c23e2c23d4 | ||
|
|
330a97b5c5 | ||
|
|
00143f7ee5 | ||
|
|
da8f9d0337 | ||
|
|
0530e9491c | ||
|
|
11cd7fd6c5 | ||
|
|
9103fbbdde | ||
|
|
e182b59002 | ||
|
|
7f8204a9da | ||
|
|
0033a35ab8 | ||
|
|
5db9d397c5 | ||
|
|
791d90c6b7 | ||
|
|
1a8b8f0900 | ||
|
|
1be7c2d72c | ||
|
|
e93e43532a | ||
|
|
c332e2df12 | ||
|
|
da163cc1f7 | ||
|
|
7b44bf5b78 | ||
|
|
24e6a92297 | ||
|
|
8384d0b38e | ||
|
|
c2333214af | ||
|
|
bf96e8b68d | ||
|
|
a3ba8f9e35 | ||
|
|
c2b4991fd8 | ||
|
|
b81a9f2010 | ||
|
|
4eeaadbd5b | ||
|
|
e500d1216b | ||
|
|
d227a56747 | ||
|
|
fec4a29081 | ||
|
|
9ea42ebff2 | ||
|
|
e5a80195ab | ||
|
|
928580dd41 |
+16
-4
@@ -7,13 +7,13 @@ defaults: &defaults
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: cimg/ruby:3.0.2-node
|
||||
- image: cimg/ruby:3.0.2-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||
- image: circleci/postgres:alpine
|
||||
- image: circleci/redis:alpine
|
||||
- image: cimg/postgres:14.1
|
||||
- image: cimg/redis:6.2.6
|
||||
environment:
|
||||
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
|
||||
- RAILS_LOG_TO_STDOUT: false
|
||||
@@ -77,6 +77,18 @@ jobs:
|
||||
paths:
|
||||
- cc-test-reporter
|
||||
|
||||
# verify swagger specification
|
||||
- run:
|
||||
name: Verify swagger API specification
|
||||
command: |
|
||||
bundle exec rake swagger:build
|
||||
if [[ `git status swagger/swagger.json --porcelain` ]]
|
||||
then
|
||||
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
||||
exit 1
|
||||
fi
|
||||
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.3.0/openapi-generator-cli-5.3.0.jar > ~/tmp/openapi-generator-cli-5.3.0.jar
|
||||
java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json
|
||||
# Database setup
|
||||
- run: yarn install --check-files
|
||||
- run: bundle exec rake db:create
|
||||
@@ -98,7 +110,7 @@ jobs:
|
||||
- run:
|
||||
name: Run backend tests
|
||||
command: |
|
||||
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
|
||||
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 --format documentation
|
||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||
- persist_to_workspace:
|
||||
root: ~/tmp
|
||||
|
||||
+32
-19
@@ -1,4 +1,4 @@
|
||||
version: "2"
|
||||
version: '2'
|
||||
plugins:
|
||||
rubocop:
|
||||
enabled: false
|
||||
@@ -17,26 +17,39 @@ checks:
|
||||
method-count:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 30
|
||||
threshold: 32
|
||||
file-lines:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 300
|
||||
method-lines:
|
||||
config:
|
||||
threshold: 50
|
||||
exclude_patterns:
|
||||
- "spec/"
|
||||
- "**/specs/"
|
||||
- "db/*"
|
||||
- "bin/**/*"
|
||||
- "db/**/*"
|
||||
- "config/**/*"
|
||||
- "public/**/*"
|
||||
- "vendor/**/*"
|
||||
- "node_modules/**/*"
|
||||
- "lib/tasks/auto_annotate_models.rake"
|
||||
- "app/test-matchers.js"
|
||||
- "docs/*"
|
||||
- "**/*.md"
|
||||
- "**/*.yml"
|
||||
- "app/javascript/dashboard/i18n/locale"
|
||||
- "**/*.stories.js"
|
||||
- "stories/"
|
||||
- 'spec/'
|
||||
- '**/specs/'
|
||||
- 'db/*'
|
||||
- 'bin/**/*'
|
||||
- 'db/**/*'
|
||||
- 'config/**/*'
|
||||
- 'public/**/*'
|
||||
- 'vendor/**/*'
|
||||
- 'node_modules/**/*'
|
||||
- 'lib/tasks/auto_annotate_models.rake'
|
||||
- 'app/test-matchers.js'
|
||||
- 'docs/*'
|
||||
- '**/*.md'
|
||||
- '**/*.yml'
|
||||
- 'app/javascript/dashboard/i18n/locale'
|
||||
- '**/*.stories.js'
|
||||
- 'stories/'
|
||||
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js'
|
||||
- 'app/javascript/shared/constants/countries.js'
|
||||
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
|
||||
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
|
||||
- 'app/javascript/dashboard/i18n/index.js'
|
||||
- 'app/javascript/widget/i18n/index.js'
|
||||
- 'app/javascript/survey/i18n/index.js'
|
||||
|
||||
@@ -32,6 +32,11 @@ REDIS_SENTINELS=
|
||||
# You can find list of master using "SENTINEL masters" command
|
||||
REDIS_SENTINEL_MASTER_NAME=
|
||||
|
||||
# Redis premium breakage in heroku fix
|
||||
# enable the following configuration
|
||||
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||
# REDIS_OPENSSL_VERIFY_MODE=none
|
||||
|
||||
# Postgres Database config variables
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_USERNAME=postgres
|
||||
@@ -57,6 +62,9 @@ SMTP_AUTHENTICATION=
|
||||
SMTP_ENABLE_STARTTLS_AUTO=true
|
||||
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
||||
SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
# Comment out the following environment variables if required by your SMTP server
|
||||
# SMTP_TLS=
|
||||
# SMTP_SSL=
|
||||
|
||||
# Mail Incoming
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
|
||||
+3
-2
@@ -28,8 +28,9 @@ module.exports = {
|
||||
}],
|
||||
'vue/html-self-closing': 'off',
|
||||
"vue/no-v-html": 'off',
|
||||
'import/extensions': ['off']
|
||||
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'import/extensions': ['off'],
|
||||
'no-console': 'error'
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# #
|
||||
# # This action will publish Chatwoot CE docker image.
|
||||
# # This is set to run against merges to develop, master
|
||||
# # and when tags are created.
|
||||
# #
|
||||
|
||||
name: Publish Chatwoot CE docker images
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Set Chatwoot edition
|
||||
run: |
|
||||
echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile
|
||||
|
||||
- name: set docker tag
|
||||
run: |
|
||||
echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV
|
||||
|
||||
- name: replace docker tag if master
|
||||
if: github.ref_name == 'master'
|
||||
run: |
|
||||
echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_TAG }}
|
||||
@@ -0,0 +1,72 @@
|
||||
# #
|
||||
# # This action will strip the enterprise folder
|
||||
# # and run the spec.
|
||||
# # This is set to run against every PR.
|
||||
# #
|
||||
|
||||
name: Run Chatwoot CE spec
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:10.8
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ""
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
# tmpfs makes DB faster by using RAM
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.0.2 # Not needed with a .ruby-version file
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
|
||||
- name: Seed database
|
||||
run: bundle exec rake db:schema:load
|
||||
|
||||
- name: yarn check-files
|
||||
run: yarn install --check-files
|
||||
|
||||
# Run rails tests
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
npm run eslint
|
||||
bundle exec rubocop -a
|
||||
git add
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
sh bin/validate_push
|
||||
+17
-1
@@ -11,10 +11,13 @@ Metrics/ClassLength:
|
||||
Max: 125
|
||||
Exclude:
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/mailers/conversation_reply_mailer.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -25,6 +28,8 @@ Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
Style/OpenStructUse:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
@@ -57,7 +62,7 @@ Metrics/BlockLength:
|
||||
- db/schema.rb
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- lib/woot_message_seeder.rb
|
||||
- lib/seeders/message_seeder.rb
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||
@@ -67,14 +72,20 @@ Rails/ApplicationController:
|
||||
- 'app/controllers/platform_controller.rb'
|
||||
- 'app/controllers/public_controller.rb'
|
||||
- 'app/controllers/survey/responses_controller.rb'
|
||||
Rails/CompactBlank:
|
||||
Enabled: false
|
||||
Rails/EnvironmentVariableAccess:
|
||||
Enabled: false
|
||||
Rails/TimeZoneAssignment:
|
||||
Enabled: false
|
||||
Rails/RedundantPresenceValidationOnBelongsTo:
|
||||
Enabled: false
|
||||
Style/ClassAndModuleChildren:
|
||||
EnforcedStyle: compact
|
||||
Exclude:
|
||||
- 'config/application.rb'
|
||||
Style/MapToHash:
|
||||
Enabled: false
|
||||
RSpec/NestedGroups:
|
||||
Enabled: true
|
||||
Max: 4
|
||||
@@ -82,11 +93,14 @@ RSpec/MessageSpies:
|
||||
Enabled: false
|
||||
RSpec/StubbedMock:
|
||||
Enabled: false
|
||||
RSpec/FactoryBot/SyntaxMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Exclude:
|
||||
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
|
||||
@@ -102,6 +116,7 @@ Metrics/AbcSize:
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
|
||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
Exclude:
|
||||
@@ -116,6 +131,7 @@ Rails/ReversibleMigration:
|
||||
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
||||
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
|
||||
Rails/BulkChangeTable:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
hello@chatwoot.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Contributing to Chatwoot
|
||||
|
||||
Thanks for taking the time to contribute! :tada::+1:
|
||||
|
||||
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions on how to contribute.
|
||||
@@ -42,7 +42,7 @@ gem 'down', '~> 5.0'
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'image_processing'
|
||||
gem 'image_processing', '~> 1.12.2'
|
||||
|
||||
##-- gems for database --#
|
||||
gem 'groupdate'
|
||||
@@ -102,7 +102,7 @@ gem 'sentry-ruby'
|
||||
gem 'sentry-sidekiq'
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq', '~> 6.4.0'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron'
|
||||
|
||||
@@ -121,6 +121,13 @@ gem 'hairtrigger'
|
||||
|
||||
gem 'procore-sift'
|
||||
|
||||
# parse email
|
||||
gem 'email_reply_trimmer'
|
||||
gem 'html2text'
|
||||
|
||||
# to calculate working hours
|
||||
gem 'working_hours'
|
||||
|
||||
group :production, :staging do
|
||||
# we dont want request timing out in development while using byebug
|
||||
gem 'rack-timeout'
|
||||
|
||||
+221
-211
@@ -9,73 +9,73 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
actioncable (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
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)
|
||||
actionmailbox (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
mail (>= 2.7.1)
|
||||
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)
|
||||
actionmailer (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.4.1)
|
||||
actionview (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
actionpack (6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
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.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activestorage (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
actiontext (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
actionview (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
activejob (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
globalid (>= 0.3.6)
|
||||
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.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activejob (= 6.1.4.1)
|
||||
activerecord (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
activemodel (6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activerecord (6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
activerecord-import (1.3.0)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
marcel (~> 1.0.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4.1)
|
||||
activesupport (6.1.4.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
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)
|
||||
acts-as-taggable-on (9.0.1)
|
||||
activerecord (>= 6.0, < 7.1)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.16.0)
|
||||
administrate (0.17.0)
|
||||
actionpack (>= 5.0)
|
||||
actionview (>= 5.0)
|
||||
activerecord (>= 5.0)
|
||||
@@ -85,46 +85,46 @@ GEM
|
||||
momentjs-rails (~> 2.8)
|
||||
sassc-rails (~> 2.1)
|
||||
selectize-rails (~> 0.6)
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_extras (6.2.4)
|
||||
attr_extras (6.2.5)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.513.0)
|
||||
aws-sdk-core (3.121.1)
|
||||
aws-partitions (1.556.0)
|
||||
aws-sdk-core (3.126.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.49.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-kms (1.54.0)
|
||||
aws-sdk-core (~> 3, >= 3.126.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.103.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-s3 (1.112.0)
|
||||
aws-sdk-core (~> 3, >= 3.126.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.1)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
azure-storage-common (2.0.2)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
azure-storage-common (2.0.4)
|
||||
faraday (~> 1.0)
|
||||
faraday_middleware (~> 1.0.0.rc1)
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.9.1)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (5.1.1)
|
||||
bootsnap (1.10.3)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (5.2.1)
|
||||
browser (5.3.1)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.5)
|
||||
bullet (7.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
@@ -135,13 +135,13 @@ GEM
|
||||
byebug (11.1.3)
|
||||
climate_control (1.0.1)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.2)
|
||||
commonmarker (0.23.4)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.11.0)
|
||||
cypress-on-rails (1.12.1)
|
||||
rack
|
||||
database_cleaner (2.0.1)
|
||||
database_cleaner-active_record (~> 2.0.0)
|
||||
@@ -151,11 +151,12 @@ GEM
|
||||
database_cleaner-core (2.0.1)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
ddtrace (0.53.0)
|
||||
ffi (~> 1.0)
|
||||
ddtrace (0.54.2)
|
||||
debase-ruby_core_source (<= 0.10.14)
|
||||
msgpack
|
||||
debase-ruby_core_source (0.10.14)
|
||||
declarative (0.0.20)
|
||||
devise (4.8.0)
|
||||
devise (4.8.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
@@ -165,7 +166,7 @@ GEM
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
rails (>= 4.2.0, < 6.2)
|
||||
diff-lcs (1.4.4)
|
||||
diff-lcs (1.5.0)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
@@ -175,13 +176,14 @@ GEM
|
||||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
down (5.2.4)
|
||||
down (5.3.0)
|
||||
addressable (~> 2.8)
|
||||
ecma-re-validator (0.3.0)
|
||||
regexp_parser (~> 2.0)
|
||||
ecma-re-validator (0.4.0)
|
||||
regexp_parser (~> 2.2)
|
||||
email_reply_trimmer (0.1.13)
|
||||
erubi (1.10.0)
|
||||
erubis (2.7.0)
|
||||
et-orbi (1.2.5)
|
||||
et-orbi (1.2.6)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
@@ -196,11 +198,11 @@ GEM
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fcm (1.0.3)
|
||||
fcm (1.0.5)
|
||||
faraday (~> 1)
|
||||
ffi (1.15.4)
|
||||
ffi (1.15.5)
|
||||
flag_shih_tzu (0.3.23)
|
||||
flay (2.12.1)
|
||||
erubis (~> 2.7.0)
|
||||
@@ -217,11 +219,11 @@ GEM
|
||||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.7.0)
|
||||
gli (2.20.1)
|
||||
globalid (0.5.2)
|
||||
geocoder (1.7.3)
|
||||
gli (2.21.0)
|
||||
globalid (1.0.0)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.4.1)
|
||||
google-apis-core (0.4.2)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -230,9 +232,9 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.7.0)
|
||||
google-apis-iamcredentials_v1 (0.10.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.8.0)
|
||||
google-apis-storage_v1 (0.11.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
@@ -246,22 +248,22 @@ GEM
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.34.1)
|
||||
addressable (~> 2.5)
|
||||
google-cloud-storage (1.36.1)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
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.18.1)
|
||||
google-protobuf (3.18.1-universal-darwin)
|
||||
google-protobuf (3.18.1-x86_64-linux)
|
||||
google-protobuf (3.19.4)
|
||||
google-protobuf (3.19.4-x86_64-darwin)
|
||||
google-protobuf (3.19.4-x86_64-linux)
|
||||
googleapis-common-protos (1.3.12)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.2.0)
|
||||
googleapis-common-protos-types (1.3.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.17.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
@@ -270,26 +272,28 @@ GEM
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.15)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.41.0)
|
||||
google-protobuf (~> 3.17)
|
||||
groupdate (6.0.1)
|
||||
activesupport (>= 5.2)
|
||||
grpc (1.43.1)
|
||||
google-protobuf (~> 3.18)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.41.0-universal-darwin)
|
||||
google-protobuf (~> 3.17)
|
||||
grpc (1.43.1-universal-darwin)
|
||||
google-protobuf (~> 3.18)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.41.0-x86_64-linux)
|
||||
google-protobuf (~> 3.17)
|
||||
grpc (1.43.1-x86_64-linux)
|
||||
google-protobuf (~> 3.18)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (0.2.24)
|
||||
activerecord (>= 5.0, < 7)
|
||||
hairtrigger (0.2.25)
|
||||
activerecord (>= 5.0, < 8)
|
||||
ruby2ruby (~> 2.4)
|
||||
ruby_parser (~> 3.10)
|
||||
hana (1.3.7)
|
||||
hashdiff (1.0.1)
|
||||
hashie (4.1.0)
|
||||
hashie (5.0.0)
|
||||
hkdf (0.3.0)
|
||||
html2text (0.2.1)
|
||||
nokogiri (~> 1.6)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
@@ -297,53 +301,55 @@ GEM
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.10)
|
||||
i18n (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.1)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
jbuilder (2.11.2)
|
||||
jbuilder (2.11.5)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.4.0)
|
||||
jmespath (1.6.0)
|
||||
jquery-rails (4.4.0)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.5.1)
|
||||
json_refs (0.1.6)
|
||||
json (2.6.1)
|
||||
json_refs (0.1.7)
|
||||
hana
|
||||
json_schemer (0.2.18)
|
||||
json_schemer (0.2.19)
|
||||
ecma-re-validator (~> 0.3)
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.3.0)
|
||||
kaminari (1.2.1)
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
kaminari-activerecord (= 1.2.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-actionview (1.2.1)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
kaminari-activerecord (= 1.2.2)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-actionview (1.2.2)
|
||||
actionview
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-activerecord (1.2.1)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-activerecord (1.2.2)
|
||||
activerecord
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-core (1.2.1)
|
||||
koala (3.0.0)
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
koala (3.1.0)
|
||||
addressable
|
||||
faraday
|
||||
faraday (< 2)
|
||||
json (>= 1.8)
|
||||
rexml
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
line-bot-api (1.22.0)
|
||||
line-bot-api (1.23.0)
|
||||
liquid (5.1.0)
|
||||
listen (3.7.0)
|
||||
listen (3.7.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.12.0)
|
||||
loofah (2.14.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@@ -352,43 +358,43 @@ GEM
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0901)
|
||||
mime-types-data (3.2022.0105)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.5.3)
|
||||
minitest (5.14.4)
|
||||
mock_redis (0.29.0)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.15.0)
|
||||
mock_redis (0.30.0)
|
||||
ruby2_keywords
|
||||
momentjs-rails (2.20.1)
|
||||
momentjs-rails (2.29.1.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.4.2)
|
||||
msgpack (1.4.5)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
newrelic_rpm (8.0.0)
|
||||
newrelic_rpm (8.4.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.11.7)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
nokogiri (1.13.4)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-arm64-darwin)
|
||||
nokogiri (1.13.4-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
nokogiri (1.13.4-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-linux)
|
||||
nokogiri (1.13.4-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
oauth (0.5.8)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.1)
|
||||
os (1.1.4)
|
||||
parallel (1.21.0)
|
||||
parser (3.0.2.0)
|
||||
parser (3.1.1.0)
|
||||
ast (~> 2.4.1)
|
||||
path_expander (1.1.0)
|
||||
pg (1.2.3)
|
||||
pg (1.3.2)
|
||||
procore-sift (0.16.0)
|
||||
rails (> 4.2.0)
|
||||
pry (0.14.1)
|
||||
@@ -397,57 +403,57 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.5.2)
|
||||
puma (5.6.4)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.1)
|
||||
pundit (2.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.5.2)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3)
|
||||
rack-attack (6.5.0)
|
||||
rack-attack (6.6.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-proxy (0.7.0)
|
||||
rack-proxy (0.7.2)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.6.0)
|
||||
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)
|
||||
rails (6.1.4.7)
|
||||
actioncable (= 6.1.4.7)
|
||||
actionmailbox (= 6.1.4.7)
|
||||
actionmailer (= 6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
actiontext (= 6.1.4.7)
|
||||
actionview (= 6.1.4.7)
|
||||
activejob (= 6.1.4.7)
|
||||
activemodel (= 6.1.4.7)
|
||||
activerecord (= 6.1.4.7)
|
||||
activestorage (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4.1)
|
||||
railties (= 6.1.4.7)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
activesupport (= 6.1.4.1)
|
||||
railties (6.1.4.7)
|
||||
actionpack (= 6.1.4.7)
|
||||
activesupport (= 6.1.4.7)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
rainbow (3.0.0)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-fsevent (0.11.1)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (4.4.0)
|
||||
redis (4.6.0)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
regexp_parser (2.2.1)
|
||||
representable (3.1.1)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
@@ -462,19 +468,19 @@ GEM
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
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)
|
||||
rspec (3.11.0)
|
||||
rspec-core (~> 3.11.0)
|
||||
rspec-expectations (~> 3.11.0)
|
||||
rspec-mocks (~> 3.11.0)
|
||||
rspec-core (3.11.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-expectations (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-mocks (3.10.2)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-mocks (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.2)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-rails (5.0.3)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
@@ -482,36 +488,36 @@ GEM
|
||||
rspec-expectations (~> 3.10)
|
||||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rubocop (1.22.1)
|
||||
rspec-support (3.11.0)
|
||||
rubocop (1.25.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
parser (>= 3.1.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
rubocop-ast (>= 1.15.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.12.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-performance (1.11.5)
|
||||
rubocop-ast (1.16.0)
|
||||
parser (>= 3.1.1.0)
|
||||
rubocop-performance (1.13.2)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.12.3)
|
||||
rubocop-rails (2.13.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.5.0)
|
||||
rubocop-rspec (2.8.0)
|
||||
rubocop (~> 1.19)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-vips (2.1.3)
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_parser (3.17.0)
|
||||
sexp_processor (~> 4.15, >= 4.15.1)
|
||||
ruby_parser (3.18.1)
|
||||
sexp_processor (~> 4.16)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
@@ -520,30 +526,28 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (4.1.2)
|
||||
scout_apm (5.1.1)
|
||||
parser
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (4.7.3)
|
||||
sentry-rails (5.1.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sentry-ruby (4.7.3)
|
||||
sentry-ruby-core (~> 5.1.0)
|
||||
sentry-ruby (5.1.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
faraday (>= 1.0)
|
||||
sentry-ruby-core (= 4.7.3)
|
||||
sentry-ruby-core (4.7.3)
|
||||
sentry-ruby-core (= 5.1.0)
|
||||
sentry-ruby-core (5.1.0)
|
||||
concurrent-ruby
|
||||
faraday
|
||||
sentry-sidekiq (4.7.3)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sentry-sidekiq (5.1.0)
|
||||
sentry-ruby-core (~> 5.1.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.15.3)
|
||||
shoulda-matchers (5.0.0)
|
||||
sexp_processor (4.16.0)
|
||||
shoulda-matchers (5.1.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.2.2)
|
||||
sidekiq (6.4.1)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
@@ -560,7 +564,7 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (0.17.0)
|
||||
slack-ruby-client (1.0.0)
|
||||
faraday (>= 1.0)
|
||||
faraday_middleware
|
||||
gli
|
||||
@@ -570,22 +574,22 @@ GEM
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 1.2, < 3.0)
|
||||
sprockets (4.0.2)
|
||||
sprockets (4.0.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.2)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets-rails (3.4.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
squasher (0.6.2)
|
||||
statsd-ruby (1.5.0)
|
||||
telephone_number (1.4.12)
|
||||
thor (1.1.0)
|
||||
telephone_number (1.4.13)
|
||||
thor (1.2.1)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
trailblazer-option (0.1.1)
|
||||
trailblazer-option (0.1.2)
|
||||
twilio-ruby (5.32.0)
|
||||
faraday (~> 1.0.0)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
@@ -594,7 +598,7 @@ GEM
|
||||
oauth
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.3)
|
||||
tzinfo-data (1.2021.5)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
@@ -605,12 +609,12 @@ GEM
|
||||
unicode-display_width (2.1.0)
|
||||
uniform_notifier (1.14.2)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (4.0.0)
|
||||
valid_email2 (4.0.3)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.1.0)
|
||||
web-console (4.2.0)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
@@ -632,7 +636,10 @@ GEM
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.4.2)
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -668,6 +675,7 @@ DEPENDENCIES
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
down (~> 5.0)
|
||||
email_reply_trimmer
|
||||
facebook-messenger
|
||||
factory_bot_rails
|
||||
faker
|
||||
@@ -682,7 +690,8 @@ DEPENDENCIES
|
||||
haikunator
|
||||
hairtrigger
|
||||
hashie
|
||||
image_processing
|
||||
html2text
|
||||
image_processing (~> 1.12.2)
|
||||
jbuilder
|
||||
json_refs
|
||||
json_schemer
|
||||
@@ -721,7 +730,7 @@ DEPENDENCIES
|
||||
sentry-ruby
|
||||
sentry-sidekiq
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq (~> 6.4.0)
|
||||
sidekiq-cron
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client
|
||||
@@ -740,9 +749,10 @@ DEPENDENCIES
|
||||
webpacker (~> 5.x)
|
||||
webpush
|
||||
wisper (= 2.0.0)
|
||||
working_hours
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.0.2p107
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.25
|
||||
2.3.8
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017-2021 Chatwoot Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under the "enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "enterprise/LICENSE".
|
||||
* All third party components incorporated into the Chatwoot Software are licensed under the original license provided by the owner of the applicable component.
|
||||
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
||||
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -17,7 +20,6 @@ ___
|
||||
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/cloud/build/chatwoot/chatwoot" alt="Docker Build Badge"></a>
|
||||
<img src="https://img.shields.io/github/license/chatwoot/chatwoot" alt="License">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
|
||||
<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>
|
||||
@@ -26,34 +28,39 @@ ___
|
||||
<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"/>
|
||||
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
Chatwoot is an open-source omnichannel customer support software. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it open-source, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
|
||||
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community.
|
||||
|
||||
|
||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
||||
|
||||
## Features
|
||||
|
||||
Chatwoot gives an integrated view of conversations happening in different communication channels.
|
||||
|
||||
It supports the following conversation channels:
|
||||
Chatwoot supports the following conversation channels:
|
||||
|
||||
- **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
|
||||
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
|
||||
- **Instagram**: Connect your Instagram profile and start replying to the direct messages.
|
||||
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
|
||||
- **Whatsapp**: Connect your Whatsapp business account and manage the conversation in Chatwoot
|
||||
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot
|
||||
- **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard.
|
||||
- **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot.
|
||||
- **Line**: Connect your Line account and manage the conversations in Chatwoot.
|
||||
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot.
|
||||
- **API Channel**: Build custom communication channels using our API channel.
|
||||
- **Email (beta)**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
|
||||
- **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
|
||||
|
||||
And more.
|
||||
|
||||
Other features include:
|
||||
|
||||
- **Multi-brand inboxes**: Manage multiple brands or pages using a single dashboard.
|
||||
- **Private notes**: Inter team communication is possible using private notes in a conversation.
|
||||
- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes.
|
||||
- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow.
|
||||
- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox.
|
||||
- **Private notes**: Use @mentions and private notes to communicate internally about a conversation.
|
||||
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
|
||||
- **Conversation Labels**: Use conversation labelling to create custom workflows.
|
||||
- **Conversation Labels**: Use conversation labels to create custom workflows.
|
||||
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
|
||||
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot would send an email to the customer under the agent name so that the user can continue the conversation over the email.
|
||||
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email.
|
||||
- **Multi-lingual support**: Chatwoot supports 10+ languages.
|
||||
- **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.
|
||||
@@ -81,9 +88,18 @@ 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.
|
||||
|
||||
|
||||
### DigitalOcean 1-Click Kubernetes deployment
|
||||
|
||||
Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app.
|
||||
|
||||
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
||||
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
||||
</a>
|
||||
|
||||
### Other deployment options
|
||||
|
||||
Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover.
|
||||
For other supported options, checkout our [deployment page](https://chatwoot.com/deploy).
|
||||
|
||||
## Security
|
||||
|
||||
@@ -102,4 +118,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
|
||||
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
|
||||
*Chatwoot* © 2017-2021, Chatwoot Inc - Released under the MIT License.
|
||||
*Chatwoot* © 2017-2022, Chatwoot Inc - Released under the MIT License.
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
"INSTALLATION_ENV": {
|
||||
"description": "Installation method used for Chatwoot.",
|
||||
"value": "heroku"
|
||||
},
|
||||
"REDIS_OPENSSL_VERIFY_MODE":{
|
||||
"description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues",
|
||||
"value": "none"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
||||
@@ -52,12 +52,11 @@ class ContactIdentifyAction
|
||||
end
|
||||
|
||||
def update_contact
|
||||
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, :phone_number).reject do |_k, v|
|
||||
v.blank?
|
||||
end.merge({ custom_attributes: custom_attributes }))
|
||||
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes }))
|
||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
@@ -68,4 +67,16 @@ class ContactIdentifyAction
|
||||
mergee_contact: merge_contact
|
||||
).perform
|
||||
end
|
||||
|
||||
def custom_attributes
|
||||
params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes
|
||||
end
|
||||
|
||||
def additional_attributes
|
||||
if params[:additional_attributes]
|
||||
@contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys)
|
||||
else
|
||||
@contact.additional_attributes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,11 +48,10 @@ class ContactMergeAction
|
||||
|
||||
# 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])
|
||||
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact,
|
||||
tokens: [@base_contact.contact_inboxes.filter_map(&:pubsub_token)])
|
||||
@base_contact.update!(merged_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password]
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin]
|
||||
|
||||
def perform
|
||||
if @user.nil?
|
||||
@@ -65,6 +65,7 @@ class AccountBuilder
|
||||
password: user_password,
|
||||
password_confirmation: user_password,
|
||||
name: @user_full_name)
|
||||
@user.type = 'SuperAdmin' if @super_admin
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
end
|
||||
|
||||
@@ -33,7 +33,8 @@ class ContactBuilder
|
||||
phone_number: contact_attributes[:phone_number],
|
||||
email: contact_attributes[:email],
|
||||
identifier: contact_attributes[:identifier],
|
||||
additional_attributes: contact_attributes[:additional_attributes]
|
||||
additional_attributes: contact_attributes[:additional_attributes],
|
||||
custom_attributes: contact_attributes[:custom_attributes]
|
||||
)
|
||||
end
|
||||
|
||||
@@ -69,7 +70,7 @@ class ContactBuilder
|
||||
update_contact_avatar(contact)
|
||||
contact_inbox
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Rails.logger.error e
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ class ContactInboxBuilder
|
||||
def perform
|
||||
@contact = Contact.find(contact_id)
|
||||
@inbox = @contact.account.inboxes.find(inbox_id)
|
||||
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api'].include? @inbox.channel_type
|
||||
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
||||
|
||||
source_id = @source_id || generate_source_id
|
||||
create_contact_inbox(source_id) if source_id.present?
|
||||
@@ -13,11 +13,25 @@ class ContactInboxBuilder
|
||||
private
|
||||
|
||||
def generate_source_id
|
||||
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
|
||||
return @contact.email if @inbox.channel_type == 'Channel::Email'
|
||||
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
|
||||
case @inbox.channel_type
|
||||
when 'Channel::TwilioSms'
|
||||
twilio_source_id
|
||||
when 'Channel::Whatsapp'
|
||||
wa_source_id
|
||||
when 'Channel::Email'
|
||||
@contact.email
|
||||
when 'Channel::Sms'
|
||||
@contact.phone_number
|
||||
when 'Channel::Api'
|
||||
SecureRandom.uuid
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
def wa_source_id
|
||||
return unless @contact.phone_number
|
||||
|
||||
# whatsapp doesn't want the + in e164 format
|
||||
"#{@contact.phone_number}.delete('+')"
|
||||
end
|
||||
|
||||
def twilio_source_id
|
||||
|
||||
@@ -27,7 +27,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
end
|
||||
ensure_contact_avatar
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||
Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
true
|
||||
|
||||
@@ -8,9 +8,11 @@ class Messages::MessageBuilder
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@attachments = params[:attachments]
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
end
|
||||
|
||||
def perform
|
||||
@@ -27,11 +29,12 @@ class Messages::MessageBuilder
|
||||
return if @attachments.blank?
|
||||
|
||||
@attachments.each do |uploaded_attachment|
|
||||
@message.attachments.build(
|
||||
attachment = @message.attachments.build(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
|
||||
attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
class Messages::Messenger::MessageBuilder
|
||||
include ::FileTypeHelper
|
||||
|
||||
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]
|
||||
fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention'
|
||||
update_attachment_file_type(attachment_obj)
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
@@ -22,7 +26,7 @@ class Messages::Messenger::MessageBuilder
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
if [:image, :file, :audio, :video, :share, :story_mention].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
@@ -39,4 +43,31 @@ class Messages::Messenger::MessageBuilder
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def update_attachment_file_type(attachment)
|
||||
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
||||
|
||||
attachment.file_type = file_type(attachment.file&.content_type)
|
||||
attachment.save!
|
||||
end
|
||||
|
||||
def fetch_story_link(attachment)
|
||||
message = attachment.message
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(message.source_id, fields: %w[story from]) || {}
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
story_id = result['story']['mention']['id']
|
||||
story_sender = result['from']['username']
|
||||
message.content_attributes[:story_sender] = story_sender
|
||||
message.content_attributes[:story_id] = story_id
|
||||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
||||
message.save!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
class V2::ReportBuilder
|
||||
include DateRangeHelper
|
||||
include ReportHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
DEFAULT_GROUP_BY = 'day'.freeze
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
|
||||
timezone_offset = (params[:timezone_offset] || 0).to_f
|
||||
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
|
||||
end
|
||||
|
||||
def timeseries
|
||||
@@ -13,8 +19,14 @@ class V2::ReportBuilder
|
||||
|
||||
# For backward compatible with old report
|
||||
def build
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].to_time.to_i }
|
||||
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
|
||||
end
|
||||
else
|
||||
timeseries.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,23 +41,16 @@ class V2::ReportBuilder
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope
|
||||
case params[:type]
|
||||
when :account
|
||||
account
|
||||
when :inbox
|
||||
inbox
|
||||
when :agent
|
||||
user
|
||||
when :label
|
||||
label
|
||||
when :team
|
||||
team
|
||||
def conversation_metrics
|
||||
if params[:type].equal?(:account)
|
||||
conversations
|
||||
else
|
||||
agent_metrics
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
@@ -62,56 +67,37 @@ class V2::ReportBuilder
|
||||
@team ||= account.teams.find(params[:id])
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
scope.conversations
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
def get_grouped_values(object_scope)
|
||||
@grouped_values = object_scope.group_by_period(
|
||||
params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at,
|
||||
default_value: 0,
|
||||
range: range,
|
||||
permit: %w[day week month year],
|
||||
time_zone: @timezone
|
||||
)
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
scope.messages.incoming.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
def agent_metrics
|
||||
users = @account.users
|
||||
users = users.where(id: params[:user_id]) if params[:user_id].present?
|
||||
users.each_with_object([]) do |user, arr|
|
||||
@user = user
|
||||
arr << {
|
||||
user: { id: user.id, name: user.name, thumbnail: user.avatar_url },
|
||||
metric: conversations
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.outgoing.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
scope.conversations
|
||||
.resolved
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
scope.events
|
||||
.where(name: 'first_response')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time
|
||||
scope.events.where(name: 'conversation_resolved')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
end
|
||||
|
||||
# Taking average of average is not too accurate
|
||||
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||
# TODO: Will optimize this later
|
||||
def avg_resolution_time_summary
|
||||
return 0 if avg_resolution_time.values.empty?
|
||||
|
||||
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
|
||||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
return 0 if avg_first_response_time.values.empty?
|
||||
|
||||
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||
def conversations
|
||||
@open_conversations = scope.conversations.open
|
||||
first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||
metric = {
|
||||
open: @open_conversations.count,
|
||||
unattended: @open_conversations.count - first_response_count
|
||||
}
|
||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||
metric
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
class RoomChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
# TODO: should we only do ensure stream if current account is present?
|
||||
# for now going ahead with guard clauses in update_subscription and broadcast_presence
|
||||
|
||||
ensure_stream
|
||||
current_user
|
||||
current_account
|
||||
@@ -15,6 +18,8 @@ class RoomChannel < ApplicationCable::Channel
|
||||
private
|
||||
|
||||
def broadcast_presence
|
||||
return if @current_account.blank?
|
||||
|
||||
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
|
||||
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
|
||||
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
|
||||
@@ -26,12 +31,14 @@ class RoomChannel < ApplicationCable::Channel
|
||||
end
|
||||
|
||||
def update_subscription
|
||||
return if @current_account.blank?
|
||||
|
||||
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user ||= if params[:user_id].blank?
|
||||
Contact.find_by!(pubsub_token: @pubsub_token)
|
||||
ContactInbox.find_by!(pubsub_token: @pubsub_token).contact
|
||||
else
|
||||
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent_bot.destroy
|
||||
@agent_bot.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_agent, except: [:create, :index]
|
||||
before_action :check_authorization
|
||||
before_action :find_user, only: [:create]
|
||||
before_action :validate_limit, only: [:create]
|
||||
before_action :create_user, only: [:create]
|
||||
before_action :save_account_user, only: [:create]
|
||||
|
||||
@@ -17,7 +18,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent.current_account_user.destroy
|
||||
@agent.current_account_user.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -67,6 +68,10 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def agents
|
||||
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
|
||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
||||
end
|
||||
|
||||
def validate_limit
|
||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan') if agents.count >= Current.account.usage_limits[:agents]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone]
|
||||
|
||||
def index
|
||||
@automation_rules = Current.account.automation_rules
|
||||
end
|
||||
|
||||
def create
|
||||
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||
@automation_rule.actions = params[:actions]
|
||||
|
||||
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
||||
|
||||
@automation_rule.save!
|
||||
process_attachments
|
||||
@automation_rule
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@automation_rule.update!(automation_rules_permit)
|
||||
@automation_rule.actions = params[:actions] if params[:actions]
|
||||
@automation_rule.save!
|
||||
process_attachments
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@automation_rule.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def clone
|
||||
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
||||
new_rule = automation_rule.dup
|
||||
new_rule.save
|
||||
@automation_rule = new_rule
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_attachments
|
||||
return if params[:attachments].blank?
|
||||
|
||||
params[:attachments].each do |uploaded_attachment|
|
||||
@automation_rule.files.attach(uploaded_attachment)
|
||||
end
|
||||
@automation_rule
|
||||
end
|
||||
|
||||
def automation_rules_permit
|
||||
params.permit(
|
||||
:name, :description, :event_name, :account_id, :active,
|
||||
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
||||
actions: [:action_name, { action_params: [] }]
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_automation_rule
|
||||
@automation_rule = Current.account.automation_rules.find_by(id: params[:id])
|
||||
end
|
||||
end
|
||||
@@ -1,32 +1,6 @@
|
||||
class Api::V1::Accounts::BaseController < Api::BaseController
|
||||
include SwitchLocale
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :current_account
|
||||
around_action :switch_locale_using_account_locale
|
||||
|
||||
private
|
||||
|
||||
def current_account
|
||||
@current_account ||= ensure_current_account
|
||||
Current.account = @current_account
|
||||
end
|
||||
|
||||
def ensure_current_account
|
||||
account = Account.find(params[:account_id])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
@current_account_user = account.account_users.find_by(user_id: current_user.id)
|
||||
Current.account_user = @current_account_user
|
||||
render_unauthorized('You are not authorized to access this account') unless @current_account_user
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController
|
||||
before_action :type_matches?
|
||||
|
||||
def create
|
||||
if type_matches?
|
||||
::BulkActionsJob.perform_later(
|
||||
account: @current_account,
|
||||
user: current_user,
|
||||
params: permitted_params
|
||||
)
|
||||
head :ok
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type_matches?
|
||||
['Conversation'].include?(params[:type])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []])
|
||||
end
|
||||
end
|
||||
@@ -74,10 +74,10 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def long_lived_token(omniauth_token)
|
||||
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
|
||||
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Rails.logger.error e
|
||||
end
|
||||
|
||||
def mark_already_existing_facebook_pages(data)
|
||||
|
||||
@@ -11,14 +11,14 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@campaign.destroy
|
||||
@campaign.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@campaign.update(campaign_params)
|
||||
@campaign.update!(campaign_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
|
||||
end
|
||||
|
||||
def destroy
|
||||
@canned_response.destroy
|
||||
@canned_response.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
|
||||
|
||||
def canned_responses
|
||||
if params[:search]
|
||||
Current.account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
|
||||
Current.account.canned_responses.where('short_code ILIKE :search OR content ILIKE :search', search: "%#{params[:search]}%")
|
||||
else
|
||||
Current.account.canned_responses
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
|
||||
end
|
||||
|
||||
def destroy
|
||||
@note.destroy
|
||||
@note.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search]
|
||||
before_action :set_current_page, only: [:index, :active, :search, :filter]
|
||||
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes, :destroy_custom_attributes]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search, :filter]
|
||||
|
||||
def index
|
||||
@contacts_count = resolved_contacts.count
|
||||
@@ -81,11 +81,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
def update
|
||||
@contact.assign_attributes(contact_update_params)
|
||||
@contact.save!
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render json: {
|
||||
message: e.record.errors.full_messages.join(', '),
|
||||
contact: Current.account.contacts.find_by(email: contact_params[:email])
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :conversation
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage::DirectUploadsController
|
||||
include EnsureCurrentAccountHelper
|
||||
before_action :current_account
|
||||
before_action :conversation
|
||||
|
||||
def create
|
||||
return if @conversation.nil? || @current_account.nil?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
|
||||
end
|
||||
end
|
||||
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
|
||||
def destroy
|
||||
ActiveRecord::Base.transaction do
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), deleted: true)
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
|
||||
message.attachments.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,6 +61,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
||||
end
|
||||
|
||||
def toggle_typing_status
|
||||
@@ -74,9 +75,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
@conversation.agent_last_seen_at = DateTime.now.utc
|
||||
@conversation.assignee_last_seen_at = DateTime.now.utc if assignee?
|
||||
@conversation.save!
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
@conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
|
||||
@conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee?
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def custom_attributes
|
||||
@@ -92,6 +94,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
|
||||
def assign_conversation
|
||||
@agent = Current.account.users.find(current_user.id)
|
||||
@conversation.update_assignee(@agent)
|
||||
end
|
||||
|
||||
def trigger_typing_event(event, is_private)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
|
||||
|
||||
@@ -30,8 +30,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||
def set_csat_survey_responses
|
||||
@csat_survey_responses = filtrate(
|
||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||
)
|
||||
@csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present?
|
||||
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
|
||||
end
|
||||
|
||||
def set_current_page_surveys
|
||||
|
||||
@@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_attribute_definition.destroy
|
||||
@custom_attribute_definition.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
@@ -39,7 +39,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
||||
:attribute_display_type,
|
||||
:attribute_key,
|
||||
:attribute_model,
|
||||
:default_value
|
||||
attribute_values: []
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_filter.destroy
|
||||
@custom_filter.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_inbox
|
||||
before_action :current_agents_ids, only: [:update]
|
||||
before_action :current_agents_ids, only: [:create, :update]
|
||||
|
||||
def create
|
||||
authorize @inbox, :create?
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:user_ids].map { |user_id| @inbox.add_member(user_id) }
|
||||
agents_to_be_added_ids.map { |user_id| @inbox.add_member(user_id) }
|
||||
end
|
||||
fetch_updated_agents
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
include Api::V1::InboxesHelper
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
before_action :validate_limit, only: [:create]
|
||||
# we are already handling the authorization in fetch inbox
|
||||
before_action :check_authorization, except: [:show]
|
||||
|
||||
@@ -41,12 +43,16 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
def update
|
||||
@inbox.update(permitted_params.except(:channel))
|
||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||
|
||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||
|
||||
# Inbox update doesn't necessarily need channel attributes
|
||||
return if permitted_params(channel_attributes)[:channel].blank?
|
||||
|
||||
if @inbox.inbox_type == 'Email'
|
||||
validate_email_channel(channel_attributes)
|
||||
@inbox.channel.reauthorized!
|
||||
end
|
||||
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||
update_channel_feature_flags
|
||||
end
|
||||
@@ -67,7 +73,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@inbox.destroy
|
||||
@inbox.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -89,20 +95,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def create_channel
|
||||
case permitted_params[:channel][:type]
|
||||
when 'web_widget'
|
||||
Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'api'
|
||||
Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'email'
|
||||
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
|
||||
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
||||
|
||||
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
@@ -116,25 +111,46 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
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,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||
channel: [:type, *channel_attributes]
|
||||
)
|
||||
end
|
||||
|
||||
def channel_type_from_params
|
||||
{
|
||||
'web_widget' => Channel::WebWidget,
|
||||
'api' => Channel::Api,
|
||||
'email' => Channel::Email,
|
||||
'line' => Channel::Line,
|
||||
'telegram' => Channel::Telegram,
|
||||
'whatsapp' => Channel::Whatsapp,
|
||||
'sms' => Channel::Sms
|
||||
}[permitted_params[:channel][:type]]
|
||||
end
|
||||
|
||||
def account_channels_method
|
||||
{
|
||||
'web_widget' => Current.account.web_widgets,
|
||||
'api' => Current.account.api_channels,
|
||||
'email' => Current.account.email_channels,
|
||||
'line' => Current.account.line_channels,
|
||||
'telegram' => Current.account.telegram_channels,
|
||||
'whatsapp' => Current.account.whatsapp_channels,
|
||||
'sms' => Current.account.sms_channels
|
||||
}[permitted_params[:channel][:type]]
|
||||
end
|
||||
|
||||
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
|
||||
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
|
||||
channel_type.constantize::EDITABLE_ATTRS.presence
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def validate_limit
|
||||
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
||||
|
||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy
|
||||
@hook.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy
|
||||
@hook.destroy!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase:
|
||||
end
|
||||
|
||||
def destroy
|
||||
@category.destroy
|
||||
@category.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::Ba
|
||||
end
|
||||
|
||||
def destroy
|
||||
@portal.destroy
|
||||
@portal.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@label.destroy
|
||||
@label.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@team_members = params[:user_ids].map { |user_id| @team.add_member(user_id) }
|
||||
@team_members = members_to_be_added_ids.map { |user_id| @team.add_member(user_id) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@team.destroy
|
||||
@team.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@webhook.destroy
|
||||
@webhook.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
before_action :validate_captcha, only: [:create]
|
||||
before_action :fetch_account, except: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
|
||||
@@ -55,7 +56,11 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfig.get_value('ENABLE_ACCOUNT_SIGNUP') == 'false'
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
def validate_captcha
|
||||
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
|
||||
@@ -7,6 +7,12 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
|
||||
render json: notification_subscription
|
||||
end
|
||||
|
||||
def destroy
|
||||
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
|
||||
notification_subscription.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
|
||||
@@ -13,6 +13,11 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
@user.update!(profile_params)
|
||||
end
|
||||
|
||||
def avatar
|
||||
@user.avatar.attachment.destroy! if @user.avatar.attached?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def availability
|
||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||
end
|
||||
@@ -33,6 +38,7 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
:name,
|
||||
:display_name,
|
||||
:avatar,
|
||||
:message_signature,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Widget::BaseController < ApplicationController
|
||||
include SwitchLocale
|
||||
include WebsiteTokenHelper
|
||||
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
@@ -19,23 +20,6 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
@conversation ||= conversations.last
|
||||
end
|
||||
|
||||
def auth_token_params
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
@current_account = @web_widget.account
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
raise ActiveRecord::RecordNotFound unless @contact
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
::Conversation.create!(conversation_params)
|
||||
end
|
||||
@@ -55,7 +39,8 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
browser: browser_params,
|
||||
referer: permitted_params[:message][:referer_url],
|
||||
initiated_at: timestamp_params
|
||||
}
|
||||
},
|
||||
custom_attributes: permitted_params[:custom_attributes].presence || {}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -68,16 +53,33 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
mergee_contact: @contact
|
||||
).perform
|
||||
else
|
||||
@contact.update!(email: email, name: contact_name)
|
||||
@contact.update!(email: email)
|
||||
end
|
||||
end
|
||||
|
||||
def update_contact_phone_number(phone_number)
|
||||
contact_with_phone_number = @current_account.contacts.find_by(phone_number: phone_number)
|
||||
if contact_with_phone_number
|
||||
@contact = ::ContactMergeAction.new(
|
||||
account: @current_account,
|
||||
base_contact: contact_with_phone_number,
|
||||
mergee_contact: @contact
|
||||
).perform
|
||||
else
|
||||
@contact.update!(phone_number: phone_number)
|
||||
end
|
||||
end
|
||||
|
||||
def contact_email
|
||||
permitted_params[:contact][:email].downcase
|
||||
permitted_params[:contact][:email].downcase if permitted_params[:contact].present?
|
||||
end
|
||||
|
||||
def contact_name
|
||||
params[:contact][:name] || contact_email.split('@')[0]
|
||||
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
|
||||
end
|
||||
|
||||
def contact_phone_number
|
||||
params[:contact][:phone_number]
|
||||
end
|
||||
|
||||
def browser_params
|
||||
@@ -94,10 +96,6 @@ 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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
before_action :process_hmac
|
||||
before_action :process_hmac, only: [:update]
|
||||
|
||||
def show; end
|
||||
|
||||
@@ -8,7 +8,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
contact: @contact,
|
||||
params: permitted_params.to_h.deep_symbolize_keys
|
||||
)
|
||||
render json: contact_identify_action.perform
|
||||
@contact = contact_identify_action.perform
|
||||
end
|
||||
|
||||
# TODO : clean up this with proper routes delete contacts/custom_attributes
|
||||
@@ -21,13 +21,22 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
private
|
||||
|
||||
def process_hmac
|
||||
return if params[:identifier_hash].blank? && !@web_widget.hmac_mandatory
|
||||
return unless should_verify_hmac?
|
||||
|
||||
render json: { error: 'HMAC failed: Invalid Identifier Hash Provided' }, status: :unauthorized unless valid_hmac?
|
||||
|
||||
@contact_inbox.update(hmac_verified: true)
|
||||
end
|
||||
|
||||
def should_verify_hmac?
|
||||
return false if params[:identifier_hash].blank? && !@web_widget.hmac_mandatory
|
||||
|
||||
# Taking an extra caution that the hmac is triggered whenever identifier is present
|
||||
return false if params[:custom_attributes].present? && params[:identifier].blank?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def valid_hmac?
|
||||
params[:identifier_hash] == OpenSSL::HMAC.hexdigest(
|
||||
'sha256',
|
||||
@@ -37,6 +46,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {},
|
||||
additional_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,12 +7,18 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||
process_update_contact
|
||||
@conversation = create_conversation
|
||||
conversation.messages.create(message_params)
|
||||
end
|
||||
end
|
||||
|
||||
def process_update_contact
|
||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present?
|
||||
@contact.update!(name: contact_name) if contact_name.present?
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
@@ -44,6 +50,18 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
return head :not_found if conversation.nil?
|
||||
|
||||
return head :forbidden unless @web_widget.end_conversation?
|
||||
|
||||
unless conversation.resolved?
|
||||
conversation.status = :resolved
|
||||
conversation.save
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trigger_typing_event(event)
|
||||
@@ -51,6 +69,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id],
|
||||
custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController
|
||||
include WebsiteTokenHelper
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
|
||||
def create
|
||||
return if @contact.nil? || @current_account.nil?
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
@@ -29,11 +29,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
return if params[:message][:attachments].blank?
|
||||
|
||||
params[:message][:attachments].each do |uploaded_attachment|
|
||||
@message.attachments.new(
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type),
|
||||
file: uploaded_attachment
|
||||
)
|
||||
|
||||
attachment.file_type = helpers.file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -35,33 +35,77 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
def conversations
|
||||
return head :unprocessable_entity if params[:type].blank?
|
||||
|
||||
render json: conversation_metrics
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def summary_params
|
||||
def common_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
id: params[:id],
|
||||
group_by: params[:group_by],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||
}
|
||||
end
|
||||
|
||||
def current_summary_params
|
||||
common_params.merge({
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until]
|
||||
})
|
||||
end
|
||||
|
||||
def previous_summary_params
|
||||
common_params.merge({
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until]
|
||||
})
|
||||
end
|
||||
|
||||
def report_params
|
||||
common_params.merge({
|
||||
metric: params[:metric],
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
})
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
metric: params[:metric],
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
user_id: params[:user_id]
|
||||
}
|
||||
end
|
||||
|
||||
def range
|
||||
{
|
||||
current: {
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
},
|
||||
previous: {
|
||||
since: (params[:since].to_i - (params[:until].to_i - params[:since].to_i)).to_s,
|
||||
until: params[:since]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def summary_metrics
|
||||
builder = V2::ReportBuilder.new(Current.account, summary_params)
|
||||
builder.summary
|
||||
summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary
|
||||
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
|
||||
summary
|
||||
end
|
||||
|
||||
def conversation_metrics
|
||||
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
module EnsureCurrentAccountHelper
|
||||
private
|
||||
|
||||
def current_account
|
||||
@current_account ||= ensure_current_account
|
||||
Current.account = @current_account
|
||||
end
|
||||
|
||||
def ensure_current_account
|
||||
account = Account.find(params[:account_id])
|
||||
if current_user
|
||||
account_accessible_for_user?(account)
|
||||
elsif @resource.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
account
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
@current_account_user = account.account_users.find_by(user_id: current_user.id)
|
||||
Current.account_user = @current_account_user
|
||||
render_unauthorized('You are not authorized to access this account') unless @current_account_user
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
end
|
||||
@@ -31,13 +31,18 @@ module RequestExceptionHandler
|
||||
render json: { error: message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def render_payment_required(message)
|
||||
render json: { error: message }, status: :payment_required
|
||||
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(', ')
|
||||
message: exception.record.errors.full_messages.join(', '),
|
||||
attributes: exception.record.errors.attribute_names
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
module WebsiteTokenHelper
|
||||
def auth_token_params
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
@current_account = @web_widget.account
|
||||
end
|
||||
|
||||
def set_contact
|
||||
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
|
||||
source_id: auth_token_params[:source_id]
|
||||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
raise ActiveRecord::RecordNotFound unless @contact
|
||||
|
||||
Current.contact = @contact
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token)
|
||||
end
|
||||
end
|
||||
@@ -25,15 +25,22 @@ class DashboardController < ActionController::Base
|
||||
'API_CHANNEL_NAME',
|
||||
'API_CHANNEL_THUMBNAIL',
|
||||
'ANALYTICS_TOKEN',
|
||||
'ANALYTICS_HOST'
|
||||
).merge(
|
||||
APP_VERSION: Chatwoot.config[:version],
|
||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false')
|
||||
)
|
||||
'ANALYTICS_HOST',
|
||||
'DIRECT_UPLOADS_ENABLED',
|
||||
'HCAPTCHA_SITE_KEY',
|
||||
'LOGOUT_REDIRECT_LINK',
|
||||
'DISABLE_USER_PROFILE_UPDATE'
|
||||
).merge(app_config)
|
||||
end
|
||||
|
||||
def ensure_installation_onboarding
|
||||
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||
end
|
||||
|
||||
def app_config
|
||||
{ APP_VERSION: Chatwoot.config[:version],
|
||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', '') }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,10 +28,7 @@ class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
|
||||
end
|
||||
|
||||
def create_reset_token_link(user)
|
||||
raw, enc = Devise.token_generator.generate(user.class, :reset_password_token)
|
||||
user.reset_password_token = enc
|
||||
user.reset_password_sent_at = Time.now.utc
|
||||
user.save(validate: false)
|
||||
"/app/auth/password/edit?config=default&redirect_url=&reset_password_token=#{raw}"
|
||||
token = user.send(:set_reset_password_token)
|
||||
"/app/auth/password/edit?config=default&redirect_url=&reset_password_token=#{token}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,6 +10,7 @@ class Installation::OnboardingController < ApplicationController
|
||||
user_full_name: onboarding_params.dig(:user, :name),
|
||||
email: onboarding_params.dig(:user, :email),
|
||||
user_password: params.dig(:user, :password),
|
||||
super_admin: true,
|
||||
confirmed: true
|
||||
).perform
|
||||
rescue StandardError => e
|
||||
|
||||
@@ -13,7 +13,7 @@ class Platform::Api::V1::AccountUsersController < PlatformController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy
|
||||
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
|
||||
def create
|
||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||
@resource.skip_confirmation!
|
||||
@resource.save!
|
||||
@resource.confirm
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
|
||||
end
|
||||
|
||||
def login
|
||||
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{@resource.email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
|
||||
encoded_email = ERB::Util.url_encode(@resource.email)
|
||||
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
@@ -33,11 +33,11 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
def resource_params
|
||||
permitted_params = super
|
||||
permitted_params[:limits] = permitted_params[:limits].to_h.compact
|
||||
permitted_params
|
||||
end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
def show
|
||||
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
|
||||
# ref: https://github.com/rubocop/rubocop/issues/7767
|
||||
# rubocop:disable Style/HashTransformValues
|
||||
@fb_config = InstallationConfig.where(name: @allowed_configs)
|
||||
.pluck(:name, :serialized_value)
|
||||
.map { |name, serialized_value| [name, serialized_value['value']] }
|
||||
.to_h
|
||||
# rubocop:enable Style/HashTransformValues
|
||||
end
|
||||
|
||||
def create
|
||||
params['app_config'].each do |key, value|
|
||||
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
||||
i.value = value
|
||||
i.save!
|
||||
end
|
||||
redirect_to super_admin_app_config_url
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,7 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
|
||||
def create
|
||||
redirect_to(super_admin_session_path, flash: { error: @error_message }) && return unless valid_credentials?
|
||||
|
||||
sign_in(@super_admin, scope: :super_admin)
|
||||
sign_in(:super_admin, @super_admin)
|
||||
flash.discard
|
||||
redirect_to super_admin_users_path
|
||||
end
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
class SuperAdmin::SuperAdminsController < SuperAdmin::ApplicationController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
||||
@@ -33,12 +33,15 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
def resource_params
|
||||
permitted_params = super
|
||||
permitted_params.delete(:password) if permitted_params[:password].blank?
|
||||
permitted_params
|
||||
end
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
def find_resource(param)
|
||||
super.becomes(User)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Rails.logger.error e
|
||||
redirect_to twitter_app_redirect_url
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Webhooks::InstagramController < ApplicationController
|
||||
::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']}")
|
||||
Rails.logger.warn("Message is not received from the instagram webhook event: #{params['object']}")
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -25,6 +25,6 @@ class Webhooks::InstagramController < ApplicationController
|
||||
private
|
||||
|
||||
def valid_instagram_token?(token)
|
||||
token == ENV['IG_VERIFY_TOKEN']
|
||||
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class Webhooks::SmsController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::SmsEventsJob.perform_later(params['_json']&.first&.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,9 @@
|
||||
class WidgetTestsController < ActionController::Base
|
||||
before_action :set_web_widget
|
||||
before_action :ensure_web_widget
|
||||
before_action :ensure_widget_position
|
||||
before_action :ensure_widget_type
|
||||
before_action :ensure_widget_style
|
||||
before_action :ensure_dark_mode
|
||||
|
||||
def index
|
||||
render
|
||||
@@ -7,7 +11,28 @@ class WidgetTestsController < ActionController::Base
|
||||
|
||||
private
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = Channel::WebWidget.first
|
||||
def ensure_widget_style
|
||||
@widget_style = params[:widget_style] || 'standard'
|
||||
end
|
||||
|
||||
def ensure_dark_mode
|
||||
@dark_mode = params[:dark_mode] || 'light'
|
||||
end
|
||||
|
||||
def ensure_widget_position
|
||||
@widget_position = params[:position] || 'left'
|
||||
end
|
||||
|
||||
def ensure_widget_type
|
||||
@widget_type = params[:type] || 'expanded_bubble'
|
||||
end
|
||||
|
||||
def inbox_id
|
||||
@inbox_id ||= params[:inbox_id] || Channel::WebWidget.first.inbox.id
|
||||
end
|
||||
|
||||
def ensure_web_widget
|
||||
@inbox = Inbox.find(inbox_id)
|
||||
@web_widget = @inbox.channel
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class WidgetsController < ActionController::Base
|
||||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'DIRECT_UPLOADS_ENABLED')
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@@ -29,21 +29,21 @@ class WidgetsController < ActionController::Base
|
||||
def set_contact
|
||||
return if @auth_token_params[:source_id].nil?
|
||||
|
||||
contact_inbox = ::ContactInbox.find_by(
|
||||
@contact_inbox = ::ContactInbox.find_by(
|
||||
inbox_id: @web_widget.inbox.id,
|
||||
source_id: @auth_token_params[:source_id]
|
||||
)
|
||||
|
||||
@contact = contact_inbox ? contact_inbox.contact : nil
|
||||
@contact = @contact_inbox ? @contact_inbox.contact : nil
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if @contact.present?
|
||||
|
||||
contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
|
||||
@contact = contact_inbox.contact
|
||||
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
|
||||
@contact = @contact_inbox.contact
|
||||
|
||||
payload = { source_id: contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
||||
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
||||
@token = ::Widget::TokenService.new(payload: payload).generate_token
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
|
||||
enterprise_attribute_types = ChatwootApp.enterprise? ? { limits: Enterprise::AccountLimitsField } : {}
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
@@ -16,7 +18,7 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
conversations: CountField,
|
||||
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
|
||||
account_users: Field::HasMany
|
||||
}.freeze
|
||||
}.merge(enterprise_attribute_types).freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
@@ -33,7 +35,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
||||
SHOW_PAGE_ATTRIBUTES = (%i[
|
||||
id
|
||||
name
|
||||
created_at
|
||||
@@ -41,15 +44,16 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
locale
|
||||
conversations
|
||||
account_users
|
||||
].freeze
|
||||
] + enterprise_show_page_attributes).freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
||||
FORM_ATTRIBUTES = (%i[
|
||||
name
|
||||
locale
|
||||
].freeze
|
||||
] + enterprise_form_attributes).freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
@@ -69,4 +73,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
def display_resource(account)
|
||||
"##{account.id} #{account.name}"
|
||||
end
|
||||
|
||||
def permitted_attributes
|
||||
super + [limits: {}]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
avatar_url: AvatarField,
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
||||
description: Field::String,
|
||||
outgoing_url: Field::String,
|
||||
created_at: Field::DateTime,
|
||||
@@ -26,6 +27,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
account
|
||||
name
|
||||
outgoing_url
|
||||
].freeze
|
||||
@@ -34,7 +36,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
account
|
||||
name
|
||||
description
|
||||
outgoing_url
|
||||
@@ -45,6 +47,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
account
|
||||
description
|
||||
outgoing_url
|
||||
].freeze
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
require 'administrate/base_dashboard'
|
||||
|
||||
class SuperAdminDashboard < Administrate::BaseDashboard
|
||||
# ATTRIBUTE_TYPES
|
||||
# a hash that describes the type of each of the model's fields.
|
||||
#
|
||||
# Each different type represents an Administrate::Field object,
|
||||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
id: Field::Number,
|
||||
email: Field::String,
|
||||
password: Field::Password,
|
||||
remember_created_at: Field::DateTime,
|
||||
sign_in_count: Field::Number,
|
||||
current_sign_in_at: Field::DateTime,
|
||||
last_sign_in_at: Field::DateTime,
|
||||
current_sign_in_ip: Field::String.with_options(searchable: false),
|
||||
last_sign_in_ip: Field::String.with_options(searchable: false),
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's index page.
|
||||
#
|
||||
# By default, it's limited to four items to reduce clutter on index pages.
|
||||
# Feel free to add, remove, or rearrange items.
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
remember_created_at
|
||||
sign_in_count
|
||||
current_sign_in_at
|
||||
last_sign_in_at
|
||||
current_sign_in_ip
|
||||
last_sign_in_ip
|
||||
created_at
|
||||
updated_at
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
# an array of attributes that will be displayed
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
email
|
||||
password
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
# a hash that defines filters that can be used while searching via the search
|
||||
# field of the dashboard.
|
||||
#
|
||||
# For example to add an option to search for open resources by typing "open:"
|
||||
# in the search field:
|
||||
#
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how super admins are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
#
|
||||
# def display_resource(super_admin)
|
||||
# "SuperAdmin ##{super_admin.id}"
|
||||
# end
|
||||
end
|
||||
@@ -30,6 +30,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
pubsub_token: Field::String,
|
||||
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
|
||||
accounts: CountField
|
||||
}.freeze
|
||||
|
||||
@@ -44,6 +45,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
name
|
||||
email
|
||||
accounts
|
||||
type
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
@@ -53,10 +55,12 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
avatar_url
|
||||
unconfirmed_email
|
||||
name
|
||||
type
|
||||
display_name
|
||||
email
|
||||
created_at
|
||||
updated_at
|
||||
confirmed_at
|
||||
account_users
|
||||
].freeze
|
||||
|
||||
@@ -68,6 +72,8 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
display_name
|
||||
email
|
||||
password
|
||||
confirmed_at
|
||||
type
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
|
||||
@@ -12,10 +12,12 @@ class AsyncDispatcher < BaseDispatcher
|
||||
[
|
||||
CampaignListener.instance,
|
||||
CsatSurveyListener.instance,
|
||||
EventListener.instance,
|
||||
HookListener.instance,
|
||||
InstallationWebhookListener.instance,
|
||||
WebhookListener.instance
|
||||
NotificationListener.instance,
|
||||
ReportingEventListener.instance,
|
||||
WebhookListener.instance,
|
||||
AutomationRuleListener.instance
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
|
||||
end
|
||||
|
||||
def listeners
|
||||
[ActionCableListener.instance, AgentBotListener.instance, NotificationListener.instance]
|
||||
[ActionCableListener.instance, AgentBotListener.instance]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
class ConversationDrop < BaseDrop
|
||||
include MessageFormatHelper
|
||||
|
||||
def display_id
|
||||
@obj.try(:display_id)
|
||||
end
|
||||
|
||||
def contact_name
|
||||
@obj.try(:contact).name.capitalize || 'Customer'
|
||||
end
|
||||
|
||||
def recent_messages
|
||||
@obj.try(:recent_messages).map do |message|
|
||||
{
|
||||
'sender' => message_sender_name(message.sender),
|
||||
'content' => render_message_content(transform_user_mention_content(message.content)),
|
||||
'attachments' => message.attachments.map(&:file_url)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_sender_name(sender)
|
||||
return 'Bot' if sender.blank?
|
||||
return contact_name if sender.instance_of?(Contact)
|
||||
|
||||
sender&.available_name || sender&.name
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
class InboxDrop < BaseDrop
|
||||
def name
|
||||
@obj.try(:name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class MessageDrop < BaseDrop
|
||||
end
|
||||
|
||||
def text_content
|
||||
content = @obj.try(:content)
|
||||
transform_user_mention_content content
|
||||
content = @obj.try(:content) || ''
|
||||
render_message_content(transform_user_mention_content(content))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,6 @@ require 'administrate/field/base'
|
||||
|
||||
class AvatarField < Administrate::Field::Base
|
||||
def avatar_url
|
||||
data.presence || '/admin/avatar.png'
|
||||
data.presence&.gsub('?d=404', '?d=mp')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
require 'administrate/field/base'
|
||||
|
||||
class Enterprise::AccountLimitsField < Administrate::Field::Base
|
||||
def to_s
|
||||
data.present? ? data.to_json : { agents: nil, inboxes: nil }.to_json
|
||||
end
|
||||
end
|
||||
@@ -55,7 +55,7 @@ class ConversationFinder
|
||||
|
||||
def set_inboxes
|
||||
@inbox_ids = if params[:inbox_id]
|
||||
current_account.inboxes.where(id: params[:inbox_id])
|
||||
@current_user.assigned_inboxes.where(id: params[:inbox_id])
|
||||
else
|
||||
@current_user.assigned_inboxes.pluck(:id)
|
||||
end
|
||||
@@ -70,7 +70,12 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def find_all_conversations
|
||||
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||
if params[:conversation_type] == 'mention'
|
||||
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
|
||||
@conversations = current_account.conversations.where(id: conversation_ids)
|
||||
else
|
||||
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_by_assignee_type
|
||||
@@ -121,8 +126,12 @@ class ConversationFinder
|
||||
|
||||
def conversations
|
||||
@conversations = @conversations.includes(
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||
)
|
||||
@conversations.latest.page(current_page)
|
||||
if params[:conversation_type] == 'mention'
|
||||
@conversations.page(current_page)
|
||||
else
|
||||
@conversations.latest.page(current_page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class EmailChannelFinder
|
||||
def initialize(email_object)
|
||||
@email_object = email_object
|
||||
end
|
||||
|
||||
def perform
|
||||
channel = nil
|
||||
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
|
||||
recipient_mails.each do |email|
|
||||
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase)
|
||||
break if channel.present?
|
||||
end
|
||||
channel
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
module Api::V1::InboxesHelper
|
||||
def validate_email_channel(attributes)
|
||||
channel_data = permitted_params(attributes)[:channel]
|
||||
|
||||
validate_imap(channel_data)
|
||||
validate_smtp(channel_data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_imap(channel_data)
|
||||
return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled]
|
||||
|
||||
Mail.defaults do
|
||||
retriever_method :imap, { address: channel_data[:imap_address],
|
||||
port: channel_data[:imap_port],
|
||||
user_name: channel_data[:imap_login],
|
||||
password: channel_data[:imap_password],
|
||||
enable_ssl: channel_data[:imap_enable_ssl] }
|
||||
end
|
||||
|
||||
Mail.connection do # rubocop:disable:block
|
||||
end
|
||||
end
|
||||
|
||||
def validate_smtp(channel_data)
|
||||
return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
|
||||
|
||||
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
||||
|
||||
set_smtp_encryption(channel_data, smtp)
|
||||
check_smtp_connection(channel_data, smtp)
|
||||
end
|
||||
|
||||
def check_smtp_connection(channel_data, smtp)
|
||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
|
||||
channel_data[:smtp_authentication]&.to_sym || :login)
|
||||
smtp.finish unless smtp&.nil?
|
||||
end
|
||||
|
||||
def set_smtp_encryption(channel_data, smtp)
|
||||
if channel_data[:smtp_enable_ssl_tls]
|
||||
set_enable_tls(channel_data, smtp)
|
||||
elsif channel_data[:smtp_enable_starttls_auto]
|
||||
set_enable_starttls_auto(channel_data, smtp)
|
||||
end
|
||||
end
|
||||
|
||||
def set_enable_starttls_auto(channel_data, smtp)
|
||||
return unless smtp.respond_to?(:enable_starttls_auto)
|
||||
|
||||
if channel_data[:smtp_openssl_verify_mode]
|
||||
context = enable_openssl_mode(channel_data[:smtp_openssl_verify_mode])
|
||||
smtp.enable_starttls_auto(context)
|
||||
else
|
||||
smtp.enable_starttls_auto
|
||||
end
|
||||
end
|
||||
|
||||
def set_enable_tls(channel_data, smtp)
|
||||
return unless smtp.respond_to?(:enable_tls)
|
||||
|
||||
if channel_data[:smtp_openssl_verify_mode]
|
||||
context = enable_openssl_mode(channel_data[:smtp_openssl_verify_mode])
|
||||
smtp.enable_tls(context)
|
||||
else
|
||||
smtp.enable_tls
|
||||
end
|
||||
end
|
||||
|
||||
def enable_openssl_mode(smtp_openssl_verify_mode)
|
||||
openssl_verify_mode = "OpenSSL::SSL::VERIFY_#{smtp_openssl_verify_mode.upcase}".constantize if smtp_openssl_verify_mode.is_a?(String)
|
||||
context = Net::SMTP.default_ssl_context
|
||||
context.verify_mode = openssl_verify_mode
|
||||
context
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module Api::V1::ReportsHelper
|
||||
end
|
||||
@@ -1,16 +1,31 @@
|
||||
module FileTypeHelper
|
||||
# NOTE: video, audio, image, etc are filetypes previewable in frontend
|
||||
def file_type(content_type)
|
||||
return :image if [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/tiff',
|
||||
'image/bmp'
|
||||
].include?(content_type)
|
||||
|
||||
return :video if content_type.include?('video/')
|
||||
return :audio if content_type.include?('audio/')
|
||||
return :image if image_file?(content_type)
|
||||
return :video if video_file?(content_type)
|
||||
return :audio if content_type&.include?('audio/')
|
||||
|
||||
:file
|
||||
end
|
||||
|
||||
def image_file?(content_type)
|
||||
[
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/bmp',
|
||||
'image/webp',
|
||||
'image'
|
||||
].include?(content_type)
|
||||
end
|
||||
|
||||
def video_file?(content_type)
|
||||
[
|
||||
'video/ogg',
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/quicktime',
|
||||
'video'
|
||||
].include?(content_type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
module MessageFormatHelper
|
||||
include RegexHelper
|
||||
|
||||
def transform_user_mention_content(message_content)
|
||||
message_content.gsub(MENTION_REGEX, '\1')
|
||||
end
|
||||
|
||||
def render_message_content(message_content)
|
||||
# rubocop:disable Rails/OutputSafety
|
||||
CommonMarker.render_html(message_content).html_safe
|
||||
# rubocop:enable Rails/OutputSafety
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
module ReportHelper
|
||||
private
|
||||
|
||||
def scope
|
||||
case params[:type]
|
||||
when :account
|
||||
account
|
||||
when :inbox
|
||||
inbox
|
||||
when :agent
|
||||
user
|
||||
when :label
|
||||
label
|
||||
when :team
|
||||
team
|
||||
end
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
(get_grouped_values scope.conversations).count
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
(get_grouped_values scope.conversations.resolved).count
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response'))
|
||||
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||
|
||||
grouped_reporting_events.average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time
|
||||
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved'))
|
||||
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||
|
||||
grouped_reporting_events.average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time_summary
|
||||
reporting_events = scope.reporting_events
|
||||
.where(name: 'conversation_resolved', created_at: range)
|
||||
avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||
|
||||
return 0 if avg_rt.blank?
|
||||
|
||||
avg_rt
|
||||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
reporting_events = scope.reporting_events
|
||||
.where(name: 'first_response', created_at: range)
|
||||
avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||
|
||||
return 0 if avg_frt.blank?
|
||||
|
||||
avg_frt
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
module ReportingEventHelper
|
||||
def business_hours(inbox, from, to)
|
||||
return 0 unless inbox.working_hours_enabled?
|
||||
|
||||
inbox_working_hours = configure_working_hours(inbox.working_hours)
|
||||
return 0 if inbox_working_hours.blank?
|
||||
|
||||
# Configure working hours
|
||||
WorkingHours::Config.working_hours = inbox_working_hours
|
||||
|
||||
# Configure timezone
|
||||
WorkingHours::Config.time_zone = inbox.timezone
|
||||
|
||||
# Use inbox timezone to change from & to values.
|
||||
from_in_inbox_timezone = from.in_time_zone(inbox.timezone).to_time
|
||||
to_in_inbox_timezone = to.in_time_zone(inbox.timezone).to_time
|
||||
from_in_inbox_timezone.working_time_until(to_in_inbox_timezone)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def configure_working_hours(working_hours)
|
||||
working_hours.each_with_object({}) do |working_hour, object|
|
||||
object[day(working_hour.day_of_week)] = working_hour_range(working_hour) unless working_hour.closed_all_day?
|
||||
end
|
||||
end
|
||||
|
||||
def day(day_of_week)
|
||||
week_days = {
|
||||
0 => :sun,
|
||||
1 => :mon,
|
||||
2 => :tue,
|
||||
3 => :wed,
|
||||
4 => :thu,
|
||||
5 => :fri,
|
||||
6 => :sat
|
||||
}
|
||||
week_days[day_of_week]
|
||||
end
|
||||
|
||||
def working_hour_range(working_hour)
|
||||
{ format_time(working_hour.open_hour, working_hour.open_minutes) => format_time(working_hour.close_hour, working_hour.close_minutes) }
|
||||
end
|
||||
|
||||
def format_time(hour, minute)
|
||||
hour = hour < 10 ? "0#{hour}" : hour
|
||||
minute = minute < 10 ? "0#{minute}" : minute
|
||||
"#{hour}:#{minute}"
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div id="app" class="app-wrapper app-root">
|
||||
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
|
||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
@@ -10,27 +11,37 @@
|
||||
<woot-snackbar-box />
|
||||
<network-notification />
|
||||
</div>
|
||||
<loading-state v-else />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import LoadingState from './components/widgets/LoadingState.vue';
|
||||
import NetworkNotification from './components/NetworkNotification';
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
} from './helper/pushHelper';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
components: {
|
||||
WootSnackbarBox,
|
||||
AddAccountModal,
|
||||
LoadingState,
|
||||
NetworkNotification,
|
||||
UpdateBanner,
|
||||
WootSnackbarBox,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
latestChatwootVersion: null,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -38,13 +49,13 @@ export default {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
authUIFlags: 'getAuthUIFlags',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
hasAccounts() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.accounts &&
|
||||
this.currentUser.accounts.length !== 0
|
||||
);
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
return accounts.length > 0;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -53,28 +64,37 @@ export default {
|
||||
if (!this.hasAccounts) {
|
||||
this.showAddAccountModal = true;
|
||||
}
|
||||
verifyServiceWorkerExistence(registration =>
|
||||
registration.pushManager.getSubscription().then(subscription => {
|
||||
if (subscription) {
|
||||
registerSubscription();
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
currentAccountId() {
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setUser');
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
this.initializeAccount();
|
||||
},
|
||||
|
||||
methods: {
|
||||
setLocale(locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
},
|
||||
|
||||
async initializeAccount() {
|
||||
const { pathname } = window.location;
|
||||
const accountId = accountIdFromPathname(pathname);
|
||||
|
||||
if (accountId) {
|
||||
await this.$store.dispatch('accounts/get');
|
||||
const { locale } = this.getAccount(accountId);
|
||||
this.setLocale(locale);
|
||||
}
|
||||
await this.$store.dispatch('accounts/get');
|
||||
const {
|
||||
locale,
|
||||
latest_chatwoot_version: latestChatwootVersion,
|
||||
} = this.getAccount(this.currentAccountId);
|
||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||
this.setLocale(locale);
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
vueActionCable.init(pubsubToken);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -82,6 +102,11 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
@import './assets/scss/app';
|
||||
.update-banner {
|
||||
height: var(--space-larger);
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user