Compare commits
501
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbe4bc2833 | ||
|
|
e9950afb1a | ||
|
|
f21298aeb9 | ||
|
|
7b6a56714f | ||
|
|
94a20af9db | ||
|
|
616371adbb | ||
|
|
2d4ef0c328 | ||
|
|
5e6e234afe | ||
|
|
18e9e771b5 | ||
|
|
0245e01763 | ||
|
|
1c9993e01b | ||
|
|
fcd40e9636 | ||
|
|
4f5c5e9f85 | ||
|
|
cb07ac16d1 | ||
|
|
4fa299ff15 | ||
|
|
71e9566854 | ||
|
|
a1a59684cd | ||
|
|
272f920811 | ||
|
|
e39d19b1e8 | ||
|
|
6c39aed882 | ||
|
|
8d43101892 | ||
|
|
f05535c5ad | ||
|
|
4503ba018d | ||
|
|
8bc2bc6d40 | ||
|
|
bfd192ebb2 | ||
|
|
6e13d22ff7 | ||
|
|
3e8b6e1aee | ||
|
|
3038aaef67 | ||
|
|
57a68666cb | ||
|
|
4acb824857 | ||
|
|
5c74674c2b | ||
|
|
52e49d5f9d | ||
|
|
e529e1206e | ||
|
|
a8009c7b39 | ||
|
|
99bb074656 | ||
|
|
9322112481 | ||
|
|
ce4cfee8bd | ||
|
|
f31fc2b375 | ||
|
|
e5f7807833 | ||
|
|
2483d99d3c | ||
|
|
336584c95a | ||
|
|
9ebabb9832 | ||
|
|
2acf09b3eb | ||
|
|
d18d3e9e2e | ||
|
|
deec1d213b | ||
|
|
25c3fb3c36 | ||
|
|
e2a6dc3e04 | ||
|
|
57feedbf25 | ||
|
|
71310c21ad | ||
|
|
623cd72897 | ||
|
|
04aa13e8aa | ||
|
|
6ab964b161 | ||
|
|
5598b4b27e | ||
|
|
64ae9f625a | ||
|
|
3dd3b7b3aa | ||
|
|
f451e59aec | ||
|
|
c04f67e8a5 | ||
|
|
6ddc99d066 | ||
|
|
4cbb3208e8 | ||
|
|
5f2a720920 | ||
|
|
acb7debd3f | ||
|
|
44f6a9ec3c | ||
|
|
24468d71fb | ||
|
|
26ef21a243 | ||
|
|
18235d3fb5 | ||
|
|
55972cc17a | ||
|
|
2daf1ae827 | ||
|
|
22421b5ac8 | ||
|
|
0d41853b0b | ||
|
|
355926897a | ||
|
|
4101a7b566 | ||
|
|
2d31535620 | ||
|
|
c54b469dcb | ||
|
|
29e8e41443 | ||
|
|
178bc80b25 | ||
|
|
ab039e14fd | ||
|
|
961c911c26 | ||
|
|
12ae49089b | ||
|
|
996f842882 | ||
|
|
616f702871 | ||
|
|
8015490aba | ||
|
|
b89c917198 | ||
|
|
7b8a3fcae0 | ||
|
|
2df83276e0 | ||
|
|
7b68a7639f | ||
|
|
e3b8c1fbb5 | ||
|
|
310c127693 | ||
|
|
37c0f1306c | ||
|
|
396ef02b0c | ||
|
|
ebefb2e201 | ||
|
|
6f09f20991 | ||
|
|
cfe86d9c06 | ||
|
|
84240e197a | ||
|
|
bafff90f18 | ||
|
|
961d810645 | ||
|
|
1a06bfd69c | ||
|
|
1cac2e86c3 | ||
|
|
04287a0924 | ||
|
|
c1c3a62412 | ||
|
|
b5dec067a1 | ||
|
|
f96f166196 | ||
|
|
9ed701065e | ||
|
|
180befbd42 | ||
|
|
4baa4363f4 | ||
|
|
1c8f41f387 | ||
|
|
7f0ca8b15d | ||
|
|
4c75bb68f4 | ||
|
|
ed82eb2932 | ||
|
|
b62166a18a | ||
|
|
bf2b75b281 | ||
|
|
c084ad5a68 | ||
|
|
dd3e658f19 | ||
|
|
453d7ad9fc | ||
|
|
47b6967dad | ||
|
|
d06cf27800 | ||
|
|
7f6a3b781f | ||
|
|
fde2b180fe | ||
|
|
2a1662c781 | ||
|
|
10d6e9551d | ||
|
|
69d46f278a | ||
|
|
0921a7c817 | ||
|
|
2a73f124ca | ||
|
|
ef33c60652 | ||
|
|
3a547de909 | ||
|
|
62e9fc1bc5 | ||
|
|
e052a061f4 | ||
|
|
9efadf8804 | ||
|
|
c8a40932cc | ||
|
|
9ddd428935 | ||
|
|
4d8ba0148c | ||
|
|
65c4ad70e1 | ||
|
|
9636478c2a | ||
|
|
65190422c4 | ||
|
|
a7bc855486 | ||
|
|
ff97536095 | ||
|
|
fcf0ba12d0 | ||
|
|
2cc82af344 | ||
|
|
c00855fce4 | ||
|
|
2d07577731 | ||
|
|
4c98006837 | ||
|
|
89e09857af | ||
|
|
12c338364e | ||
|
|
846f520ad2 | ||
|
|
6cbe1ed911 | ||
|
|
6c1ee4d965 | ||
|
|
1d8341504a | ||
|
|
f8ae6cd95c | ||
|
|
63dd2c5a93 | ||
|
|
1c47478104 | ||
|
|
39f14bebbd | ||
|
|
4b27fdf4db | ||
|
|
a6a0e78bbe | ||
|
|
703e19304d | ||
|
|
fa7bbdb0b3 | ||
|
|
d837065545 | ||
|
|
2a6f3356c9 | ||
|
|
c83105ce4f | ||
|
|
56d0b220f4 | ||
|
|
40ec0d109a | ||
|
|
480f34803b | ||
|
|
30f3928904 | ||
|
|
d188600559 | ||
|
|
4828071fc3 | ||
|
|
a3d21024a6 | ||
|
|
2b4f4f0b5c | ||
|
|
d66eb8b21f | ||
|
|
27419eef66 | ||
|
|
d7566c453d | ||
|
|
25ed66edf5 | ||
|
|
f72be94323 | ||
|
|
ea825d49da | ||
|
|
1d718b92b7 | ||
|
|
f3f665cabb | ||
|
|
8babf7dec3 | ||
|
|
5b480f563d | ||
|
|
3a77e672f8 | ||
|
|
3e75ac2de5 | ||
|
|
91c1061214 | ||
|
|
ec65b43993 | ||
|
|
1a81245987 | ||
|
|
2c3ad73542 | ||
|
|
59b1d59574 | ||
|
|
9de2edd300 | ||
|
|
66751b71dd | ||
|
|
7e415fbee3 | ||
|
|
50a927bac2 | ||
|
|
19ff738211 | ||
|
|
7c080fa9fa | ||
|
|
91e2da5e74 | ||
|
|
21691f2407 | ||
|
|
d495411ef5 | ||
|
|
918e1f25c2 | ||
|
|
10dd0ba647 | ||
|
|
550bea099c | ||
|
|
c7bf93ef8a | ||
|
|
ec7434d000 | ||
|
|
941e7e06cf | ||
|
|
09f46aa912 | ||
|
|
332ab5888c | ||
|
|
a940f4c1d2 | ||
|
|
5b4f92ccd2 | ||
|
|
ad03be4529 | ||
|
|
571e6bd0ec | ||
|
|
528da1d2cf | ||
|
|
4e8d17f017 | ||
|
|
3054a4cb59 | ||
|
|
71837bedf9 | ||
|
|
3c4514c9f7 | ||
|
|
4c7c1cd78a | ||
|
|
985b3f3bfc | ||
|
|
986abde531 | ||
|
|
b57063a8b8 | ||
|
|
022f4f899f | ||
|
|
191b8a64fe | ||
|
|
ea78020f2f | ||
|
|
dc1a41b136 | ||
|
|
28e7a5d228 | ||
|
|
40830046e8 | ||
|
|
d05c953eef | ||
|
|
4a770fdea7 | ||
|
|
239b1e2bf1 | ||
|
|
f963e00731 | ||
|
|
155a5b9947 | ||
|
|
26a75b250d | ||
|
|
20b84ee6a1 | ||
|
|
f1f14312fe | ||
|
|
212d7caab1 | ||
|
|
9cda16d0e8 | ||
|
|
a8acbb7427 | ||
|
|
1176e5eb8a | ||
|
|
996325f35b | ||
|
|
4f8ce7b597 | ||
|
|
f8e631a00c | ||
|
|
54bf33083e | ||
|
|
533f9f7fe4 | ||
|
|
82e5fc413e | ||
|
|
93daaea19b | ||
|
|
e6a49b5800 | ||
|
|
595e6e79f0 | ||
|
|
93d8157a55 | ||
|
|
6bea2cbc4a | ||
|
|
afb7e67795 | ||
|
|
bc813b71dd | ||
|
|
9ac4a1eb30 | ||
|
|
9bb455ec2d | ||
|
|
c410fe333a | ||
|
|
5e807c5875 | ||
|
|
9bed57c7d1 | ||
|
|
5c5381c0a9 | ||
|
|
138630b4ae | ||
|
|
9d0de04f7c | ||
|
|
2c3337b117 | ||
|
|
237358af24 | ||
|
|
35dfff0a5b | ||
|
|
86b2896333 | ||
|
|
e8a27bea4b | ||
|
|
0d465362ac | ||
|
|
e383e9259b | ||
|
|
2c34dca347 | ||
|
|
69b9123963 | ||
|
|
cac9fe1880 | ||
|
|
06ac7c829b | ||
|
|
9985f1c143 | ||
|
|
1ee6a8fe90 | ||
|
|
6eb4fa41ff | ||
|
|
8b91f0ffac | ||
|
|
d2aa19579e | ||
|
|
2e79a32db7 | ||
|
|
a86e236d19 | ||
|
|
c755978352 | ||
|
|
23ca6d56f9 | ||
|
|
429ec7194f | ||
|
|
c99d9f9557 | ||
|
|
9ff5978d6e | ||
|
|
cd28f401ba | ||
|
|
879a244f93 | ||
|
|
f64f2138db | ||
|
|
48f2e58e59 | ||
|
|
c715e396f0 | ||
|
|
c8fac08e0b | ||
|
|
7fd220c177 | ||
|
|
b3b76d00ff | ||
|
|
2f2ae88cba | ||
|
|
2b7989f52b | ||
|
|
d7314079c9 | ||
|
|
1017903ee1 | ||
|
|
d25e7fd54e | ||
|
|
7e89e3a34a | ||
|
|
39eaed7766 | ||
|
|
b1a4907ede | ||
|
|
a01d81a7e1 | ||
|
|
117d5301b4 | ||
|
|
aae6081d73 | ||
|
|
d03924b846 | ||
|
|
b333d0c986 | ||
|
|
9f3d155822 | ||
|
|
9203b097d2 | ||
|
|
12cb97db5f | ||
|
|
09971fd613 | ||
|
|
ca9d108741 | ||
|
|
f28533bc47 | ||
|
|
68f82c11a5 | ||
|
|
abc27fa791 | ||
|
|
d93a8d05bc | ||
|
|
373f5f5b64 | ||
|
|
ae7df60a75 | ||
|
|
412d750b6a | ||
|
|
f1a77ba934 | ||
|
|
ffc6364690 | ||
|
|
3a7633b564 | ||
|
|
91d1d44aa9 | ||
|
|
afc97faa8b | ||
|
|
1d0930ef63 | ||
|
|
776ea6d39a | ||
|
|
2c85098698 | ||
|
|
6bd0e074dc | ||
|
|
123fc73394 | ||
|
|
88cef88d80 | ||
|
|
8e79cf72e1 | ||
|
|
bfaca851f1 | ||
|
|
5618b5ebd3 | ||
|
|
4807052c37 | ||
|
|
44b1047b90 | ||
|
|
9c6c19c3e5 | ||
|
|
2764338453 | ||
|
|
4f0fcfcbcd | ||
|
|
fdbaf98c3b | ||
|
|
03bbd048a7 | ||
|
|
d481b9fbcf | ||
|
|
68dedc37ba | ||
|
|
958c0d7946 | ||
|
|
9553329092 | ||
|
|
d01f2063d0 | ||
|
|
590e4e9c1c | ||
|
|
0e903d2365 | ||
|
|
d6ce1ceeeb | ||
|
|
e8f56d0e56 | ||
|
|
b8dae07c0f | ||
|
|
e3f4be97c0 | ||
|
|
1f4d096804 | ||
|
|
1bcc438b21 | ||
|
|
8b3d13a920 | ||
|
|
dcdd58bae5 | ||
|
|
7b9a9831c1 | ||
|
|
7ab7bac6bf | ||
|
|
b988a01df3 | ||
|
|
105f9a27d2 | ||
|
|
132cf802d5 | ||
|
|
f2f33038f2 | ||
|
|
590ce788b9 | ||
|
|
a25179c342 | ||
|
|
aea9470b6a | ||
|
|
6382707146 | ||
|
|
49ef4e54ca | ||
|
|
db0d32f01d | ||
|
|
de1fb6ba4e | ||
|
|
7b8bcaba11 | ||
|
|
3f39b7ad1f | ||
|
|
a3547c5a1f | ||
|
|
0f776a173c | ||
|
|
d320605556 | ||
|
|
ca85d4e1c6 | ||
|
|
b994706265 | ||
|
|
d9f2cd0a6e | ||
|
|
f2a34ad8aa | ||
|
|
65e91f6a6f | ||
|
|
7dd1562b40 | ||
|
|
c70367bbd7 | ||
|
|
5a8eff35b5 | ||
|
|
1936369b12 | ||
|
|
354010a6e1 | ||
|
|
385eab6b96 | ||
|
|
198cd9b28d | ||
|
|
abdf00d2cf | ||
|
|
2c3160cfee | ||
|
|
708bddf4db | ||
|
|
020dcc4dc7 | ||
|
|
271263bcc2 | ||
|
|
c97d6021e0 | ||
|
|
d99997d17d | ||
|
|
9c5d062efc | ||
|
|
87f758ee1f | ||
|
|
d0a1ad746a | ||
|
|
86384bf876 | ||
|
|
55e5fa2205 | ||
|
|
65bad90b69 | ||
|
|
07aaa046c1 | ||
|
|
520bdabefe | ||
|
|
662967b5d3 | ||
|
|
6137a45214 | ||
|
|
5e0ce7793c | ||
|
|
ce3e38df0f | ||
|
|
2b88ecdfc4 | ||
|
|
683f259771 | ||
|
|
51fb3b7e8e | ||
|
|
3dedfee350 | ||
|
|
022383d942 | ||
|
|
59433d9d3c | ||
|
|
85e57c2e94 | ||
|
|
b081fe08b8 | ||
|
|
0d014d5d4b | ||
|
|
937338e3ea | ||
|
|
6e506b070e | ||
|
|
18c5c47836 | ||
|
|
257a294166 | ||
|
|
12f121f0d8 | ||
|
|
847d7ea082 | ||
|
|
c8041392dc | ||
|
|
be3c75e858 | ||
|
|
02c467b6db | ||
|
|
13fe439d9f | ||
|
|
2313edd244 | ||
|
|
09c9300c21 | ||
|
|
c0e905b5d8 | ||
|
|
04da8aa8dc | ||
|
|
32f7342cd6 | ||
|
|
74afb785c2 | ||
|
|
cae1e30160 | ||
|
|
99dfe1d5af | ||
|
|
3fa654f5c6 | ||
|
|
37fde64c67 | ||
|
|
3c2d6faf68 | ||
|
|
5d30dabf97 | ||
|
|
cef1f97d18 | ||
|
|
92705723f1 | ||
|
|
af971c9b18 | ||
|
|
2f2cdd7e7c | ||
|
|
d95283f5c2 | ||
|
|
402428fb4d | ||
|
|
5600b518ac | ||
|
|
0bbb28c432 | ||
|
|
cf91e9eb58 | ||
|
|
b529baa5eb | ||
|
|
c071f66cdd | ||
|
|
92fa9c4fdc | ||
|
|
f6e0453bb2 | ||
|
|
1a07828b1b | ||
|
|
54880b2342 | ||
|
|
e32f3e71e4 | ||
|
|
0874aeee2d | ||
|
|
f1fc658a0d | ||
|
|
e3193dcabc | ||
|
|
f825a22997 | ||
|
|
815322b27a | ||
|
|
474e65f4c8 | ||
|
|
3a35281b3f | ||
|
|
ac6de50b4d | ||
|
|
a34729c153 | ||
|
|
6b2736aa63 | ||
|
|
d1584eea72 | ||
|
|
527042afd1 | ||
|
|
bd1e69e4b4 | ||
|
|
76d4c22c2d | ||
|
|
5cdc7d654a | ||
|
|
821d49943a | ||
|
|
9c0259da6b | ||
|
|
ea2c442328 | ||
|
|
b93b8cdab6 | ||
|
|
e6505fc7a4 | ||
|
|
c5c36af529 | ||
|
|
026e03c307 | ||
|
|
37b7098673 | ||
|
|
7bd400772d | ||
|
|
cb2f86b983 | ||
|
|
905e77048f | ||
|
|
4505c5dda3 | ||
|
|
c9ce9e5b8f | ||
|
|
9e2f991484 | ||
|
|
80dcd17f6e | ||
|
|
a38ecf3dde | ||
|
|
9090eabb8a | ||
|
|
ca2506a941 | ||
|
|
1886d4ce08 | ||
|
|
4d49b81f1c | ||
|
|
2b736f4698 | ||
|
|
9ca21df9fd | ||
|
|
44837aa657 | ||
|
|
d04344c094 | ||
|
|
d45512df72 | ||
|
|
2731c2a5be | ||
|
|
610463c980 | ||
|
|
09ce85b30d | ||
|
|
17ff1f11a7 | ||
|
|
f4e121cc44 | ||
|
|
cf934450ab | ||
|
|
ad75a79135 | ||
|
|
d49989ace1 | ||
|
|
e69e0bc984 | ||
|
|
e877b01e00 | ||
|
|
da11feb39b | ||
|
|
138afd9af6 | ||
|
|
e753365493 | ||
|
|
91dc7733b0 | ||
|
|
463c09184c | ||
|
|
040e9a732f | ||
|
|
71c5a1e1d4 | ||
|
|
a521762dd6 | ||
|
|
406e8405eb | ||
|
|
3e6d23e071 | ||
|
|
0e3eb51d7a | ||
|
|
4ffe73a1ce |
+27
-28
@@ -7,17 +7,17 @@ defaults: &defaults
|
|||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
docker:
|
||||||
# specify the version you desire here
|
# specify the version you desire here
|
||||||
- image: cimg/ruby:3.1.3-browsers
|
- image: cimg/ruby:3.2.2-browsers
|
||||||
|
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||||
- image: cimg/postgres:14.1
|
- image: cimg/postgres:15.3
|
||||||
- image: cimg/redis:6.2.6
|
- image: cimg/redis:6.2.6
|
||||||
environment:
|
environment:
|
||||||
- RAILS_LOG_TO_STDOUT: false
|
- RAILS_LOG_TO_STDOUT: false
|
||||||
- COVERAGE: true
|
- COVERAGE: true
|
||||||
- LOG_LEVEL: warn
|
- LOG_LEVEL: warn
|
||||||
parallelism: 4
|
parallelism: 4
|
||||||
resource_class: large
|
resource_class: large
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||||
nvm install v16
|
nvm install v20
|
||||||
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
|
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
|
||||||
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
||||||
|
|
||||||
@@ -64,7 +64,6 @@ jobs:
|
|||||||
- ~/.bundle
|
- ~/.bundle
|
||||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||||
|
|
||||||
|
|
||||||
# Only necessary if app uses webpacker or yarn in some other way
|
# Only necessary if app uses webpacker or yarn in some other way
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
@@ -73,7 +72,7 @@ jobs:
|
|||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: yarn
|
name: yarn
|
||||||
command: yarn install --cache-folder ~/.cache/yarn
|
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||||
|
|
||||||
# Store yarn / webpacker cache
|
# Store yarn / webpacker cache
|
||||||
- save_cache:
|
- save_cache:
|
||||||
@@ -82,7 +81,7 @@ jobs:
|
|||||||
- ~/.cache/yarn
|
- ~/.cache/yarn
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Download cc-test-reporter
|
name: Download cc-test-reporter
|
||||||
command: |
|
command: |
|
||||||
mkdir -p ~/tmp
|
mkdir -p ~/tmp
|
||||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||||
@@ -104,9 +103,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
||||||
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
||||||
|
|
||||||
# Database setup
|
# Database setup
|
||||||
- run: yarn install --check-files
|
|
||||||
- run: bundle exec rake db:create
|
- run: bundle exec rake db:create
|
||||||
- run: bundle exec rake db:schema:load
|
- run: bundle exec rake db:schema:load
|
||||||
|
|
||||||
@@ -117,7 +115,7 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Rubocop
|
name: Rubocop
|
||||||
command: bundle exec rubocop
|
command: bundle exec rubocop
|
||||||
|
|
||||||
# - run:
|
# - run:
|
||||||
# name: Brakeman
|
# name: Brakeman
|
||||||
# command: bundle exec brakeman
|
# command: bundle exec brakeman
|
||||||
@@ -126,6 +124,21 @@ jobs:
|
|||||||
name: eslint
|
name: eslint
|
||||||
command: yarn run eslint
|
command: yarn run eslint
|
||||||
|
|
||||||
|
# Run frontend tests
|
||||||
|
- run:
|
||||||
|
name: Run frontend tests
|
||||||
|
command: |
|
||||||
|
mkdir -p ~/tmp/test-results/frontend_specs
|
||||||
|
~/tmp/cc-test-reporter before-build
|
||||||
|
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
|
||||||
|
yarn test:coverage --profile 10 \
|
||||||
|
--out ~/tmp/test-results/yarn.xml \
|
||||||
|
-- ${TESTFILES}
|
||||||
|
- run:
|
||||||
|
name: Code Climate Test Coverage
|
||||||
|
command: |
|
||||||
|
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||||
|
|
||||||
# Run rails tests
|
# Run rails tests
|
||||||
- run:
|
- run:
|
||||||
name: Run backend tests
|
name: Run backend tests
|
||||||
@@ -145,20 +158,6 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
|
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
|
||||||
|
|
||||||
- run:
|
|
||||||
name: Run frontend tests
|
|
||||||
command: |
|
|
||||||
mkdir -p ~/tmp/test-results/frontend_specs
|
|
||||||
~/tmp/cc-test-reporter before-build
|
|
||||||
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
|
|
||||||
yarn test:coverage --profile 10 \
|
|
||||||
--out ~/tmp/test-results/yarn.xml \
|
|
||||||
-- ${TESTFILES}
|
|
||||||
- run:
|
|
||||||
name: Code Climate Test Coverage
|
|
||||||
command: |
|
|
||||||
~/tmp/cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json buildreports/lcov.info
|
|
||||||
|
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: coverage
|
root: coverage
|
||||||
paths:
|
paths:
|
||||||
@@ -182,7 +181,7 @@ jobs:
|
|||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/build
|
at: ~/build
|
||||||
- run:
|
- run:
|
||||||
name: Download cc-test-reporter
|
name: Download cc-test-reporter
|
||||||
command: |
|
command: |
|
||||||
mkdir -p ~/tmp
|
mkdir -p ~/tmp
|
||||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||||
@@ -204,4 +203,4 @@ workflows:
|
|||||||
- build
|
- build
|
||||||
- upload-coverage:
|
- upload-coverage:
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|||||||
npm
|
npm
|
||||||
|
|
||||||
# Install rbenv and ruby
|
# Install rbenv and ruby
|
||||||
ARG RUBY_VERSION="3.1.3"
|
ARG RUBY_VERSION="3.2.2"
|
||||||
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
|
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
|
||||||
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
|
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
|
||||||
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
.bundle
|
.bundle
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
docker-compose.*
|
docker-compose.*
|
||||||
docker/Dockerfile
|
docker/Dockerfile
|
||||||
docker/dockerfiles
|
docker/dockerfiles
|
||||||
|
|||||||
+15
-3
@@ -110,6 +110,8 @@ AWS_REGION=
|
|||||||
RAILS_LOG_TO_STDOUT=true
|
RAILS_LOG_TO_STDOUT=true
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
LOG_SIZE=500
|
LOG_SIZE=500
|
||||||
|
# Configure this environment variable if you want to use lograge instead of rails logger
|
||||||
|
#LOGRAGE_ENABLED=true
|
||||||
|
|
||||||
### This environment variables are only required if you are setting up social media channels
|
### This environment variables are only required if you are setting up social media channels
|
||||||
|
|
||||||
@@ -159,9 +161,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
|||||||
# for mobile apps
|
# for mobile apps
|
||||||
# FCM_SERVER_KEY=
|
# FCM_SERVER_KEY=
|
||||||
|
|
||||||
## Bot Customizations
|
|
||||||
USE_INBOX_AVATAR_FOR_BOT=true
|
|
||||||
|
|
||||||
### APM and Error Monitoring configurations
|
### APM and Error Monitoring configurations
|
||||||
## Elastic APM
|
## Elastic APM
|
||||||
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
|
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
|
||||||
@@ -174,6 +173,9 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||||||
## LogRocket
|
## LogRocket
|
||||||
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
|
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
|
||||||
|
|
||||||
|
# MICROSOFT CLARITY
|
||||||
|
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||||
|
|
||||||
## Scout
|
## Scout
|
||||||
## https://scoutapm.com/docs/ruby/configuration
|
## https://scoutapm.com/docs/ruby/configuration
|
||||||
# SCOUT_KEY=YOURKEY
|
# SCOUT_KEY=YOURKEY
|
||||||
@@ -197,6 +199,8 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||||||
## Rack Attack configuration
|
## Rack Attack configuration
|
||||||
## To prevent and throttle abusive requests
|
## To prevent and throttle abusive requests
|
||||||
# ENABLE_RACK_ATTACK=true
|
# ENABLE_RACK_ATTACK=true
|
||||||
|
# RACK_ATTACK_IP_LIMIT=3000
|
||||||
|
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||||
|
|
||||||
## Running chatwoot as an API only server
|
## Running chatwoot as an API only server
|
||||||
## setting this value to true will disable the frontend dashboard endpoints
|
## setting this value to true will disable the frontend dashboard endpoints
|
||||||
@@ -228,3 +232,11 @@ AZURE_APP_SECRET=
|
|||||||
## Change these values to fine tune performance
|
## Change these values to fine tune performance
|
||||||
# control the concurrency setting of sidekiq
|
# control the concurrency setting of sidekiq
|
||||||
# SIDEKIQ_CONCURRENCY=10
|
# SIDEKIQ_CONCURRENCY=10
|
||||||
|
|
||||||
|
|
||||||
|
# AI powered features
|
||||||
|
## OpenAI key
|
||||||
|
# OPENAI_API_KEY=
|
||||||
|
|
||||||
|
# Sentiment analysis model file path
|
||||||
|
SENTIMENT_FILE_PATH=
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: 'Bug'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See the error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
|
|
||||||
Share a clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Browser logs**
|
|
||||||
|
|
||||||
Share the browser logs to debug the issue further.
|
|
||||||
|
|
||||||
**Server logs**
|
|
||||||
|
|
||||||
Share the server logs to debug the issue further.
|
|
||||||
|
|
||||||
**Environment**
|
|
||||||
|
|
||||||
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
|
||||||
|
|
||||||
- [ ] app.chatwoot.com (Chatwoot Cloud)
|
|
||||||
- [ ] Self-hosted
|
|
||||||
- - [ ] Linux VM
|
|
||||||
- - [ ] Docker
|
|
||||||
- - [ ] Kubernetes
|
|
||||||
- - [ ] Heroku
|
|
||||||
- - [ ] Other (Please specify)
|
|
||||||
|
|
||||||
|
|
||||||
**Desktop (please complete the following information)** (If applicable)
|
|
||||||
- OS: [e.g. Linux, Windows, MacOS]
|
|
||||||
- Browser [e.g. chrome, firefox, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information)** (If applicable)
|
|
||||||
- Device: [e.g. iPhone6, Pixel7]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Browser [e.g. stock browser, firefox, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Docker** (If applicable)
|
|
||||||
|
|
||||||
Please share the output of the following.
|
|
||||||
- `docker version`
|
|
||||||
- `docker info`
|
|
||||||
- `docker-compose version`
|
|
||||||
|
|
||||||
**Cloud Provider** (If applicable)
|
|
||||||
- [ ] AWS
|
|
||||||
- [ ] GCP
|
|
||||||
- [ ] Azure
|
|
||||||
- [ ] DigitalOcean
|
|
||||||
- [ ] Others
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
|
|
||||||
Add any other context about the problem here.
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: 'Bug'
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A concise description of what you expected to happen along with screenshots if applicable.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: To Reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. In this environment...
|
||||||
|
2. With this config...
|
||||||
|
3. Run '...'
|
||||||
|
4. See error...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: A concise description of what you expected to happen.
|
||||||
|
- type: dropdown
|
||||||
|
id: environment
|
||||||
|
attributes:
|
||||||
|
label: Environment
|
||||||
|
description: Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
||||||
|
options:
|
||||||
|
- app.chatwoot.com
|
||||||
|
- Linux VM
|
||||||
|
- Docker
|
||||||
|
- Kubernetes
|
||||||
|
- Heroku
|
||||||
|
- Other [please specify in the description]
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: provider
|
||||||
|
attributes:
|
||||||
|
label: Cloud Provider
|
||||||
|
description:
|
||||||
|
options:
|
||||||
|
- AWS
|
||||||
|
- GCP
|
||||||
|
- Azure
|
||||||
|
- DigitalOcean
|
||||||
|
- Other [please specify in the description]
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: Describe the platform you are using
|
||||||
|
options:
|
||||||
|
- Browser
|
||||||
|
- Mobile
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: The operating system and the version you are using.
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Browser and version
|
||||||
|
description: The name of the browser and version you are using.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Docker (if applicable)
|
||||||
|
description: |
|
||||||
|
Please share the output of the following.
|
||||||
|
- `docker version`
|
||||||
|
- `docker info`
|
||||||
|
- `docker-compose version`
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Report a security issue
|
||||||
|
url: https://www.chatwoot.com/docs/contributing-guide/security-reports/
|
||||||
|
about: Guidelines and steps to report a security vulnerability. Please report security vulnerabilities here.
|
||||||
|
- name: Product Documentation
|
||||||
|
url: https://www.chatwoot.com/help-center
|
||||||
|
about: If you have questions, are confused, or just want to understand our product better, please check out our documentation.
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Enhancement request
|
|
||||||
about: Suggest any enhancements for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your enhancement request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the enhancement request here.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: 🧙 Feature request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels: 'feature-request'
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Is your feature or enhancement related to a problem? Please describe.
|
||||||
|
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context or screenshots about the feature request here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# ref: https://github.com/amannn/action-semantic-pull-request
|
||||||
|
# ensure PR title is in semantic format
|
||||||
|
|
||||||
|
name: "Lint PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Log Lines Percentage Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
log_lines_check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Check for log lines and calculate percentage
|
||||||
|
run: |
|
||||||
|
# Define the log line pattern
|
||||||
|
LOG_LINE_PATTERN="Rails\.logger"
|
||||||
|
|
||||||
|
# Get the list of changed files in the pull request
|
||||||
|
CHANGED_FILES=$(git diff --name-only)
|
||||||
|
|
||||||
|
# Initialize a flag to track if any files have insufficient log lines
|
||||||
|
INSUFFICIENT_LOGS=0
|
||||||
|
|
||||||
|
for file in $CHANGED_FILES; do
|
||||||
|
if [[ $file =~ \.rb$ && ! $file =~ _spec\.rb$ ]]; then
|
||||||
|
# Count the total number of lines in the file
|
||||||
|
total_lines=$(wc -l < "$file")
|
||||||
|
|
||||||
|
# Count the number of log lines in the file
|
||||||
|
log_lines=$(grep -c "$LOG_LINE_PATTERN" "$file")
|
||||||
|
|
||||||
|
# Calculate the percentage of log lines
|
||||||
|
if [ "$total_lines" -gt 0 ]; then
|
||||||
|
percentage=$(awk "BEGIN { pc=100*${log_lines}/${total_lines}; i=int(pc); print (pc-i<0.5)?i:i+1 }")
|
||||||
|
else
|
||||||
|
percentage=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the percentage is less than 5%
|
||||||
|
if [ "$percentage" -lt 5 ]; then
|
||||||
|
echo "Error: Log lines percentage is less than 5% ($percentage%) in $file. Please add more log lines using Rails.logger statements."
|
||||||
|
INSUFFICIENT_LOGS=1
|
||||||
|
else
|
||||||
|
echo "Log lines percentage is $percentage% in $file. Code looks good!"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# If any files have insufficient log lines, fail the action
|
||||||
|
if [ "$INSUFFICIENT_LOGS" -eq 1 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -18,11 +18,12 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:10.8
|
image: postgres:15.3
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: ""
|
POSTGRES_PASSWORD: ""
|
||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
# needed because the postgres container does not provide a healthcheck
|
# needed because the postgres container does not provide a healthcheck
|
||||||
@@ -51,7 +52,8 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
- name: yarn
|
- name: yarn
|
||||||
run: yarn install
|
run: yarn install
|
||||||
@@ -74,6 +76,8 @@ jobs:
|
|||||||
- name: Run backend tests
|
- name: Run backend tests
|
||||||
run: |
|
run: |
|
||||||
bundle exec rspec --profile=10 --format documentation
|
bundle exec rspec --profile=10 --format documentation
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
- name: Upload rails log folder
|
- name: Upload rails log folder
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# #
|
||||||
|
# # This workflow will run specs related to response bot
|
||||||
|
# # This can only be activated in installations Where vector extension is available.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Run Response Bot spec
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: ankane/pgvector
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ""
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
# needed because the postgres container does not provide a healthcheck
|
||||||
|
# tmpfs makes DB faster by using RAM
|
||||||
|
options: >-
|
||||||
|
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
|
- name: yarn
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Create database
|
||||||
|
run: bundle exec rake db:create
|
||||||
|
|
||||||
|
- name: Seed database
|
||||||
|
run: bundle exec rake db:schema:load
|
||||||
|
|
||||||
|
- name: Enable ResponseBotService in installation
|
||||||
|
run: RAILS_ENV=test bundle exec rails runner "Features::ResponseBotService.new.enable_in_installation"
|
||||||
|
|
||||||
|
# Run Response Bot specs
|
||||||
|
- name: Run backend tests
|
||||||
|
run: |
|
||||||
|
bundle exec rspec spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 --profile=10 --format documentation
|
||||||
|
|
||||||
|
- name: Upload rails log folder
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: rails-log-folder
|
||||||
|
path: log
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: Run Size Limit Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: yarn
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: Run asset compile
|
||||||
|
run: bundle exec rake assets:precompile
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
|
- name: Size Check
|
||||||
|
run: yarn run size
|
||||||
+12
-2
@@ -61,8 +61,18 @@ test/cypress/videos/*
|
|||||||
/config/master.key
|
/config/master.key
|
||||||
/config/*.enc
|
/config/*.enc
|
||||||
|
|
||||||
.vscode/settings.json
|
#ignore files under .vscode directory
|
||||||
|
.vscode
|
||||||
|
|
||||||
# yalc for local testing
|
# yalc for local testing
|
||||||
.yalc
|
.yalc
|
||||||
yalc.lock
|
yalc.lock
|
||||||
|
|
||||||
|
/public/packs
|
||||||
|
/public/packs-test
|
||||||
|
/node_modules
|
||||||
|
/yarn-error.log
|
||||||
|
yarn-debug.log*
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
/storybook-static
|
||||||
+32
-7
@@ -8,15 +8,11 @@ Layout/LineLength:
|
|||||||
Max: 150
|
Max: 150
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Max: 125
|
Max: 175
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/conversation.rb'
|
|
||||||
- 'app/models/contact.rb'
|
|
||||||
- 'app/mailers/conversation_reply_mailer.rb'
|
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'app/builders/messages/facebook/message_builder.rb'
|
- 'app/models/conversation.rb'
|
||||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
|
||||||
- 'app/listeners/action_cable_listener.rb'
|
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
@@ -71,6 +67,10 @@ Rails/ApplicationController:
|
|||||||
- 'app/controllers/platform_controller.rb'
|
- 'app/controllers/platform_controller.rb'
|
||||||
- 'app/controllers/public_controller.rb'
|
- 'app/controllers/public_controller.rb'
|
||||||
- 'app/controllers/survey/responses_controller.rb'
|
- 'app/controllers/survey/responses_controller.rb'
|
||||||
|
Rails/FindEach:
|
||||||
|
Enabled: true
|
||||||
|
Include:
|
||||||
|
- 'app/**/*.rb'
|
||||||
Rails/CompactBlank:
|
Rails/CompactBlank:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Rails/EnvironmentVariableAccess:
|
Rails/EnvironmentVariableAccess:
|
||||||
@@ -83,6 +83,7 @@ Style/ClassAndModuleChildren:
|
|||||||
EnforcedStyle: compact
|
EnforcedStyle: compact
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/application.rb'
|
- 'config/application.rb'
|
||||||
|
- 'config/initializers/monkey_patches/*'
|
||||||
Style/MapToHash:
|
Style/MapToHash:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Style/HashSyntax:
|
Style/HashSyntax:
|
||||||
@@ -158,8 +159,32 @@ Rails/RenderInline:
|
|||||||
Performance/CollectionLiteralInLoop:
|
Performance/CollectionLiteralInLoop:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
- 'db/migrate/20210315101919_enable_email_channel.rb'
|
||||||
|
Rails/ThreeStateBooleanColumn:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb'
|
||||||
|
- 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb'
|
||||||
|
- 'db/migrate/20200606132552_create_labels.rb'
|
||||||
|
- 'db/migrate/20201027135006_create_working_hours.rb'
|
||||||
|
- 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb'
|
||||||
|
- 'db/migrate/20210114202310_create_teams.rb'
|
||||||
|
- 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb'
|
||||||
|
- 'db/migrate/20210428135041_add_campaigns.rb'
|
||||||
|
- 'db/migrate/20210602182058_add_hmac_to_api_channel.rb'
|
||||||
|
- 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb'
|
||||||
|
- 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb'
|
||||||
|
- 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb'
|
||||||
|
- 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb'
|
||||||
|
- 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb'
|
||||||
|
- 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb'
|
||||||
|
- 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb'
|
||||||
|
- 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb'
|
||||||
|
- 'db/migrate/20220511072655_add_archive_column_to_portal.rb'
|
||||||
|
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||||
|
RSpec/IndexedLet:
|
||||||
|
Enabled: false
|
||||||
RSpec/NamedSubject:
|
RSpec/NamedSubject:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
# we should bring this down
|
# we should bring this down
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 14
|
Max: 14
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.1.3
|
3.2.2
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/spec
|
||||||
+13
-1
@@ -23,6 +23,18 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
'@storybook/addon-links',
|
'@storybook/addon-links',
|
||||||
'@storybook/addon-essentials',
|
'@storybook/addon-essentials',
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Fix Storybook issue with PostCSS@8
|
||||||
|
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||||
|
*/
|
||||||
|
name: '@storybook/addon-postcss',
|
||||||
|
options: {
|
||||||
|
postcssLoaderOptions: {
|
||||||
|
implementation: require('postcss'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
webpackFinal: config => {
|
webpackFinal: config => {
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
@@ -35,7 +47,7 @@ module.exports = {
|
|||||||
|
|
||||||
newConfig.module.rules.push({
|
newConfig.module.rules.push({
|
||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
use: ['style-loader', 'css-loader', 'sass-loader'],
|
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
|
||||||
include: path.resolve(__dirname, '../app/javascript'),
|
include: path.resolve(__dirname, '../app/javascript'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import Vuex from 'vuex';
|
|||||||
import VueI18n from 'vue-i18n';
|
import VueI18n from 'vue-i18n';
|
||||||
import Vuelidate from 'vuelidate';
|
import Vuelidate from 'vuelidate';
|
||||||
import Multiselect from 'vue-multiselect';
|
import Multiselect from 'vue-multiselect';
|
||||||
|
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||||
|
|
||||||
import WootUiKit from '../app/javascript/dashboard/components';
|
import WootUiKit from '../app/javascript/dashboard/components';
|
||||||
import i18n from '../app/javascript/dashboard/i18n';
|
import i18n from '../app/javascript/dashboard/i18n';
|
||||||
|
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
|
||||||
|
|
||||||
import '../app/javascript/dashboard/assets/scss/storybook.scss';
|
import '../app/javascript/dashboard/assets/scss/storybook.scss';
|
||||||
|
|
||||||
@@ -15,6 +17,8 @@ Vue.use(VueI18n);
|
|||||||
Vue.use(Vuelidate);
|
Vue.use(Vuelidate);
|
||||||
Vue.use(WootUiKit);
|
Vue.use(WootUiKit);
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
|
||||||
|
|
||||||
Vue.component('multiselect', Multiselect);
|
Vue.component('multiselect', Multiselect);
|
||||||
Vue.component('fluent-icon', FluentIcon);
|
Vue.component('fluent-icon', FluentIcon);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.1.3'
|
ruby '3.2.2'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
gem 'rails', '~> 6.1', '>= 6.1.7.3'
|
gem 'rails', '~> 7.0.8.0'
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
|
||||||
@@ -34,21 +34,21 @@ gem 'commonmarker'
|
|||||||
# Validate Data against JSON Schema
|
# Validate Data against JSON Schema
|
||||||
gem 'json_schemer'
|
gem 'json_schemer'
|
||||||
# Rack middleware for blocking & throttling abusive requests
|
# Rack middleware for blocking & throttling abusive requests
|
||||||
gem 'rack-attack'
|
gem 'rack-attack', '>= 6.7.0'
|
||||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||||
gem 'down', '~> 5.0'
|
gem 'down'
|
||||||
# authentication type to fetch and send mail over oauth2.0
|
# authentication type to fetch and send mail over oauth2.0
|
||||||
gem 'gmail_xoauth'
|
gem 'gmail_xoauth'
|
||||||
# Prevent CSV injection
|
# Prevent CSV injection
|
||||||
gem 'csv-safe'
|
gem 'csv-safe'
|
||||||
# Support message translation
|
|
||||||
gem 'google-cloud-translate'
|
|
||||||
|
|
||||||
##-- for active storage --##
|
##-- for active storage --##
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
# original gem isn't maintained actively
|
||||||
|
# we wanted updated version of faraday which is a dependency for slack-ruby-client
|
||||||
|
gem 'azure-storage-blob', git: 'https://github.com/chatwoot/azure-storage-ruby', branch: 'chatwoot', require: false
|
||||||
gem 'google-cloud-storage', require: false
|
gem 'google-cloud-storage', require: false
|
||||||
gem 'image_processing', '~> 1.12.2'
|
gem 'image_processing'
|
||||||
|
|
||||||
##-- gems for database --#
|
##-- gems for database --#
|
||||||
gem 'groupdate'
|
gem 'groupdate'
|
||||||
@@ -62,38 +62,43 @@ gem 'activerecord-import'
|
|||||||
gem 'dotenv-rails'
|
gem 'dotenv-rails'
|
||||||
gem 'foreman'
|
gem 'foreman'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
gem 'webpacker', '~> 5.4', '>= 5.4.3'
|
gem 'webpacker'
|
||||||
# metrics on heroku
|
# metrics on heroku
|
||||||
gem 'barnes'
|
gem 'barnes'
|
||||||
|
|
||||||
##--- gems for authentication & authorization ---##
|
##--- gems for authentication & authorization ---##
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-secure_password', '~> 2.0', git: 'https://github.com/chatwoot/devise-secure_password'
|
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||||
gem 'devise_token_auth'
|
gem 'devise_token_auth'
|
||||||
# authorization
|
# authorization
|
||||||
gem 'jwt'
|
gem 'jwt'
|
||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
# super admin
|
# super admin
|
||||||
gem 'administrate'
|
gem 'administrate', '>= 0.19.0'
|
||||||
|
gem 'administrate-field-active_storage'
|
||||||
|
|
||||||
##--- gems for pubsub service ---##
|
##--- gems for pubsub service ---##
|
||||||
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||||
gem 'wisper', '2.0.0'
|
gem 'wisper', '2.0.0'
|
||||||
|
|
||||||
##--- gems for channels ---##
|
##--- gems for channels ---##
|
||||||
# TODO: bump up gem to 2.0
|
|
||||||
gem 'facebook-messenger'
|
gem 'facebook-messenger'
|
||||||
gem 'line-bot-api'
|
gem 'line-bot-api'
|
||||||
gem 'twilio-ruby', '~> 5.66'
|
gem 'twilio-ruby', '~> 5.66'
|
||||||
# twitty will handle subscription of twitter account events
|
# twitty will handle subscription of twitter account events
|
||||||
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||||
gem 'twitty'
|
gem 'twitty', '~> 0.1.5'
|
||||||
# facebook client
|
# facebook client
|
||||||
gem 'koala'
|
gem 'koala'
|
||||||
# slack client
|
# slack client
|
||||||
gem 'slack-ruby-client'
|
gem 'slack-ruby-client', '~> 2.0.0'
|
||||||
# for dialogflow integrations
|
# for dialogflow integrations
|
||||||
gem 'google-cloud-dialogflow'
|
gem 'google-cloud-dialogflow-v2'
|
||||||
|
gem 'grpc'
|
||||||
|
# Translate integrations
|
||||||
|
# 'google-cloud-translate' gem depends on faraday 2.0 version
|
||||||
|
# this dependency breaks the slack-ruby-client gem
|
||||||
|
gem 'google-cloud-translate-v3'
|
||||||
|
|
||||||
##-- apm and error monitoring ---#
|
##-- apm and error monitoring ---#
|
||||||
# loaded only when environment variables are set.
|
# loaded only when environment variables are set.
|
||||||
@@ -103,14 +108,14 @@ gem 'elastic-apm', require: false
|
|||||||
gem 'newrelic_rpm', require: false
|
gem 'newrelic_rpm', require: false
|
||||||
gem 'newrelic-sidekiq-metrics', require: false
|
gem 'newrelic-sidekiq-metrics', require: false
|
||||||
gem 'scout_apm', require: false
|
gem 'scout_apm', require: false
|
||||||
gem 'sentry-rails', require: false
|
gem 'sentry-rails', '>= 5.11.0', require: false
|
||||||
gem 'sentry-ruby', require: false
|
gem 'sentry-ruby', require: false
|
||||||
gem 'sentry-sidekiq', require: false
|
gem 'sentry-sidekiq', '>= 5.11.0', require: false
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '~> 6.4.2'
|
gem 'sidekiq', '>= 7.1.3'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron', '~> 1.6', '>= 1.6.0'
|
gem 'sidekiq-cron', '>= 1.10.1'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
@@ -129,7 +134,10 @@ gem 'procore-sift'
|
|||||||
|
|
||||||
# parse email
|
# parse email
|
||||||
gem 'email_reply_trimmer'
|
gem 'email_reply_trimmer'
|
||||||
gem 'html2text'
|
|
||||||
|
# TODO: we might have to fork this gem since 0.3.1 has hard depency on nokogir 1.10.
|
||||||
|
# and this gem hasn't been updated for a while.
|
||||||
|
gem 'html2text', git: 'https://github.com/chatwoot/html2text_ruby', branch: 'chatwoot'
|
||||||
|
|
||||||
# to calculate working hours
|
# to calculate working hours
|
||||||
gem 'working_hours'
|
gem 'working_hours'
|
||||||
@@ -144,12 +152,33 @@ gem 'stripe'
|
|||||||
## to populate db with sample data
|
## to populate db with sample data
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
|
|
||||||
# Can remove this in rails 7
|
# Include logrange conditionally in intializer using env variable
|
||||||
gem 'net-imap', require: false
|
gem 'lograge', '~> 0.13.0', require: false
|
||||||
gem 'net-pop', require: false
|
|
||||||
gem 'net-smtp', require: false
|
|
||||||
|
|
||||||
group :production, :staging do
|
# worked with microsoft refresh token
|
||||||
|
gem 'omniauth-oauth2'
|
||||||
|
|
||||||
|
gem 'audited', '~> 5.3'
|
||||||
|
|
||||||
|
# need for google auth
|
||||||
|
gem 'omniauth'
|
||||||
|
gem 'omniauth-google-oauth2'
|
||||||
|
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||||
|
|
||||||
|
## Gems for reponse bot
|
||||||
|
# adds cosine similarity to postgres using vector extension
|
||||||
|
gem 'neighbor'
|
||||||
|
gem 'pgvector'
|
||||||
|
# Convert Website HTML to Markdown
|
||||||
|
gem 'reverse_markdown'
|
||||||
|
|
||||||
|
# Sentiment analysis
|
||||||
|
gem 'informers'
|
||||||
|
|
||||||
|
### Gems required only in specific deployment environments ###
|
||||||
|
##############################################################
|
||||||
|
|
||||||
|
group :production do
|
||||||
# we dont want request timing out in development while using byebug
|
# we dont want request timing out in development while using byebug
|
||||||
gem 'rack-timeout'
|
gem 'rack-timeout'
|
||||||
end
|
end
|
||||||
@@ -158,7 +187,8 @@ group :development do
|
|||||||
gem 'annotate'
|
gem 'annotate'
|
||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
gem 'letter_opener'
|
gem 'letter_opener'
|
||||||
gem 'web-console'
|
gem 'scss_lint', require: false
|
||||||
|
gem 'web-console', '>= 4.2.1'
|
||||||
|
|
||||||
# used in swagger build
|
# used in swagger build
|
||||||
gem 'json_refs'
|
gem 'json_refs'
|
||||||
@@ -167,13 +197,13 @@ group :development do
|
|||||||
gem 'squasher'
|
gem 'squasher'
|
||||||
|
|
||||||
# profiling
|
# profiling
|
||||||
gem 'rack-mini-profiler', require: false
|
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
||||||
gem 'stackprof'
|
gem 'stackprof'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
# Cypress in rails.
|
# Cypress in rails.
|
||||||
gem 'cypress-on-rails', '~> 1.13', '>= 1.13.1'
|
gem 'cypress-on-rails'
|
||||||
# fast cleaning of database
|
# fast cleaning of database
|
||||||
gem 'database_cleaner'
|
gem 'database_cleaner'
|
||||||
# mock http calls
|
# mock http calls
|
||||||
@@ -190,12 +220,13 @@ group :development, :test do
|
|||||||
gem 'bundle-audit', require: false
|
gem 'bundle-audit', require: false
|
||||||
gem 'byebug', platform: :mri
|
gem 'byebug', platform: :mri
|
||||||
gem 'climate_control'
|
gem 'climate_control'
|
||||||
|
gem 'debug', '~> 1.8'
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails'
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
gem 'mock_redis'
|
gem 'mock_redis'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'rspec_junit_formatter'
|
gem 'rspec_junit_formatter'
|
||||||
gem 'rspec-rails', '~> 5.0.3'
|
gem 'rspec-rails'
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
@@ -206,13 +237,3 @@ group :development, :test do
|
|||||||
gem 'spring'
|
gem 'spring'
|
||||||
gem 'spring-watcher-listen'
|
gem 'spring-watcher-listen'
|
||||||
end
|
end
|
||||||
|
|
||||||
# worked with microsoft refresh token
|
|
||||||
gem 'omniauth-oauth2'
|
|
||||||
|
|
||||||
gem 'audited', '~> 5.2'
|
|
||||||
|
|
||||||
# need for google auth
|
|
||||||
gem 'omniauth'
|
|
||||||
gem 'omniauth-google-oauth2'
|
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
|
||||||
|
|||||||
+416
-352
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
|||||||
|
# Variables
|
||||||
|
APP_NAME := chatwoot
|
||||||
|
RAILS_ENV ?= development
|
||||||
|
|
||||||
|
# Targets
|
||||||
|
setup:
|
||||||
|
gem install bundler
|
||||||
|
bundle install
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
db_create:
|
||||||
|
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:create
|
||||||
|
|
||||||
|
db_migrate:
|
||||||
|
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:migrate
|
||||||
|
|
||||||
|
db_seed:
|
||||||
|
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:seed
|
||||||
|
|
||||||
|
db:
|
||||||
|
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:chatwoot_prepare
|
||||||
|
|
||||||
|
console:
|
||||||
|
RAILS_ENV=$(RAILS_ENV) bundle exec rails console
|
||||||
|
|
||||||
|
server:
|
||||||
|
RAILS_ENV=$(RAILS_ENV) bundle exec rails server -b 0.0.0.0 -p 3000
|
||||||
|
|
||||||
|
burn:
|
||||||
|
bundle && yarn
|
||||||
|
|
||||||
|
run:
|
||||||
|
@if [ -f ./.overmind.sock ]; then \
|
||||||
|
echo "Overmind is already running. Use 'make force_run' to start a new instance."; \
|
||||||
|
else \
|
||||||
|
overmind start -f Procfile.dev; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
force_run:
|
||||||
|
rm -f ./.overmind.sock
|
||||||
|
overmind start -f Procfile.dev
|
||||||
|
|
||||||
|
debug:
|
||||||
|
overmind connect backend
|
||||||
|
|
||||||
|
debug_worker:
|
||||||
|
overmind connect worker
|
||||||
|
|
||||||
|
docker:
|
||||||
|
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
|
||||||
|
|
||||||
|
.PHONY: setup db_create db_migrate db_seed db console server burn docker run force_run debug debug_worker
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare
|
release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare && echo $SOURCE_VERSION > .git_sha
|
||||||
web: bundle exec rails ip_lookup:setup && bin/rails server -p $PORT -e $RAILS_ENV
|
web: bundle exec rails ip_lookup:setup && bin/rails server -p $PORT -e $RAILS_ENV
|
||||||
worker: bundle exec rails ip_lookup:setup && bundle exec sidekiq -C config/sidekiq.yml
|
worker: bundle exec rails ip_lookup:setup && bundle exec sidekiq -C config/sidekiq.yml
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
backend: bin/rails s -p 3000
|
backend: bin/rails s -p 3000
|
||||||
frontend: bin/webpack-dev-server
|
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||||
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
|
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
|
||||||
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
|
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.2.0
|
2.17.0
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.1.0
|
2.3.0
|
||||||
|
|||||||
@@ -60,6 +60,9 @@
|
|||||||
],
|
],
|
||||||
"stack": "heroku-20",
|
"stack": "heroku-20",
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
|
{
|
||||||
|
"url": "heroku/nodejs"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "heroku/ruby"
|
"url": "heroku/ruby"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//= link_tree ../images
|
//= link_tree ../images
|
||||||
//= link administrate/application.css
|
//= link administrate/application.css
|
||||||
//= link administrate/application.js
|
//= link administrate/application.js
|
||||||
|
//= link administrate-field-active_storage/application.css
|
||||||
//= link dashboardChart.js
|
//= link dashboardChart.js
|
||||||
//= link secretField.js
|
//= link secretField.js
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function prepareData(data) {
|
|||||||
|
|
||||||
function getChartOptions() {
|
function getChartOptions() {
|
||||||
var fontFamily =
|
var fontFamily =
|
||||||
'Inter,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||||
return {
|
return {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
legend: { labels: { fontFamily } },
|
legend: { labels: { fontFamily } },
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Typography
|
// Typography
|
||||||
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
$base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
sans-serif !default;
|
sans-serif !default;
|
||||||
$heading-font-family: $base-font-family !default;
|
$heading-font-family: $base-font-family !default;
|
||||||
|
|
||||||
$base-font-size: 10px !default;
|
$base-font-size: 16px !default;
|
||||||
|
|
||||||
$base-line-height: 1.5 !default;
|
$base-line-height: 1.5 !default;
|
||||||
$heading-line-height: 1.2 !default;
|
$heading-line-height: 1.2 !default;
|
||||||
|
|||||||
@@ -63,28 +63,43 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
@conversation ||= Conversation.where(conversation_params).find_by(
|
||||||
|
"additional_attributes ->> 'type' = 'instagram_direct_message'"
|
||||||
|
) || build_conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_content
|
def message_content
|
||||||
@messaging[:message][:text]
|
@messaging[:message][:text]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def story_reply_attributes
|
||||||
|
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
|
||||||
|
end
|
||||||
|
|
||||||
def build_message
|
def build_message
|
||||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||||
return if message_content.blank? && all_unsupported_files?
|
return if message_content.blank? && all_unsupported_files?
|
||||||
|
|
||||||
@message = conversation.messages.create!(message_params)
|
@message = conversation.messages.create!(message_params)
|
||||||
|
save_story_id
|
||||||
|
|
||||||
attachments.each do |attachment|
|
attachments.each do |attachment|
|
||||||
process_attachment(attachment)
|
process_attachment(attachment)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def save_story_id
|
||||||
|
return if story_reply_attributes.blank?
|
||||||
|
|
||||||
|
@message.save_story_info(story_reply_attributes)
|
||||||
|
end
|
||||||
|
|
||||||
def build_conversation
|
def build_conversation
|
||||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
|
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
|
||||||
|
|
||||||
Conversation.create!(conversation_params.merge(
|
Conversation.create!(conversation_params.merge(
|
||||||
contact_inbox_id: @contact_inbox.id
|
contact_inbox_id: @contact_inbox.id,
|
||||||
|
additional_attributes: { type: 'instagram_direct_message' }
|
||||||
))
|
))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -92,10 +107,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
{
|
{
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
contact_id: contact.id,
|
contact_id: contact.id
|
||||||
additional_attributes: {
|
|
||||||
type: 'instagram_direct_message'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -48,17 +48,22 @@ class Messages::MessageBuilder
|
|||||||
def process_emails
|
def process_emails
|
||||||
return unless @conversation.inbox&.inbox_type == 'Email'
|
return unless @conversation.inbox&.inbox_type == 'Email'
|
||||||
|
|
||||||
cc_emails = []
|
cc_emails = process_email_string(@params[:cc_emails])
|
||||||
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails].present?
|
bcc_emails = process_email_string(@params[:bcc_emails])
|
||||||
|
to_emails = process_email_string(@params[:to_emails])
|
||||||
|
|
||||||
bcc_emails = []
|
all_email_addresses = cc_emails + bcc_emails + to_emails
|
||||||
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails].present?
|
|
||||||
|
|
||||||
all_email_addresses = cc_emails + bcc_emails
|
|
||||||
validate_email_addresses(all_email_addresses)
|
validate_email_addresses(all_email_addresses)
|
||||||
|
|
||||||
@message.content_attributes[:cc_emails] = cc_emails
|
@message.content_attributes[:cc_emails] = cc_emails
|
||||||
@message.content_attributes[:bcc_emails] = bcc_emails
|
@message.content_attributes[:bcc_emails] = bcc_emails
|
||||||
|
@message.content_attributes[:to_emails] = to_emails
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_email_string(email_string)
|
||||||
|
return [] if email_string.blank?
|
||||||
|
|
||||||
|
email_string.gsub(/\s+/, '').split(',')
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_email_addresses(all_emails)
|
def validate_email_addresses(all_emails)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class V2::ReportBuilder
|
|||||||
|
|
||||||
# For backward compatible with old report
|
# For backward compatible with old report
|
||||||
def build
|
def build
|
||||||
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
|
if %w[avg_first_response_time avg_resolution_time reply_time].include?(params[:metric])
|
||||||
timeseries.each_with_object([]) do |p, arr|
|
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]] }
|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
|
||||||
end
|
end
|
||||||
@@ -33,18 +33,19 @@ class V2::ReportBuilder
|
|||||||
|
|
||||||
def summary
|
def summary
|
||||||
{
|
{
|
||||||
conversations_count: conversations_count.values.sum,
|
conversations_count: conversations.count,
|
||||||
incoming_messages_count: incoming_messages_count.values.sum,
|
incoming_messages_count: incoming_messages.count,
|
||||||
outgoing_messages_count: outgoing_messages_count.values.sum,
|
outgoing_messages_count: outgoing_messages.count,
|
||||||
avg_first_response_time: avg_first_response_time_summary,
|
avg_first_response_time: avg_first_response_time_summary,
|
||||||
avg_resolution_time: avg_resolution_time_summary,
|
avg_resolution_time: avg_resolution_time_summary,
|
||||||
resolutions_count: resolutions_count.values.sum
|
resolutions_count: resolutions.count,
|
||||||
|
reply_time: reply_time_summary
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_metrics
|
def conversation_metrics
|
||||||
if params[:type].equal?(:account)
|
if params[:type].equal?(:account)
|
||||||
conversations
|
live_conversations
|
||||||
else
|
else
|
||||||
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
|
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
|
||||||
end
|
end
|
||||||
@@ -89,17 +90,16 @@ class V2::ReportBuilder
|
|||||||
email: @user.email,
|
email: @user.email,
|
||||||
thumbnail: @user.avatar_url,
|
thumbnail: @user.avatar_url,
|
||||||
availability: account_user.availability_status,
|
availability: account_user.availability_status,
|
||||||
metric: conversations
|
metric: live_conversations
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations
|
def live_conversations
|
||||||
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
||||||
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
|
||||||
metric = {
|
metric = {
|
||||||
open: @open_conversations.count,
|
open: @open_conversations.count,
|
||||||
unattended: @open_conversations.count - first_response_count
|
unattended: @open_conversations.unattended.count
|
||||||
}
|
}
|
||||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||||
metric
|
metric
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
before_action :portal
|
before_action :portal
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :fetch_article, except: [:index, :create, :attach_file]
|
before_action :fetch_article, except: [:index, :create, :reorder]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@portal_articles = @portal.articles
|
@portal_articles = @portal.articles
|
||||||
@all_articles = @portal_articles.search(list_params)
|
@all_articles = @portal_articles.search(list_params)
|
||||||
@articles_count = @all_articles.count
|
@articles_count = @all_articles.count
|
||||||
@articles = @all_articles.order_by_updated_at.page(@current_page)
|
|
||||||
|
@articles = if list_params[:category_slug].present?
|
||||||
|
@all_articles.order_by_position.page(@current_page).per(50)
|
||||||
|
else
|
||||||
|
@all_articles.order_by_updated_at.page(@current_page)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@article = @portal.articles.create!(article_params)
|
@article = @portal.articles.create!(article_params)
|
||||||
@article.associate_root_article(article_params[:associated_article_id])
|
@article.associate_root_article(article_params[:associated_article_id])
|
||||||
@@ -18,10 +26,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||||||
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
|
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@article.update!(article_params) if params[:article].present?
|
@article.update!(article_params) if params[:article].present?
|
||||||
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
|
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
|
||||||
@@ -32,15 +36,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_file
|
def reorder
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
Article.update_positions(params[:positions_hash])
|
||||||
key: nil,
|
head :ok
|
||||||
io: params[:background_image].tempfile,
|
|
||||||
filename: params[:background_image].original_filename,
|
|
||||||
content_type: params[:background_image].content_type
|
|
||||||
)
|
|
||||||
file_blob.save!
|
|
||||||
render json: { file_url: url_for(file_blob) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||||||
@automation_rules = Current.account.automation_rules
|
@automation_rules = Current.account.automation_rules
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||||
@automation_rule.actions = params[:actions]
|
@automation_rule.actions = params[:actions]
|
||||||
@@ -18,18 +20,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||||||
@automation_rule
|
@automation_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_file
|
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
|
||||||
key: nil,
|
|
||||||
io: params[:attachment].tempfile,
|
|
||||||
filename: params[:attachment].original_filename,
|
|
||||||
content_type: params[:attachment].content_type
|
|
||||||
)
|
|
||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
automation_rule_update
|
automation_rule_update
|
||||||
|
|||||||
@@ -14,8 +14,18 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||||||
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||||
set_instagram_id(page_access_token, facebook_channel)
|
set_instagram_id(page_access_token, facebook_channel)
|
||||||
set_avatar(@facebook_inbox, page_id)
|
set_avatar(@facebook_inbox, page_id)
|
||||||
rescue StandardError => e
|
end
|
||||||
ChatwootExceptionTracker.new(e).capture_exception
|
rescue StandardError => e
|
||||||
|
ChatwootExceptionTracker.new(e).capture_exception
|
||||||
|
Rails.logger.error "Error in register_facebook_page: #{e.message}"
|
||||||
|
# Additional log statements
|
||||||
|
log_additional_info
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_additional_info
|
||||||
|
Rails.logger.debug do
|
||||||
|
"user_access_token: #{params[:user_access_token]} , page_access_token: #{params[:page_access_token]} ,
|
||||||
|
page_id: #{params[:page_id]}, inbox_name: #{params[:inbox_name]}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -30,6 +40,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
instagram_id = response['instagram_business_account']['id']
|
instagram_id = response['instagram_business_account']['id']
|
||||||
facebook_channel.update(instagram_id: instagram_id)
|
facebook_channel.update(instagram_id: instagram_id)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "Error in set_instagram_id: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# get params[:inbox_id], current_account. params[:omniauth_token]
|
# get params[:inbox_id], current_account. params[:omniauth_token]
|
||||||
@@ -61,6 +73,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||||||
fb_page&.reauthorized!
|
fb_page&.reauthorized!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e).capture_exception
|
ChatwootExceptionTracker.new(e).capture_exception
|
||||||
|
Rails.logger.error "Error in update_fb_page: #{e.message}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -77,7 +90,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||||||
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
||||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.error "Error in long_lived_token: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_already_existing_facebook_pages(data)
|
def mark_already_existing_facebook_pages(data)
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
|||||||
@campaigns = Current.account.campaigns
|
@campaigns = Current.account.campaigns
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@campaign = Current.account.campaigns.create!(campaign_params)
|
@campaign = Current.account.campaigns.create!(campaign_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@campaign.update!(campaign_params)
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@campaign.destroy!
|
@campaign.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@campaign.update!(campaign_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def campaign
|
def campaign
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
|||||||
@categories = @portal.categories.search(params)
|
@categories = @portal.categories.search(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@category = @portal.categories.create!(category_params)
|
@category = @portal.categories.create!(category_params)
|
||||||
@category.related_categories << related_categories_records
|
@category.related_categories << related_categories_records
|
||||||
@@ -17,8 +19,6 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
|||||||
@category.save!
|
@category.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@category.update!(category_params)
|
@category.update!(category_params)
|
||||||
@category.related_categories = related_categories_records if related_categories_records.any?
|
@category.related_categories = related_categories_records if related_categories_records.any?
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_twilio
|
def authenticate_twilio
|
||||||
client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
|
client = if permitted_params[:api_key_sid].present?
|
||||||
|
Twilio::REST::Client.new(permitted_params[:api_key_sid], permitted_params[:auth_token], permitted_params[:account_sid])
|
||||||
|
else
|
||||||
|
Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
|
||||||
|
end
|
||||||
client.messages.list(limit: 1)
|
client.messages.list(limit: 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -40,6 +44,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
|||||||
@twilio_channel = Current.account.twilio_sms.create!(
|
@twilio_channel = Current.account.twilio_sms.create!(
|
||||||
account_sid: permitted_params[:account_sid],
|
account_sid: permitted_params[:account_sid],
|
||||||
auth_token: permitted_params[:auth_token],
|
auth_token: permitted_params[:auth_token],
|
||||||
|
api_key_sid: permitted_params[:api_key_sid],
|
||||||
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
|
messaging_service_sid: permitted_params[:messaging_service_sid].presence,
|
||||||
phone_number: phone_number,
|
phone_number: phone_number,
|
||||||
medium: medium
|
medium: medium
|
||||||
@@ -52,7 +57,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
|||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.require(:twilio_channel).permit(
|
params.require(:twilio_channel).permit(
|
||||||
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium
|
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium, :api_key_sid
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
|
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
|
||||||
|
include HmacConcern
|
||||||
before_action :ensure_inbox, only: [:create]
|
before_action :ensure_inbox, only: [:create]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@contact_inbox = ContactInboxBuilder.new(
|
@contact_inbox = ContactInboxBuilder.new(
|
||||||
contact: @contact,
|
contact: @contact,
|
||||||
inbox: @inbox,
|
inbox: @inbox,
|
||||||
source_id: params[:source_id]
|
source_id: params[:source_id],
|
||||||
|
hmac_verified: hmac_verified?
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
|
|||||||
@notes = @contact.notes.latest.includes(:user)
|
@notes = @contact.notes.latest.includes(:user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@note = @contact.notes.create!(note_params)
|
@note = @contact.notes.create!(note_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@note.update(note_params)
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@note.destroy!
|
@note.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@note.update(note_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def note
|
def note
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction]
|
sort_on :name, internal_name: :order_on_name, type: :scope, scope_params: [:direction]
|
||||||
sort_on :phone_number, type: :string
|
sort_on :phone_number, type: :string
|
||||||
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
|
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
|
||||||
|
sort_on :created_at, internal_name: :order_on_created_at, type: :scope, scope_params: [:direction]
|
||||||
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
|
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
|
||||||
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
|
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
|
||||||
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
|
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
|
||||||
@@ -17,7 +18,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts_count = resolved_contacts.count
|
@contacts_count = resolved_contacts.count
|
||||||
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
|
@contacts = fetch_contacts(resolved_contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
@@ -28,7 +29,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
search: "%#{params[:q].strip}%"
|
search: "%#{params[:q].strip}%"
|
||||||
)
|
)
|
||||||
@contacts_count = contacts.count
|
@contacts_count = contacts.count
|
||||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
@contacts = fetch_contacts(contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def import
|
def import
|
||||||
@@ -42,6 +43,12 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def export
|
||||||
|
column_names = params['column_names']
|
||||||
|
Account::ContactsExportJob.perform_later(Current.account.id, column_names)
|
||||||
|
head :ok, message: I18n.t('errors.contacts.export.success')
|
||||||
|
end
|
||||||
|
|
||||||
# returns online contacts
|
# returns online contacts
|
||||||
def active
|
def active
|
||||||
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||||
@@ -56,7 +63,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
|
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
|
||||||
contacts = result[:contacts]
|
contacts = result[:contacts]
|
||||||
@contacts_count = result[:count]
|
@contacts_count = result[:count]
|
||||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
@contacts = fetch_contacts(contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def contactable_inboxes
|
def contactable_inboxes
|
||||||
@@ -118,17 +125,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
@current_page = params[:page] || 1
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contacts_with_conversation_count(contacts)
|
def fetch_contacts(contacts)
|
||||||
conversation_count_sub_query = 'SELECT COUNT(*) FROM "conversations" WHERE "conversations"."contact_id" = "contacts"."id"'
|
contacts_with_avatar = filtrate(contacts)
|
||||||
contacts_with_conversation_count = filtrate(contacts)
|
.includes([{ avatar_attachment: [:blob] }])
|
||||||
.select("contacts.*, (#{conversation_count_sub_query}) as conversations_count")
|
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||||
.group('contacts.id')
|
|
||||||
.includes([{ avatar_attachment: [:blob] }])
|
|
||||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
|
||||||
|
|
||||||
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||||
|
|
||||||
contacts_with_conversation_count
|
contacts_with_avatar
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
|
||||||
include EnsureCurrentAccountHelper
|
|
||||||
before_action :conversation
|
before_action :conversation
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
include DateRangeHelper
|
include DateRangeHelper
|
||||||
|
include HmacConcern
|
||||||
|
|
||||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
||||||
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
||||||
@@ -22,6 +23,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
@conversations_count = result[:count]
|
@conversations_count = result[:count]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attachments
|
||||||
|
@attachments = @conversation.attachments
|
||||||
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
||||||
@@ -29,8 +36,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def filter
|
def filter
|
||||||
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
|
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
|
||||||
@conversations = result[:conversations]
|
@conversations = result[:conversations]
|
||||||
@@ -64,13 +69,14 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def toggle_priority
|
||||||
|
@conversation.toggle_priority(params[:priority])
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def toggle_typing_status
|
def toggle_typing_status
|
||||||
case params[:typing_status]
|
typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, current_user, params)
|
||||||
when 'on'
|
typing_status_manager.toggle_typing_status
|
||||||
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
|
|
||||||
when 'off'
|
|
||||||
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
|
|
||||||
end
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -99,9 +105,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_conversation_status
|
def set_conversation_status
|
||||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
|
||||||
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
|
||||||
# status = params[:status] == 'bot' ? 'pending' : params[:status]
|
|
||||||
@conversation.status = params[:status]
|
@conversation.status = params[:status]
|
||||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||||
end
|
end
|
||||||
@@ -111,11 +114,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
@conversation.update_assignee(@agent)
|
@conversation.update_assignee(@agent)
|
||||||
end
|
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)
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
|
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
|
||||||
authorize @conversation.inbox, :show?
|
authorize @conversation.inbox, :show?
|
||||||
@@ -152,7 +150,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
ContactInboxBuilder.new(
|
ContactInboxBuilder.new(
|
||||||
contact: @contact,
|
contact: @contact,
|
||||||
inbox: @inbox,
|
inbox: @inbox,
|
||||||
source_id: params[:source_id]
|
source_id: params[:source_id],
|
||||||
|
hmac_verified: hmac_verified?
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_csat_survey_responses
|
def set_csat_survey_responses
|
||||||
@csat_survey_responses = filtrate(
|
base_query = Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
||||||
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
|
@csat_survey_responses = filtrate(base_query).filter_by_created_at(range)
|
||||||
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
|
.filter_by_assigned_agent_id(params[:user_ids])
|
||||||
|
.filter_by_inbox_id(params[:inbox_id])
|
||||||
|
.filter_by_team_id(params[:team_id])
|
||||||
|
.filter_by_rating(params[:rating])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_current_page_surveys
|
def set_current_page_surveys
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :check_authorization
|
||||||
before_action :fetch_custom_filters, except: [:create]
|
before_action :fetch_custom_filters, except: [:create]
|
||||||
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
|
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
|
||||||
DEFAULT_FILTER_TYPE = 'conversation'.freeze
|
DEFAULT_FILTER_TYPE = 'conversation'.freeze
|
||||||
@@ -11,6 +12,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
|
|||||||
@custom_filter = current_user.custom_filters.create!(
|
@custom_filter = current_user.custom_filters.create!(
|
||||||
permitted_payload.merge(account_id: Current.account.id)
|
permitted_payload.merge(account_id: Current.account.id)
|
||||||
)
|
)
|
||||||
|
render json: { error: @custom_filter.errors.messages }, status: :unprocessable_entity and return unless @custom_filter.valid?
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
|||||||
before_action :fetch_inbox
|
before_action :fetch_inbox
|
||||||
before_action :current_agents_ids, only: [:create, :update]
|
before_action :current_agents_ids, only: [:create, :update]
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @inbox, :show?
|
||||||
|
fetch_updated_agents
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize @inbox, :create?
|
authorize @inbox, :create?
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@@ -10,11 +15,6 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
|||||||
fetch_updated_agents
|
fetch_updated_agents
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
|
||||||
authorize @inbox, :show?
|
|
||||||
fetch_updated_agents
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize @inbox, :update?
|
authorize @inbox, :update?
|
||||||
update_agents_list
|
update_agents_list
|
||||||
|
|||||||
@@ -44,26 +44,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
def update
|
def update
|
||||||
@inbox.update!(permitted_params.except(:channel))
|
@inbox.update!(permitted_params.except(:channel))
|
||||||
update_inbox_working_hours
|
update_inbox_working_hours
|
||||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
update_channel if channel_update_required?
|
||||||
|
|
||||||
# Inbox update doesn't necessarily need channel attributes
|
|
||||||
return if permitted_params(channel_attributes)[:channel].blank?
|
|
||||||
|
|
||||||
if @inbox.inbox_type == 'Email'
|
|
||||||
begin
|
|
||||||
validate_email_channel(channel_attributes)
|
|
||||||
rescue StandardError => e
|
|
||||||
render json: { message: e }, status: :unprocessable_entity and return
|
|
||||||
end
|
|
||||||
@inbox.channel.reauthorized!
|
|
||||||
end
|
|
||||||
|
|
||||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
|
||||||
update_channel_feature_flags
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_inbox_working_hours
|
|
||||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def agent_bot
|
def agent_bot
|
||||||
@@ -82,7 +63,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
::DeleteObjectJob.perform_later(@inbox) if @inbox.present?
|
::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
|
||||||
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -103,6 +84,35 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
|
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_inbox_working_hours
|
||||||
|
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_channel
|
||||||
|
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||||
|
return if permitted_params(channel_attributes)[:channel].blank?
|
||||||
|
|
||||||
|
validate_and_update_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
|
||||||
|
|
||||||
|
reauthorize_and_update_channel(channel_attributes)
|
||||||
|
update_channel_feature_flags
|
||||||
|
end
|
||||||
|
|
||||||
|
def channel_update_required?
|
||||||
|
permitted_params(get_channel_attributes(@inbox.channel_type))[:channel].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_and_update_email_channel(channel_attributes)
|
||||||
|
validate_email_channel(channel_attributes)
|
||||||
|
rescue StandardError => e
|
||||||
|
render json: { message: e }, status: :unprocessable_entity and return
|
||||||
|
end
|
||||||
|
|
||||||
|
def reauthorize_and_update_channel(channel_attributes)
|
||||||
|
@inbox.channel.reauthorized! if @inbox.channel.respond_to?(:reauthorized!)
|
||||||
|
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||||
|
end
|
||||||
|
|
||||||
def update_channel_feature_flags
|
def update_channel_feature_flags
|
||||||
return unless @inbox.web_widget?
|
return unless @inbox.web_widget?
|
||||||
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
||||||
@@ -114,10 +124,13 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
def inbox_attributes
|
def inbox_attributes
|
||||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||||
:lock_to_single_conversation]
|
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name]
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params(channel_attributes = [])
|
def permitted_params(channel_attributes = [])
|
||||||
|
# We will remove this line after fixing https://linear.app/chatwoot/issue/CW-1567/null-value-passed-as-null-string-to-backend
|
||||||
|
params.each { |k, v| params[k] = params[k] == 'null' ? nil : v }
|
||||||
|
|
||||||
params.permit(
|
params.permit(
|
||||||
*inbox_attributes,
|
*inbox_attributes,
|
||||||
channel: [:type, *channel_attributes]
|
channel: [:type, *channel_attributes]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_hook, only: [:update, :destroy]
|
before_action :fetch_hook, except: [:create]
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -10,6 +10,10 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
|||||||
@hook.update!(permitted_params.slice(:status, :settings))
|
@hook.update!(permitted_params.slice(:status, :settings))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_event
|
||||||
|
render json: { message: @hook.process_event(params[:event]) }
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@hook.destroy!
|
@hook.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_admin_authorization?
|
before_action :check_admin_authorization?
|
||||||
before_action :fetch_hook, only: [:update, :destroy]
|
before_action :fetch_hook, only: [:update, :destroy, :list_all_channels]
|
||||||
|
|
||||||
|
def list_all_channels
|
||||||
|
@channels = channel_builder.fetch_channels
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
hook_builder = Integrations::Slack::HookBuilder.new(
|
||||||
builder = Integrations::Slack::HookBuilder.new(
|
account: Current.account,
|
||||||
account: Current.account,
|
code: params[:code],
|
||||||
code: params[:code],
|
inbox_id: params[:inbox_id]
|
||||||
inbox_id: params[:inbox_id]
|
)
|
||||||
)
|
@hook = hook_builder.perform
|
||||||
@hook = builder.perform
|
|
||||||
create_chatwoot_slack_channel
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
create_chatwoot_slack_channel
|
@hook = channel_builder.update(permitted_params[:reference_id])
|
||||||
render json: @hook
|
render json: { error: I18n.t('errors.slack.invalid_channel_id') }, status: :unprocessable_entity if @hook.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@hook.destroy!
|
@hook.destroy!
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -31,11 +31,11 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
|||||||
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'slack')
|
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'slack')
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_chatwoot_slack_channel
|
def channel_builder
|
||||||
channel = params[:channel] || 'customer-conversations'
|
Integrations::Slack::ChannelBuilder.new(hook: @hook)
|
||||||
builder = Integrations::Slack::ChannelBuilder.new(
|
end
|
||||||
hook: @hook, channel: channel
|
|
||||||
)
|
def permitted_params
|
||||||
builder.perform
|
params.permit(:reference_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||||||
@macros = Macro.with_visibility(current_user, params)
|
@macros = Macro.with_visibility(current_user, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
head :not_found if @macro.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
|
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
|
||||||
@macro.set_visibility(current_user, permitted_params)
|
@macro.set_visibility(current_user, permitted_params)
|
||||||
@@ -18,25 +22,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||||||
@macro
|
@macro
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
|
||||||
head :not_found if @macro.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@macro.destroy!
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def attach_file
|
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
|
||||||
key: nil,
|
|
||||||
io: params[:attachment].tempfile,
|
|
||||||
filename: params[:attachment].original_filename,
|
|
||||||
content_type: params[:attachment].content_type
|
|
||||||
)
|
|
||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@macro.update!(macros_with_user)
|
@macro.update!(macros_with_user)
|
||||||
@@ -49,6 +34,11 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@macro.destroy!
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
|
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
include ::FileTypeHelper
|
include ::FileTypeHelper
|
||||||
|
|
||||||
before_action :fetch_portal, except: [:index, :create, :attach_file]
|
before_action :fetch_portal, except: [:index, :create]
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@portal.update!(portal_params) if params[:portal].present?
|
@portal.update!(portal_params) if params[:portal].present?
|
||||||
# @portal.custom_domain = parsed_custom_domain
|
# @portal.custom_domain = parsed_custom_domain
|
||||||
process_attached_logo
|
process_attached_logo if params[:blob_id].present?
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
|
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
|
||||||
@@ -53,16 +53,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||||||
@portal.logo.attach(blob)
|
@portal.logo.attach(blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_file
|
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
|
||||||
key: nil,
|
|
||||||
io: params[:logo].tempfile,
|
|
||||||
filename: params[:logo].original_filename,
|
|
||||||
content_type: params[:logo].content_type
|
|
||||||
)
|
|
||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_portal
|
def fetch_portal
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
CustomExceptions::Account::UserErrors,
|
CustomExceptions::Account::UserErrors,
|
||||||
with: :render_error_response
|
with: :render_error_response
|
||||||
|
|
||||||
|
def show
|
||||||
|
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
||||||
|
render 'api/v1/accounts/show', format: :json
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@user, @account = AccountBuilder.new(
|
@user, @account = AccountBuilder.new(
|
||||||
account_name: account_params[:account_name],
|
account_name: account_params[:account_name],
|
||||||
@@ -32,14 +37,10 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cache_keys
|
def cache_keys
|
||||||
|
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
|
||||||
render json: { cache_keys: get_cache_keys }, status: :ok
|
render json: { cache_keys: get_cache_keys }, status: :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
|
||||||
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
|
||||||
render 'api/v1/accounts/show', format: :json
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class Api::V1::UploadController < Api::BaseController
|
||||||
|
def create
|
||||||
|
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
|
key: nil,
|
||||||
|
io: params[:attachment].tempfile,
|
||||||
|
filename: params[:attachment].original_filename,
|
||||||
|
content_type: params[:attachment].content_type
|
||||||
|
)
|
||||||
|
file_blob.save!
|
||||||
|
|
||||||
|
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,7 +17,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||||||
@message.update!(submitted_email: contact_email)
|
@message.update!(submitted_email: contact_email)
|
||||||
ContactIdentifyAction.new(
|
ContactIdentifyAction.new(
|
||||||
contact: @contact,
|
contact: @contact,
|
||||||
params: { email: contact_email, name: contact_name }
|
params: { email: contact_email, name: contact_name },
|
||||||
|
retain_original_contact_name: true
|
||||||
).perform
|
).perform
|
||||||
else
|
else
|
||||||
@message.update!(message_update_params[:message])
|
@message.update!(message_update_params[:message])
|
||||||
@@ -48,7 +49,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||||||
def message_finder_params
|
def message_finder_params
|
||||||
{
|
{
|
||||||
filter_internal_messages: true,
|
filter_internal_messages: true,
|
||||||
before: permitted_params[:before]
|
before: permitted_params[:before],
|
||||||
|
after: permitted_params[:after]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -62,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
# timestamp parameter is used in create conversation method
|
# timestamp parameter is used in create conversation method
|
||||||
params.permit(:id, :before, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_message
|
def set_message
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
include Api::V2::Accounts::ReportsHelper
|
include Api::V2::Accounts::ReportsHelper
|
||||||
|
include Api::V2::Accounts::HeatmapHelper
|
||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@@ -34,6 +36,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
def conversation_traffic
|
def conversation_traffic
|
||||||
@report_data = generate_conversations_heatmap_report
|
@report_data = generate_conversations_heatmap_report
|
||||||
|
timezone_offset = (params[:timezone_offset] || 0).to_f
|
||||||
|
@timezone = ActiveSupport::TimeZone[timezone_offset]
|
||||||
|
|
||||||
generate_csv('conversation_traffic_reports', 'api/v2/accounts/reports/conversation_traffic')
|
generate_csv('conversation_traffic_reports', 'api/v2/accounts/reports/conversation_traffic')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -67,14 +72,16 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||||||
def current_summary_params
|
def current_summary_params
|
||||||
common_params.merge({
|
common_params.merge({
|
||||||
since: range[:current][:since],
|
since: range[:current][:since],
|
||||||
until: range[:current][:until]
|
until: range[:current][:until],
|
||||||
|
timezone_offset: params[:timezone_offset]
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def previous_summary_params
|
def previous_summary_params
|
||||||
common_params.merge({
|
common_params.merge({
|
||||||
since: range[:previous][:since],
|
since: range[:previous][:since],
|
||||||
until: range[:previous][:until]
|
until: range[:previous][:until],
|
||||||
|
timezone_offset: params[:timezone_offset]
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class ApiController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
render json: { version: Chatwoot.config[:version],
|
render json: { version: Chatwoot.config[:version],
|
||||||
timestamp: Time.now.utc.to_formatted_s(:db),
|
timestamp: Time.now.utc.to_fs(:db),
|
||||||
queue_services: redis_status,
|
queue_services: redis_status,
|
||||||
data_services: postgres_status }
|
data_services: postgres_status }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module EnsureCurrentAccountHelper
|
|||||||
|
|
||||||
def ensure_current_account
|
def ensure_current_account
|
||||||
account = Account.find(params[:account_id])
|
account = Account.find(params[:account_id])
|
||||||
ensure_account_is_active?(account)
|
render_unauthorized('Account is suspended') and return unless account.active?
|
||||||
|
|
||||||
if current_user
|
if current_user
|
||||||
account_accessible_for_user?(account)
|
account_accessible_for_user?(account)
|
||||||
@@ -27,8 +27,4 @@ module EnsureCurrentAccountHelper
|
|||||||
def account_accessible_for_bot?(account)
|
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)
|
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_account_is_active?(account)
|
|
||||||
render_unauthorized('Account is suspended') unless account.active?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module HmacConcern
|
||||||
|
def hmac_verified?
|
||||||
|
ActiveModel::Type::Boolean.new.cast(params[:hmac_verified]).present?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
class DashboardController < ActionController::Base
|
class DashboardController < ActionController::Base
|
||||||
include SwitchLocale
|
include SwitchLocale
|
||||||
|
|
||||||
|
before_action :set_application_pack
|
||||||
before_action :set_global_config
|
before_action :set_global_config
|
||||||
around_action :switch_locale
|
around_action :switch_locale
|
||||||
before_action :ensure_installation_onboarding, only: [:index]
|
before_action :ensure_installation_onboarding, only: [:index]
|
||||||
@@ -14,7 +15,7 @@ class DashboardController < ActionController::Base
|
|||||||
|
|
||||||
def set_global_config
|
def set_global_config
|
||||||
@global_config = GlobalConfig.get(
|
@global_config = GlobalConfig.get(
|
||||||
'LOGO', 'LOGO_THUMBNAIL',
|
'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL',
|
||||||
'INSTALLATION_NAME',
|
'INSTALLATION_NAME',
|
||||||
'WIDGET_BRAND_URL', 'TERMS_URL',
|
'WIDGET_BRAND_URL', 'TERMS_URL',
|
||||||
'PRIVACY_URL',
|
'PRIVACY_URL',
|
||||||
@@ -54,9 +55,18 @@ class DashboardController < ActionController::Base
|
|||||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||||
FACEBOOK_API_VERSION: 'v14.0',
|
FACEBOOK_API_VERSION: 'v17.0',
|
||||||
IS_ENTERPRISE: ChatwootApp.enterprise?,
|
IS_ENTERPRISE: ChatwootApp.enterprise?,
|
||||||
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', '')
|
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
|
||||||
|
GIT_SHA: GIT_HASH
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_application_pack
|
||||||
|
@application_pack = if request.path.include?('/auth') || request.path.include?('/login')
|
||||||
|
'v3app'
|
||||||
|
else
|
||||||
|
'application'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
|||||||
skip_before_action :require_no_authentication, raise: false
|
skip_before_action :require_no_authentication, raise: false
|
||||||
skip_before_action :authenticate_user!, raise: false
|
skip_before_action :authenticate_user!, raise: false
|
||||||
|
|
||||||
|
def create
|
||||||
|
@user = User.find_by(email: params[:email])
|
||||||
|
if @user
|
||||||
|
@user.send_reset_password_instructions
|
||||||
|
build_response(I18n.t('messages.reset_password_success'), 200)
|
||||||
|
else
|
||||||
|
build_response(I18n.t('messages.reset_password_failure'), 404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
# params: reset_password_token, password, password_confirmation
|
# params: reset_password_token, password, password_confirmation
|
||||||
original_token = params[:reset_password_token]
|
original_token = params[:reset_password_token]
|
||||||
@@ -17,16 +27,6 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
|
||||||
@user = User.find_by(email: params[:email])
|
|
||||||
if @user
|
|
||||||
@user.send_reset_password_instructions
|
|
||||||
build_response(I18n.t('messages.reset_password_success'), 200)
|
|
||||||
else
|
|
||||||
build_response(I18n.t('messages.reset_password_failure'), 404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def reset_password_and_confirmation(recoverable)
|
def reset_password_and_confirmation(recoverable)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsController
|
class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
|
||||||
# Prevent session parameter from being passed
|
# Prevent session parameter from being passed
|
||||||
# Unpermitted parameter: session
|
# Unpermitted parameter: session
|
||||||
wrap_parameters format: []
|
wrap_parameters format: []
|
||||||
@@ -37,3 +37,5 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
|
|||||||
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
|
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
DeviseOverrides::SessionsController.prepend_mod_with('DeviseOverrides::SessionsController')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenValidationsController
|
class DeviseOverrides::TokenValidationsController < DeviseTokenAuth::TokenValidationsController
|
||||||
def validate_token
|
def validate_token
|
||||||
# @resource will have been set by set_user_by_token concern
|
# @resource will have been set by set_user_by_token concern
|
||||||
if @resource
|
if @resource
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
class Platform::Api::V1::AccountsController < PlatformController
|
class Platform::Api::V1::AccountsController < PlatformController
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@resource = Account.create!(account_params)
|
@resource = Account.create!(account_params)
|
||||||
update_resource_features
|
update_resource_features
|
||||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.assign_attributes(account_params)
|
@resource.assign_attributes(account_params)
|
||||||
update_resource_features
|
update_resource_features
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
|||||||
@resources = @platform_app.platform_app_permissibles.where(permissible_type: 'AgentBot').all
|
@resources = @platform_app.platform_app_permissibles.where(permissible_type: 'AgentBot').all
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@resource = AgentBot.new(agent_bot_params)
|
@resource = AgentBot.new(agent_bot_params)
|
||||||
@resource.save!
|
@resource.save!
|
||||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.update!(agent_bot_params)
|
@resource.update!(agent_bot_params)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
|||||||
before_action(only: [:login]) { set_resource }
|
before_action(only: [:login]) { set_resource }
|
||||||
before_action(only: [:login]) { validate_platform_app_permissible }
|
before_action(only: [:login]) { validate_platform_app_permissible }
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||||
@resource.skip_confirmation!
|
@resource.skip_confirmation!
|
||||||
@@ -16,8 +18,6 @@ class Platform::Api::V1::UsersController < PlatformController
|
|||||||
render json: { url: @resource.generate_sso_link }
|
render json: { url: @resource.generate_sso_link }
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.assign_attributes(user_update_params)
|
@resource.assign_attributes(user_update_params)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
|||||||
before_action :contact_inbox, except: [:create]
|
before_action :contact_inbox, except: [:create]
|
||||||
before_action :process_hmac
|
before_action :process_hmac
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
source_id = params[:source_id] || SecureRandom.uuid
|
source_id = params[:source_id] || SecureRandom.uuid
|
||||||
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
@@ -11,8 +13,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
|||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
contact_identify_action = ContactIdentifyAction.new(
|
contact_identify_action = ContactIdentifyAction.new(
|
||||||
contact: @contact_inbox.contact,
|
contact: @contact_inbox.contact,
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
|
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
|
||||||
|
include Events::Types
|
||||||
|
before_action :set_conversation, only: [:toggle_typing, :update_last_seen]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
|
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
|
||||||
end
|
end
|
||||||
@@ -7,12 +10,36 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
|||||||
@conversation = create_conversation
|
@conversation = create_conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def toggle_typing
|
||||||
|
case params[:typing_status]
|
||||||
|
when 'on'
|
||||||
|
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||||
|
when 'off'
|
||||||
|
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||||
|
end
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_last_seen
|
||||||
|
@conversation.contact_last_seen_at = DateTime.now.utc
|
||||||
|
@conversation.save!
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_conversation
|
||||||
|
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
def create_conversation
|
def create_conversation
|
||||||
::Conversation.create!(conversation_params)
|
::Conversation.create!(conversation_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def trigger_typing_event(event)
|
||||||
|
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: @conversation.contact)
|
||||||
|
end
|
||||||
|
|
||||||
def conversation_params
|
def conversation_params
|
||||||
{
|
{
|
||||||
account_id: @contact_inbox.contact.account_id,
|
account_id: @contact_inbox.contact.account_id,
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||||
before_action :portal
|
before_action :portal
|
||||||
before_action :set_category, except: [:index]
|
before_action :set_category, except: [:index, :show]
|
||||||
before_action :set_article, only: [:show]
|
before_action :set_article, only: [:show]
|
||||||
layout 'portal'
|
layout 'portal'
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@articles = @portal.articles
|
@articles = @portal.articles
|
||||||
@articles = @articles.search(list_params) if list_params.present?
|
@articles = @articles.search(list_params) if list_params.present?
|
||||||
@articles.order(position: :asc)
|
order_by_sort_param
|
||||||
|
@articles.page(list_params[:page]) if list_params[:page].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def order_by_sort_param
|
||||||
|
@articles = if list_params[:sort].present? && list_params[:sort] == 'views'
|
||||||
|
@articles.order_by_views
|
||||||
|
else
|
||||||
|
@articles.order_by_position
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_article
|
def set_article
|
||||||
@article = @category.articles.find(permitted_params[:id])
|
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
||||||
@article.increment_view_count
|
@article.increment_view_count
|
||||||
@parsed_content = render_article_content(@article.content)
|
@parsed_content = render_article_content(@article.content)
|
||||||
end
|
end
|
||||||
@@ -35,16 +44,14 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
|||||||
end
|
end
|
||||||
|
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:query, :locale)
|
params.permit(:query, :locale, :sort)
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:slug, :category_slug, :locale, :id)
|
params.permit(:slug, :category_slug, :locale, :id, :article_slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_article_content(content)
|
def render_article_content(content)
|
||||||
# rubocop:disable Rails/OutputSafety
|
ChatwootMarkdownRenderer.new(content).render_article
|
||||||
CommonMarker.render_html(content).html_safe
|
|
||||||
# rubocop:enable Rails/OutputSafety
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
class Public::Api::V1::Portals::BaseController < PublicController
|
class Public::Api::V1::Portals::BaseController < PublicController
|
||||||
|
before_action :show_plain_layout
|
||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
|
after_action :allow_iframe_requests
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def show_plain_layout
|
||||||
|
@is_plain_layout_enabled = params[:show_plain_layout] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
def set_locale(&)
|
def set_locale(&)
|
||||||
switch_locale_with_portal(&) if params[:locale].present?
|
switch_locale_with_portal(&) if params[:locale].present?
|
||||||
|
switch_locale_with_article(&) if params[:article_slug].present?
|
||||||
|
|
||||||
|
yield
|
||||||
end
|
end
|
||||||
|
|
||||||
def switch_locale_with_portal(&)
|
def switch_locale_with_portal(&)
|
||||||
@@ -19,4 +28,20 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
|||||||
|
|
||||||
I18n.with_locale(@locale, &)
|
I18n.with_locale(@locale, &)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def switch_locale_with_article(&)
|
||||||
|
article = Article.find_by(slug: params[:article_slug])
|
||||||
|
|
||||||
|
@locale = if article.category.present?
|
||||||
|
article.category.locale
|
||||||
|
else
|
||||||
|
'en'
|
||||||
|
end
|
||||||
|
|
||||||
|
I18n.with_locale(@locale, &)
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_iframe_requests
|
||||||
|
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -45,6 +45,24 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
|||||||
|
|
||||||
def seed
|
def seed
|
||||||
Internal::SeedAccountJob.perform_later(requested_resource)
|
Internal::SeedAccountJob.perform_later(requested_resource)
|
||||||
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
|
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_cache
|
||||||
|
requested_resource.reset_cache_keys
|
||||||
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
|
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Cache keys cleared')
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
account = Account.find(params[:id])
|
||||||
|
|
||||||
|
DeleteObjectJob.perform_later(account) if account.present?
|
||||||
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
|
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account deletion is in progress.')
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,4 +41,14 @@ class SuperAdmin::AgentBotsController < SuperAdmin::ApplicationController
|
|||||||
|
|
||||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||||
# for more information
|
# for more information
|
||||||
|
|
||||||
|
def destroy_avatar
|
||||||
|
avatar = requested_resource.avatar
|
||||||
|
avatar.purge
|
||||||
|
redirect_back(fallback_location: super_admin_agent_bots_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def scoped_resource
|
||||||
|
resource_class.with_attached_avatar
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
|||||||
sha
|
sha
|
||||||
postgres_status
|
postgres_status
|
||||||
redis_metrics
|
redis_metrics
|
||||||
|
chatwoot_edition
|
||||||
|
end
|
||||||
|
|
||||||
|
def chatwoot_edition
|
||||||
|
@metrics['Chatwoot edition'] = if ChatwootApp.enterprise?
|
||||||
|
'Enterprise'
|
||||||
|
elsif ChatwootApp.custom?
|
||||||
|
'Custom'
|
||||||
|
else
|
||||||
|
'Community'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def chatwoot_version
|
def chatwoot_version
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
|||||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||||
# and `dashboard`:
|
# and `dashboard`:
|
||||||
#
|
#
|
||||||
|
|
||||||
|
def destroy_avatar
|
||||||
|
avatar = requested_resource.avatar
|
||||||
|
avatar.purge
|
||||||
|
redirect_back(fallback_location: super_admin_users_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def scoped_resource
|
||||||
|
resource_class.with_attached_avatar
|
||||||
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
permitted_params = super
|
permitted_params = super
|
||||||
permitted_params.delete(:password) if permitted_params[:password].blank?
|
permitted_params.delete(:password) if permitted_params[:password].blank?
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class SwaggerController < ApplicationController
|
class SwaggerController < ApplicationController
|
||||||
def respond
|
def respond
|
||||||
if Rails.env.development? || Rails.env.test?
|
if Rails.env.development? || Rails.env.test?
|
||||||
render inline: File.read(Rails.root.join('swagger', derived_path))
|
render inline: Rails.root.join('swagger', derived_path).read
|
||||||
else
|
else
|
||||||
head :not_found
|
head :not_found
|
||||||
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)
|
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
ChatwootExceptionTracker.new(e).capture_exception
|
||||||
redirect_to twitter_app_redirect_url
|
redirect_to twitter_app_redirect_url
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -49,10 +49,22 @@ class Twitter::CallbacksController < Twitter::BaseController
|
|||||||
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
||||||
profile_id: parsed_body['user_id']
|
profile_id: parsed_body['user_id']
|
||||||
)
|
)
|
||||||
account.inboxes.create!(
|
inbox = account.inboxes.create!(
|
||||||
name: parsed_body['screen_name'],
|
name: parsed_body['screen_name'],
|
||||||
channel: twitter_profile
|
channel: twitter_profile
|
||||||
)
|
)
|
||||||
|
save_profile_image(inbox)
|
||||||
|
inbox
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_profile_image(inbox)
|
||||||
|
response = twitter_client.user_show(screen_name: inbox.name)
|
||||||
|
|
||||||
|
return unless response.status.to_i == 200
|
||||||
|
|
||||||
|
parsed_user_profile = response.body
|
||||||
|
|
||||||
|
::Avatar::AvatarFromUrlJob.perform_later(inbox, parsed_user_profile['profile_image_url_https'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class WidgetsController < ActionController::Base
|
|||||||
source_id: @auth_token_params[:source_id]
|
source_id: @auth_token_params[:source_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
@contact = @contact_inbox ? @contact_inbox.contact : nil
|
@contact = @contact_inbox&.contact
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact
|
def build_contact
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ class AccountDashboard < Administrate::BaseDashboard
|
|||||||
"##{account.id} #{account.name}"
|
"##{account.id} #{account.name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_attributes
|
# We do not use the action parameter but we still need to define it
|
||||||
|
# to prevent an error from being raised (wrong number of arguments)
|
||||||
|
# Reference: https://github.com/thoughtbot/administrate/pull/2356/files#diff-4e220b661b88f9a19ac527c50d6f1577ef6ab7b0bed2bfdf048e22e6bfa74a05R204
|
||||||
|
def permitted_attributes(action)
|
||||||
super + [limits: {}]
|
super + [limits: {}]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
|||||||
ATTRIBUTE_TYPES = {
|
ATTRIBUTE_TYPES = {
|
||||||
access_token: Field::HasOne,
|
access_token: Field::HasOne,
|
||||||
avatar_url: AvatarField,
|
avatar_url: AvatarField,
|
||||||
|
avatar: Field::ActiveStorage.with_options(
|
||||||
|
destroy_url: proc do |_namespace, _resource, attachment|
|
||||||
|
[:avatar_super_admin_agent_bot, { attachment_id: attachment.id }]
|
||||||
|
end
|
||||||
|
),
|
||||||
id: Field::Number,
|
id: Field::Number,
|
||||||
name: Field::String,
|
name: Field::String,
|
||||||
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
||||||
@@ -36,6 +41,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
|||||||
# an array of attributes that will be displayed on the model's show page.
|
# an array of attributes that will be displayed on the model's show page.
|
||||||
SHOW_PAGE_ATTRIBUTES = %i[
|
SHOW_PAGE_ATTRIBUTES = %i[
|
||||||
id
|
id
|
||||||
|
avatar_url
|
||||||
account
|
account
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
@@ -47,6 +53,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
|||||||
# on the model's form (`new` and `edit`) pages.
|
# on the model's form (`new` and `edit`) pages.
|
||||||
FORM_ATTRIBUTES = %i[
|
FORM_ATTRIBUTES = %i[
|
||||||
name
|
name
|
||||||
|
avatar
|
||||||
account
|
account
|
||||||
description
|
description
|
||||||
outgoing_url
|
outgoing_url
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ class UserDashboard < Administrate::BaseDashboard
|
|||||||
account_users: Field::HasMany,
|
account_users: Field::HasMany,
|
||||||
id: Field::Number,
|
id: Field::Number,
|
||||||
avatar_url: AvatarField,
|
avatar_url: AvatarField,
|
||||||
|
avatar: Field::ActiveStorage.with_options(
|
||||||
|
destroy_url: proc do |_namespace, _resource, attachment|
|
||||||
|
[:avatar_super_admin_user, { attachment_id: attachment.id }]
|
||||||
|
end
|
||||||
|
),
|
||||||
provider: Field::String,
|
provider: Field::String,
|
||||||
uid: Field::String,
|
uid: Field::String,
|
||||||
password: Field::Password,
|
password: Field::Password,
|
||||||
@@ -69,6 +74,7 @@ class UserDashboard < Administrate::BaseDashboard
|
|||||||
# on the model's form (`new` and `edit`) pages.
|
# on the model's form (`new` and `edit`) pages.
|
||||||
FORM_ATTRIBUTES = %i[
|
FORM_ATTRIBUTES = %i[
|
||||||
name
|
name
|
||||||
|
avatar
|
||||||
display_name
|
display_name
|
||||||
email
|
email
|
||||||
password
|
password
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
class AccountDrop < BaseDrop
|
class AccountDrop < BaseDrop
|
||||||
|
def name
|
||||||
|
@obj.try(:name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ class ConversationFinder
|
|||||||
SORT_OPTIONS = {
|
SORT_OPTIONS = {
|
||||||
latest: 'latest',
|
latest: 'latest',
|
||||||
sort_on_created_at: 'sort_on_created_at',
|
sort_on_created_at: 'sort_on_created_at',
|
||||||
last_user_message_at: 'last_user_message_at'
|
last_user_message_at: 'last_user_message_at',
|
||||||
|
sort_on_priority: 'sort_on_priority',
|
||||||
|
sort_on_waiting_since: 'sort_on_waiting_since'
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
|
|
||||||
# assumptions
|
# assumptions
|
||||||
@@ -53,9 +55,10 @@ class ConversationFinder
|
|||||||
|
|
||||||
find_all_conversations
|
find_all_conversations
|
||||||
filter_by_status unless params[:q]
|
filter_by_status unless params[:q]
|
||||||
filter_by_team if @team
|
filter_by_team
|
||||||
filter_by_labels if params[:labels]
|
filter_by_labels
|
||||||
filter_by_query if params[:q]
|
filter_by_query
|
||||||
|
filter_by_source_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_inboxes
|
def set_inboxes
|
||||||
@@ -100,12 +103,14 @@ class ConversationFinder
|
|||||||
when 'participating'
|
when 'participating'
|
||||||
@conversations = current_user.participating_conversations.where(account_id: current_account.id)
|
@conversations = current_user.participating_conversations.where(account_id: current_account.id)
|
||||||
when 'unattended'
|
when 'unattended'
|
||||||
@conversations = @conversations.where(first_reply_created_at: nil)
|
@conversations = @conversations.unattended
|
||||||
end
|
end
|
||||||
@conversations
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_query
|
def filter_by_query
|
||||||
|
return unless params[:q]
|
||||||
|
|
||||||
allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]]
|
allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]]
|
||||||
@conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%")
|
@conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%")
|
||||||
.where(messages: { message_type: allowed_message_types }).includes(:messages)
|
.where(messages: { message_type: allowed_message_types }).includes(:messages)
|
||||||
@@ -120,13 +125,24 @@ class ConversationFinder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_team
|
def filter_by_team
|
||||||
|
return unless @team
|
||||||
|
|
||||||
@conversations = @conversations.where(team: @team)
|
@conversations = @conversations.where(team: @team)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_labels
|
def filter_by_labels
|
||||||
|
return unless params[:labels]
|
||||||
|
|
||||||
@conversations = @conversations.tagged_with(params[:labels], any: true)
|
@conversations = @conversations.tagged_with(params[:labels], any: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_by_source_id
|
||||||
|
return unless params[:source_id]
|
||||||
|
|
||||||
|
@conversations = @conversations.joins(:contact_inbox)
|
||||||
|
@conversations = @conversations.where(contact_inboxes: { source_id: params[:source_id] })
|
||||||
|
end
|
||||||
|
|
||||||
def set_count_for_all_conversations
|
def set_count_for_all_conversations
|
||||||
[
|
[
|
||||||
@conversations.assigned_to(current_user).count,
|
@conversations.assigned_to(current_user).count,
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ class EmailChannelFinder
|
|||||||
|
|
||||||
def perform
|
def perform
|
||||||
channel = nil
|
channel = nil
|
||||||
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
|
|
||||||
recipient_mails.each do |email|
|
recipient_mails.each do |email|
|
||||||
normalized_email = normalize_email_with_plus_addressing(email)
|
normalized_email = normalize_email_with_plus_addressing(email)
|
||||||
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', normalized_email, normalized_email)
|
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', normalized_email, normalized_email)
|
||||||
|
|
||||||
break if channel.present?
|
break if channel.present?
|
||||||
end
|
end
|
||||||
channel
|
channel
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recipient_mails
|
||||||
|
recipient_addresses = @email_object.to.to_a + @email_object.cc.to_a + @email_object.bcc.to_a + [@email_object['X-Original-To'].try(:value)]
|
||||||
|
recipient_addresses.flatten.compact
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,11 +22,29 @@ class MessageFinder
|
|||||||
|
|
||||||
def current_messages
|
def current_messages
|
||||||
if @params[:after].present? && @params[:before].present?
|
if @params[:after].present? && @params[:before].present?
|
||||||
messages.reorder('created_at asc').where('id >= ? AND id < ?', @params[:after].to_i, @params[:before].to_i).limit(1000)
|
messages_between(@params[:after].to_i, @params[:before].to_i)
|
||||||
elsif @params[:before].present?
|
elsif @params[:before].present?
|
||||||
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
|
messages_before(@params[:before].to_i)
|
||||||
|
elsif @params[:after].present?
|
||||||
|
messages_after(@params[:after].to_i)
|
||||||
else
|
else
|
||||||
messages.reorder('created_at desc').limit(20).reverse
|
messages_latest
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def messages_after(after_id)
|
||||||
|
messages.reorder('created_at asc').where('id > ?', after_id).limit(100)
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages_before(before_id)
|
||||||
|
messages.reorder('created_at desc').where('id < ?', before_id).limit(20).reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages_between(after_id, before_id)
|
||||||
|
messages.reorder('created_at asc').where('id >= ? AND id < ?', after_id, before_id).limit(1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages_latest
|
||||||
|
messages.reorder('created_at desc').limit(20).reverse
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
module Api::V2::Accounts::HeatmapHelper
|
||||||
|
def generate_conversations_heatmap_report
|
||||||
|
timezone_data = generate_heatmap_data_for_timezone(params[:timezone_offset])
|
||||||
|
|
||||||
|
group_traffic_data(timezone_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def group_traffic_data(data)
|
||||||
|
# start with an empty array
|
||||||
|
result_arr = []
|
||||||
|
|
||||||
|
# pick all the unique dates from the data in ascending order
|
||||||
|
dates = data.pluck(:date).uniq.sort
|
||||||
|
|
||||||
|
# add the dates as the first row, leave an empty cell for the hour column
|
||||||
|
# e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03']
|
||||||
|
result_arr << ([nil] + dates)
|
||||||
|
|
||||||
|
# group the data by hour, we do not need to sort it, because the data is already sorted
|
||||||
|
# given it starts from the beginning of the day
|
||||||
|
# here each hour is a key, and the value is an array of all the items for that hour at each date
|
||||||
|
# e.g. hour = 1
|
||||||
|
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
|
||||||
|
data.group_by { |d| d[:hour] }.each do |hour, items|
|
||||||
|
# create a new row for each hour
|
||||||
|
row = [hour]
|
||||||
|
|
||||||
|
# group the items by date, so we can easily access the value for each date
|
||||||
|
# grouped values will be a hasg with the date as the key, and the value as the value
|
||||||
|
# e.g. { '2023-01-01' => [{date: 2023-01-01, value: 1}], '2023-01-02' => [{date: 2023-01-02, value: 1}], ... }
|
||||||
|
grouped_values = items.group_by { |d| d[:date] }
|
||||||
|
|
||||||
|
# now for each unique date we have, we can access the value for that date and append it to the array
|
||||||
|
dates.each do |date|
|
||||||
|
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
|
||||||
|
end
|
||||||
|
|
||||||
|
# row will look like [22, 0, 0, 1, 4, 6, 7, 4]
|
||||||
|
# add the row to the result array
|
||||||
|
|
||||||
|
result_arr << row
|
||||||
|
end
|
||||||
|
|
||||||
|
# return the resultant array
|
||||||
|
# the result looks like this
|
||||||
|
# [
|
||||||
|
# [nil, '2023-01-01', '2023-1-02', '2023-01-03'],
|
||||||
|
# [0, 0, 0, 0],
|
||||||
|
# [1, 0, 0, 0],
|
||||||
|
# [2, 0, 0, 0],
|
||||||
|
# [3, 0, 0, 0],
|
||||||
|
# [4, 0, 0, 0],
|
||||||
|
# ]
|
||||||
|
result_arr
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_heatmap_data_for_timezone(offset)
|
||||||
|
timezone = ActiveSupport::TimeZone[offset]&.name
|
||||||
|
timezone_today = DateTime.now.in_time_zone(timezone).beginning_of_day
|
||||||
|
|
||||||
|
timezone_data_raw = generate_heatmap_data(timezone_today, offset)
|
||||||
|
|
||||||
|
transform_data(timezone_data_raw, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_heatmap_data(date, offset)
|
||||||
|
report_params = {
|
||||||
|
type: :account,
|
||||||
|
group_by: 'hour',
|
||||||
|
metric: 'conversations_count',
|
||||||
|
business_hours: false
|
||||||
|
}
|
||||||
|
|
||||||
|
V2::ReportBuilder.new(Current.account, report_params.merge({
|
||||||
|
since: since_timestamp(date),
|
||||||
|
until: until_timestamp(date),
|
||||||
|
timezone_offset: offset
|
||||||
|
})).build
|
||||||
|
end
|
||||||
|
|
||||||
|
def transform_data(data, zone_transform)
|
||||||
|
# rubocop:disable Rails/TimeZone
|
||||||
|
data.map do |d|
|
||||||
|
date = zone_transform ? Time.zone.at(d[:timestamp]) : Time.at(d[:timestamp])
|
||||||
|
{
|
||||||
|
date: date.to_date.to_s,
|
||||||
|
hour: date.hour,
|
||||||
|
value: d[:value]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
# rubocop:enable Rails/TimeZone
|
||||||
|
end
|
||||||
|
|
||||||
|
def since_timestamp(date)
|
||||||
|
(date - 6.days).to_i.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def until_timestamp(date)
|
||||||
|
date.to_i.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -27,27 +27,6 @@ module Api::V2::Accounts::ReportsHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_conversations_heatmap_report
|
|
||||||
report_params = {
|
|
||||||
type: :account,
|
|
||||||
group_by: 'hour',
|
|
||||||
since: params[:since],
|
|
||||||
until: params[:until],
|
|
||||||
metric: 'conversations_count',
|
|
||||||
business_hours: false
|
|
||||||
}
|
|
||||||
data = V2::ReportBuilder.new(Current.account, report_params).build
|
|
||||||
|
|
||||||
# data format is { timestamp: 1231242342, value: 3}
|
|
||||||
# we need to convert it to { date: "2020-01-01", hour: 12, value: 3}
|
|
||||||
#
|
|
||||||
# the generated report is **always** in UTC timezone
|
|
||||||
data.map do |d|
|
|
||||||
date = Time.zone.at(d[:timestamp]).to_s
|
|
||||||
[date, d[:value]]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_report(report_params)
|
def generate_report(report_params)
|
||||||
V2::ReportBuilder.new(
|
V2::ReportBuilder.new(
|
||||||
Current.account,
|
Current.account,
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
module BillingHelper
|
||||||
|
private
|
||||||
|
|
||||||
|
def default_plan?(account)
|
||||||
|
installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')
|
||||||
|
default_plan = installation_config&.value&.first
|
||||||
|
|
||||||
|
# Return false if not plans are configured, so that no checks are enforced
|
||||||
|
return false if default_plan.blank?
|
||||||
|
|
||||||
|
account.custom_attributes['plan_name'].nil? || account.custom_attributes['plan_name'] == default_plan['name']
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversations_this_month(account)
|
||||||
|
account.conversations.where('created_at > ?', 30.days.ago).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def non_web_inboxes(account)
|
||||||
|
account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,7 +7,7 @@ module DateRangeHelper
|
|||||||
def range
|
def range
|
||||||
return if params[:since].blank? || params[:until].blank?
|
return if params[:since].blank? || params[:until].blank?
|
||||||
|
|
||||||
parse_date_time(params[:since])..parse_date_time(params[:until])
|
parse_date_time(params[:since])...parse_date_time(params[:until])
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_date_time(datetime)
|
def parse_date_time(datetime)
|
||||||
|
|||||||
@@ -10,4 +10,26 @@ module EmailHelper
|
|||||||
def normalize_email_with_plus_addressing(email)
|
def normalize_email_with_plus_addressing(email)
|
||||||
"#{email.split('@').first.split('+').first}@#{email.split('@').last}".downcase
|
"#{email.split('@').first.split('+').first}@#{email.split('@').last}".downcase
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def parse_email_variables(conversation, email)
|
||||||
|
case email
|
||||||
|
when modified_liquid_content(email)
|
||||||
|
template = Liquid::Template.parse(modified_liquid_content(email))
|
||||||
|
template.render(message_drops(conversation))
|
||||||
|
when URI::MailTo::EMAIL_REGEXP
|
||||||
|
email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def modified_liquid_content(email)
|
||||||
|
# This regex is used to match the code blocks in the content
|
||||||
|
# We don't want to process liquid in code blocks
|
||||||
|
email.gsub(/`(.*?)`/m, '{% raw %}`\\1`{% endraw %}')
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_drops(conversation)
|
||||||
|
{
|
||||||
|
'contact' => ContactDrop.new(conversation.contact)
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ module MessageFormatHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_message_content(message_content)
|
def render_message_content(message_content)
|
||||||
# rubocop:disable Rails/OutputSafety
|
ChatwootMarkdownRenderer.new(message_content).render_message
|
||||||
CommonMarker.render_html(message_content).html_safe
|
|
||||||
# rubocop:enable Rails/OutputSafety
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,19 +17,36 @@ module ReportHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def conversations_count
|
def conversations_count
|
||||||
(get_grouped_values scope.conversations.where(account_id: account.id)).count
|
(get_grouped_values conversations).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def incoming_messages_count
|
def incoming_messages_count
|
||||||
(get_grouped_values scope.messages.where(account_id: account.id).incoming.unscope(:order)).count
|
(get_grouped_values incoming_messages).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def outgoing_messages_count
|
def outgoing_messages_count
|
||||||
(get_grouped_values scope.messages.where(account_id: account.id).outgoing.unscope(:order)).count
|
(get_grouped_values outgoing_messages).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolutions_count
|
def resolutions_count
|
||||||
(get_grouped_values scope.conversations.where(account_id: account.id).resolved).count
|
(get_grouped_values resolutions).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversations
|
||||||
|
scope.conversations.where(account_id: account.id, created_at: range)
|
||||||
|
end
|
||||||
|
|
||||||
|
def incoming_messages
|
||||||
|
scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order)
|
||||||
|
end
|
||||||
|
|
||||||
|
def outgoing_messages
|
||||||
|
scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order)
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolutions
|
||||||
|
scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved,
|
||||||
|
conversations: { status: :resolved }, created_at: range).distinct
|
||||||
end
|
end
|
||||||
|
|
||||||
def avg_first_response_time
|
def avg_first_response_time
|
||||||
@@ -39,6 +56,13 @@ module ReportHelper
|
|||||||
grouped_reporting_events.average(:value)
|
grouped_reporting_events.average(:value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reply_time
|
||||||
|
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'reply_time', account_id: account.id))
|
||||||
|
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||||
|
|
||||||
|
grouped_reporting_events.average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
def avg_resolution_time
|
def avg_resolution_time
|
||||||
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved', account_id: account.id))
|
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved', account_id: account.id))
|
||||||
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||||
@@ -49,17 +73,35 @@ module ReportHelper
|
|||||||
def avg_resolution_time_summary
|
def avg_resolution_time_summary
|
||||||
reporting_events = scope.reporting_events
|
reporting_events = scope.reporting_events
|
||||||
.where(name: 'conversation_resolved', account_id: account.id, created_at: range)
|
.where(name: 'conversation_resolved', account_id: account.id, created_at: range)
|
||||||
avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
avg_rt = if params[:business_hours].present?
|
||||||
|
reporting_events.average(:value_in_business_hours)
|
||||||
|
else
|
||||||
|
reporting_events.average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
return 0 if avg_rt.blank?
|
return 0 if avg_rt.blank?
|
||||||
|
|
||||||
avg_rt
|
avg_rt
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reply_time_summary
|
||||||
|
reporting_events = scope.reporting_events
|
||||||
|
.where(name: 'reply_time', account_id: account.id, created_at: range)
|
||||||
|
reply_time = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||||
|
|
||||||
|
return 0 if reply_time.blank?
|
||||||
|
|
||||||
|
reply_time
|
||||||
|
end
|
||||||
|
|
||||||
def avg_first_response_time_summary
|
def avg_first_response_time_summary
|
||||||
reporting_events = scope.reporting_events
|
reporting_events = scope.reporting_events
|
||||||
.where(name: 'first_response', account_id: account.id, created_at: range)
|
.where(name: 'first_response', account_id: account.id, created_at: range)
|
||||||
avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
avg_frt = if params[:business_hours].present?
|
||||||
|
reporting_events.average(:value_in_business_hours)
|
||||||
|
else
|
||||||
|
reporting_events.average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
return 0 if avg_frt.blank?
|
return 0 if avg_frt.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
<div
|
<div
|
||||||
v-if="!authUIFlags.isFetching"
|
v-if="!authUIFlags.isFetching"
|
||||||
id="app"
|
id="app"
|
||||||
class="app-wrapper app-root"
|
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
||||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||||
|
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||||
>
|
>
|
||||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||||
|
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
|
||||||
|
<payment-pending-banner />
|
||||||
|
<upgrade-banner />
|
||||||
|
</template>
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<router-view />
|
<router-view />
|
||||||
</transition>
|
</transition>
|
||||||
@@ -25,9 +30,12 @@ import AddAccountModal from '../dashboard/components/layout/sidebarComponents/Ad
|
|||||||
import LoadingState from './components/widgets/LoadingState.vue';
|
import LoadingState from './components/widgets/LoadingState.vue';
|
||||||
import NetworkNotification from './components/NetworkNotification';
|
import NetworkNotification from './components/NetworkNotification';
|
||||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||||
|
import UpgradeBanner from './components/app/UpgradeBanner.vue';
|
||||||
|
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||||
import vueActionCable from './helper/actionCable';
|
import vueActionCable from './helper/actionCable';
|
||||||
import WootSnackbarBox from './components/SnackbarContainer';
|
import WootSnackbarBox from './components/SnackbarContainer';
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
|
import { setColorTheme } from './helper/themeHelper';
|
||||||
import {
|
import {
|
||||||
registerSubscription,
|
registerSubscription,
|
||||||
verifyServiceWorkerExistence,
|
verifyServiceWorkerExistence,
|
||||||
@@ -41,7 +49,9 @@ export default {
|
|||||||
LoadingState,
|
LoadingState,
|
||||||
NetworkNotification,
|
NetworkNotification,
|
||||||
UpdateBanner,
|
UpdateBanner,
|
||||||
|
PaymentPendingBanner,
|
||||||
WootSnackbarBox,
|
WootSnackbarBox,
|
||||||
|
UpgradeBanner,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [rtlMixin],
|
mixins: [rtlMixin],
|
||||||
@@ -59,6 +69,7 @@ export default {
|
|||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
authUIFlags: 'getAuthUIFlags',
|
authUIFlags: 'getAuthUIFlags',
|
||||||
|
accountUIFlags: 'accounts/getUIFlags',
|
||||||
currentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
hasAccounts() {
|
hasAccounts() {
|
||||||
@@ -80,9 +91,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.initializeColorTheme();
|
||||||
|
this.listenToThemeChanges();
|
||||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
initializeColorTheme() {
|
||||||
|
setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
},
|
||||||
|
listenToThemeChanges() {
|
||||||
|
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
mql.onchange = e => setColorTheme(e.matches);
|
||||||
|
},
|
||||||
setLocale(locale) {
|
setLocale(locale) {
|
||||||
this.$root.$i18n.locale = locale;
|
this.$root.$i18n.locale = locale;
|
||||||
},
|
},
|
||||||
@@ -115,11 +135,6 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import './assets/scss/app';
|
@import './assets/scss/app';
|
||||||
.update-banner {
|
|
||||||
height: var(--space-larger);
|
|
||||||
align-items: center;
|
|
||||||
font-size: var(--font-size-small) !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class CacheEnabledApiClient extends ApiClient {
|
|||||||
return this.getFromCache();
|
return this.getFromCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.getFromNetwork();
|
||||||
|
}
|
||||||
|
|
||||||
|
getFromNetwork() {
|
||||||
return axios.get(this.url);
|
return axios.get(this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +36,12 @@ class CacheEnabledApiClient extends ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFromCache() {
|
async getFromCache() {
|
||||||
await this.dataManager.initDb();
|
try {
|
||||||
|
// IDB is not supported in Firefox private mode: https://bugzilla.mozilla.org/show_bug.cgi?id=781982
|
||||||
|
await this.dataManager.initDb();
|
||||||
|
} catch {
|
||||||
|
return this.getFromNetwork();
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await axios.get(
|
const { data } = await axios.get(
|
||||||
`/api/v1/accounts/${this.accountIdFromRoute}/cache_keys`
|
`/api/v1/accounts/${this.accountIdFromRoute}/cache_keys`
|
||||||
@@ -55,16 +64,22 @@ class CacheEnabledApiClient extends ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refetchAndCommit(newKey = null) {
|
async refetchAndCommit(newKey = null) {
|
||||||
await this.dataManager.initDb();
|
const response = await this.getFromNetwork();
|
||||||
const response = await axios.get(this.url);
|
|
||||||
this.dataManager.replace({
|
|
||||||
modelName: this.cacheModelName,
|
|
||||||
data: this.extractDataFromResponse(response),
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.dataManager.setCacheKeys({
|
try {
|
||||||
[this.cacheModelName]: newKey,
|
await this.dataManager.initDb();
|
||||||
});
|
|
||||||
|
this.dataManager.replace({
|
||||||
|
modelName: this.cacheModelName,
|
||||||
|
data: this.extractDataFromResponse(response),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.dataManager.setCacheKeys({
|
||||||
|
[this.cacheModelName]: newKey,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* global axios */
|
||||||
|
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class AuditLogs extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('audit_logs', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get({ page }) {
|
||||||
|
const url = page ? `${this.url}?page=${page}` : this.url;
|
||||||
|
return axios.get(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuditLogs();
|
||||||
@@ -3,47 +3,11 @@
|
|||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import endPoints from './endPoints';
|
import endPoints from './endPoints';
|
||||||
import {
|
import {
|
||||||
setAuthCredentials,
|
|
||||||
clearCookiesOnLogout,
|
clearCookiesOnLogout,
|
||||||
deleteIndexedDBOnLogout,
|
deleteIndexedDBOnLogout,
|
||||||
} from '../store/utils/api';
|
} from '../store/utils/api';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login(creds) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios
|
|
||||||
.post('auth/sign_in', creds)
|
|
||||||
.then(response => {
|
|
||||||
setAuthCredentials(response);
|
|
||||||
resolve(response.data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
reject(error.response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
register(creds) {
|
|
||||||
const urlData = endPoints('register');
|
|
||||||
const fetchPromise = new Promise((resolve, reject) => {
|
|
||||||
axios
|
|
||||||
.post(urlData.url, {
|
|
||||||
account_name: creds.accountName.trim(),
|
|
||||||
user_full_name: creds.fullName.trim(),
|
|
||||||
email: creds.email,
|
|
||||||
password: creds.password,
|
|
||||||
h_captcha_client_response: creds.hCaptchaClientResponse,
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
setAuthCredentials(response);
|
|
||||||
resolve(response);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return fetchPromise;
|
|
||||||
},
|
|
||||||
validityCheck() {
|
validityCheck() {
|
||||||
const urlData = endPoints('validityCheck');
|
const urlData = endPoints('validityCheck');
|
||||||
return axios.get(urlData.url);
|
return axios.get(urlData.url);
|
||||||
@@ -73,45 +37,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
verifyPasswordToken({ confirmationToken }) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios
|
|
||||||
.post('auth/confirmation', {
|
|
||||||
confirmation_token: confirmationToken,
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
setAuthCredentials(response);
|
|
||||||
resolve(response);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
reject(error.response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setNewPassword({ resetPasswordToken, password, confirmPassword }) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios
|
|
||||||
.put('auth/password', {
|
|
||||||
reset_password_token: resetPasswordToken,
|
|
||||||
password_confirmation: confirmPassword,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
setAuthCredentials(response);
|
|
||||||
resolve(response);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
reject(error.response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
resetPassword({ email }) {
|
|
||||||
const urlData = endPoints('resetPassword');
|
|
||||||
return axios.post(urlData.url, { email });
|
|
||||||
},
|
|
||||||
|
|
||||||
profileUpdate({
|
profileUpdate({
|
||||||
password,
|
password,
|
||||||
password_confirmation,
|
password_confirmation,
|
||||||
|
|||||||
@@ -9,14 +9,6 @@ class AutomationsAPI extends ApiClient {
|
|||||||
clone(automationId) {
|
clone(automationId) {
|
||||||
return axios.post(`${this.url}/${automationId}/clone`);
|
return axios.post(`${this.url}/${automationId}/clone`);
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment(file) {
|
|
||||||
return axios.post(`${this.url}/attach_file`, file, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AutomationsAPI();
|
export default new AutomationsAPI();
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ class ContactAPI extends ApiClient {
|
|||||||
destroyAvatar(contactId) {
|
destroyAvatar(contactId) {
|
||||||
return axios.delete(`${this.url}/${contactId}/avatar`);
|
return axios.delete(`${this.url}/${contactId}/avatar`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportContacts() {
|
||||||
|
return axios.get(`${this.url}/export`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ContactAPI();
|
export default new ContactAPI();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user