Compare commits
317
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fb043de54 | ||
|
|
6d07a7d92e | ||
|
|
7b9a9831c1 | ||
|
|
7ab7bac6bf | ||
|
|
b988a01df3 | ||
|
|
105f9a27d2 | ||
|
|
132cf802d5 | ||
|
|
f2f33038f2 | ||
|
|
590ce788b9 | ||
|
|
a25179c342 | ||
|
|
aea9470b6a | ||
|
|
6382707146 | ||
|
|
49ef4e54ca | ||
|
|
db0d32f01d | ||
|
|
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 | ||
|
|
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 | ||
|
|
37dd898c9a | ||
|
|
be2356724e | ||
|
|
ab6276327a | ||
|
|
ee131011f9 | ||
|
|
2ca8726005 | ||
|
|
b39e5bb642 | ||
|
|
ebd5fbef17 | ||
|
|
b7d0016d99 | ||
|
|
a0eafc94d7 | ||
|
|
110e28d08e | ||
|
|
a040aee96b | ||
|
|
6a0ca35de4 | ||
|
|
268eababa3 | ||
|
|
d2d6d271c3 | ||
|
|
363ffdbde3 | ||
|
|
44f73e044a | ||
|
|
ebc144683a | ||
|
|
44e4eee28b | ||
|
|
2e9eead20c | ||
|
|
d1ac33e98c | ||
|
|
21da03fe5b | ||
|
|
bd5ff7a8bf | ||
|
|
fd41529b82 | ||
|
|
7bd830ebfe | ||
|
|
7930902ec8 | ||
|
|
d8604107aa | ||
|
|
cef44bc557 | ||
|
|
7e3a4d2c20 | ||
|
|
a4c9a2b2f2 | ||
|
|
d1b65b6c9e | ||
|
|
4ed35cf461 | ||
|
|
803015b7f8 | ||
|
|
aa75666ad9 | ||
|
|
2b7ff48bcd | ||
|
|
6002394fcf | ||
|
|
bc8e8f3bb5 | ||
|
|
54a809ea54 | ||
|
|
fdb067a352 | ||
|
|
d082aa50a8 | ||
|
|
3535a1a708 | ||
|
|
4c10845acd | ||
|
|
5b7bed9640 | ||
|
|
b3850cb4fa | ||
|
|
7de89b6f9b | ||
|
|
00ee0478eb | ||
|
|
6000028f64 | ||
|
|
1e8881577a | ||
|
|
4b83bcb5ca | ||
|
|
70e7530cb4 | ||
|
|
d666afd757 | ||
|
|
a6e7737c56 | ||
|
|
856d9067b0 | ||
|
|
d94f195284 | ||
|
|
1370cf3c07 | ||
|
|
1dd7cfc25d | ||
|
|
f3aba84063 | ||
|
|
9c040af028 | ||
|
|
4cae5c7d51 | ||
|
|
e5134c9ef5 | ||
|
|
4f936aada5 | ||
|
|
0c9f129c74 | ||
|
|
d2aa5f4c69 | ||
|
|
e76b63f91d | ||
|
|
5c5764ca85 | ||
|
|
cf487c76a0 | ||
|
|
6848433a4c | ||
|
|
3e6d23e071 | ||
|
|
565b87fa98 | ||
|
|
de8c26dce8 | ||
|
|
7331154f04 | ||
|
|
a99c37ae5e | ||
|
|
a4bcb7f154 | ||
|
|
eb7070d946 | ||
|
|
e8a174f689 | ||
|
|
24f8befdf2 | ||
|
|
abe57873db | ||
|
|
cae3ac94cd | ||
|
|
da76537011 | ||
|
|
7cbf1857e4 | ||
|
|
8f4d4798c2 | ||
|
|
2e95d3a173 | ||
|
|
c9b63ae8eb | ||
|
|
f2684545d9 | ||
|
|
fbdc79df76 | ||
|
|
54b7c98795 | ||
|
|
757e1bb1f7 | ||
|
|
4673cf8cf1 | ||
|
|
dd8f8fc845 | ||
|
|
c20410f3f4 | ||
|
|
9bc0f67f54 | ||
|
|
eb4da33cdc | ||
|
|
0fe05dc1b9 | ||
|
|
d59fd6b747 | ||
|
|
5166fd8948 | ||
|
|
5214be67c7 | ||
|
|
d6f3643bf0 | ||
|
|
9c9183a352 | ||
|
|
c88792f4a3 | ||
|
|
2abc57300c | ||
|
|
9c6eb8b03d | ||
|
|
88ed028a06 | ||
|
|
2a385f377c | ||
|
|
ec04ddc725 | ||
|
|
60fee519bd | ||
|
|
a685e065da | ||
|
|
a4fc0eef4b | ||
|
|
a6405ea339 | ||
|
|
b185059681 | ||
|
|
89c391e7c0 | ||
|
|
9e8eb293e9 | ||
|
|
61d0a63bf7 | ||
|
|
f1827c82fd | ||
|
|
d870b0815a | ||
|
|
daf17046e9 | ||
|
|
c8cdff8bc4 | ||
|
|
34a2486e9c | ||
|
|
40e81c63ad | ||
|
|
c9c3ac4b44 | ||
|
|
4c921d3d0e | ||
|
|
e5090fcdc7 | ||
|
|
0a993978ba | ||
|
|
d4e7eaecce | ||
|
|
9bd47588fc | ||
|
|
d5a2756462 | ||
|
|
42d9b6ffed | ||
|
|
04f30e3033 | ||
|
|
69a1c4527f | ||
|
|
b141fc1289 | ||
|
|
ce807d3251 | ||
|
|
b76fda53a2 | ||
|
|
eb55ff5c9b | ||
|
|
bfb445186d | ||
|
|
7d4e6d0257 | ||
|
|
06ffaa90fc | ||
|
|
92d0398744 | ||
|
|
76650c86cd | ||
|
|
c998c84bc0 | ||
|
|
26e760a281 | ||
|
|
9fcb29484d | ||
|
|
6d4b894f95 | ||
|
|
e7d0bf8a1b | ||
|
|
60f953cd27 | ||
|
|
71f2b27728 | ||
|
|
6407745571 | ||
|
|
409466bbd5 | ||
|
|
87aabfbb9a | ||
|
|
fdf8b3f369 | ||
|
|
2674130714 | ||
|
|
8935933266 | ||
|
|
cd6a836bf6 | ||
|
|
b905d4854a | ||
|
|
aff97bff26 | ||
|
|
e3d9a0441d | ||
|
|
930863b25b | ||
|
|
62de25960c | ||
|
|
26f164d6a0 | ||
|
|
c9242fac9e | ||
|
|
b479b7c6d7 | ||
|
|
4d719a8fe3 | ||
|
|
71d8195845 | ||
|
|
59964a4f41 | ||
|
|
8c49a2efc5 |
@@ -7,7 +7,7 @@ defaults: &defaults
|
|||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
docker:
|
||||||
# specify the version you desire here
|
# specify the version you desire here
|
||||||
- image: 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
|
||||||
@@ -102,8 +102,8 @@ jobs:
|
|||||||
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.3.0/openapi-generator-cli-5.3.0.jar > ~/tmp/openapi-generator-cli-5.3.0.jar
|
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-5.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: yarn install --check-files
|
||||||
|
|||||||
@@ -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.0.4"
|
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
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
// 1025,8025 mailhog
|
// 1025,8025 mailhog
|
||||||
"forwardPorts": [8025, 3000, 3035],
|
"forwardPorts": [8025, 3000, 3035],
|
||||||
|
|
||||||
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn",
|
"postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && yarn",
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"3000": {
|
"3000": {
|
||||||
"label": "Rails Server"
|
"label": "Rails Server"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
.bundle
|
.bundle
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
docker-compose.*
|
docker-compose.*
|
||||||
docker/Dockerfile
|
docker/Dockerfile
|
||||||
docker/dockerfiles
|
docker/dockerfiles
|
||||||
|
|||||||
+8
-8
@@ -52,6 +52,8 @@ POSTGRES_HOST=postgres
|
|||||||
POSTGRES_USERNAME=postgres
|
POSTGRES_USERNAME=postgres
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=
|
||||||
RAILS_ENV=development
|
RAILS_ENV=development
|
||||||
|
# Changes the Postgres query timeout limit. The default is 14 seconds. Modify only when required.
|
||||||
|
# POSTGRES_STATEMENT_TIMEOUT=14s
|
||||||
RAILS_MAX_THREADS=5
|
RAILS_MAX_THREADS=5
|
||||||
|
|
||||||
# The email from which all outgoing emails are sent
|
# The email from which all outgoing emails are sent
|
||||||
@@ -108,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
|
||||||
|
|
||||||
@@ -157,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
|
||||||
@@ -169,6 +170,9 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||||||
## Sentry
|
## Sentry
|
||||||
# SENTRY_DSN=
|
# SENTRY_DSN=
|
||||||
|
|
||||||
|
## LogRocket
|
||||||
|
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
|
||||||
|
|
||||||
## Scout
|
## Scout
|
||||||
## https://scoutapm.com/docs/ruby/configuration
|
## https://scoutapm.com/docs/ruby/configuration
|
||||||
# SCOUT_KEY=YOURKEY
|
# SCOUT_KEY=YOURKEY
|
||||||
@@ -186,11 +190,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||||||
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
||||||
# DD_TRACE_AGENT_URL=
|
# DD_TRACE_AGENT_URL=
|
||||||
|
|
||||||
## IP look up configuration
|
# MaxMindDB API key to download GeoLite2 City database
|
||||||
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
|
|
||||||
## works only on accounts with ip look up feature enabled
|
|
||||||
# IP_LOOKUP_SERVICE=geoip2
|
|
||||||
# maxmindb api key to use geoip2 service
|
|
||||||
# IP_LOOKUP_API_KEY=
|
# IP_LOOKUP_API_KEY=
|
||||||
|
|
||||||
## Rack Attack configuration
|
## Rack Attack configuration
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -44,3 +44,9 @@ jobs:
|
|||||||
# sudo systemctl restart chatwoot.target
|
# sudo systemctl restart chatwoot.target
|
||||||
# curl http://localhost:3000/api
|
# curl http://localhost:3000/api
|
||||||
|
|
||||||
|
- name: Upload chatwoot setup log file as an artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: chatwoot-setup-log-file
|
||||||
|
path: /var/log/chatwoot-setup.log
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:10.8
|
image: postgres:10.8
|
||||||
@@ -49,6 +49,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
- name: yarn
|
- name: yarn
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
||||||
@@ -70,3 +74,10 @@ 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
|
||||||
|
|
||||||
|
- name: Upload rails log folder
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: rails-log-folder
|
||||||
|
path: log
|
||||||
|
|||||||
+10
-2
@@ -61,8 +61,16 @@ 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
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
npx --no-install lint-staged
|
npx --no-install lint-staged
|
||||||
|
|
||||||
# lint only staged ruby files
|
# lint only staged ruby files
|
||||||
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop -a
|
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a
|
||||||
|
|
||||||
# stage rubocop changes to files
|
# stage rubocop changes to files
|
||||||
git diff --name-only --cached | xargs git add
|
git diff --name-only --cached | xargs git add
|
||||||
|
|||||||
@@ -71,6 +71,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:
|
||||||
@@ -158,8 +162,33 @@ 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
|
||||||
Vendored
+6
-1
@@ -1 +1,6 @@
|
|||||||
{}
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"chatwoot",
|
||||||
|
"dompurify"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.1'
|
gem 'rails', '~> 7'
|
||||||
# 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
|
||||||
|
|
||||||
@@ -36,19 +36,19 @@ gem 'json_schemer'
|
|||||||
# Rack middleware for blocking & throttling abusive requests
|
# Rack middleware for blocking & throttling abusive requests
|
||||||
gem 'rack-attack'
|
gem 'rack-attack'
|
||||||
# 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,13 +62,13 @@ 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'
|
||||||
@@ -81,33 +81,40 @@ gem 'administrate'
|
|||||||
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 ---#
|
||||||
gem 'ddtrace'
|
# loaded only when environment variables are set.
|
||||||
gem 'elastic-apm'
|
# ref application.rb
|
||||||
gem 'newrelic_rpm'
|
gem 'ddtrace', require: false
|
||||||
gem 'scout_apm'
|
gem 'elastic-apm', require: false
|
||||||
gem 'sentry-rails', '~> 5.3', '>= 5.3.1'
|
gem 'newrelic_rpm', require: false
|
||||||
gem 'sentry-ruby', '~> 5.3'
|
gem 'newrelic-sidekiq-metrics', require: false
|
||||||
gem 'sentry-sidekiq', '~> 5.3', '>= 5.3.1'
|
gem 'scout_apm', require: false
|
||||||
|
gem 'sentry-rails', require: false
|
||||||
|
gem 'sentry-ruby', require: false
|
||||||
|
gem 'sentry-sidekiq', require: false
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '~> 6.4.2'
|
gem 'sidekiq'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron', '~> 1.6', '>= 1.6.0'
|
gem 'sidekiq-cron'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
@@ -126,7 +133,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'
|
||||||
@@ -141,12 +151,23 @@ 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.12.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 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
|
||||||
@@ -155,6 +176,7 @@ group :development do
|
|||||||
gem 'annotate'
|
gem 'annotate'
|
||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
gem 'letter_opener'
|
gem 'letter_opener'
|
||||||
|
gem 'scss_lint', require: false
|
||||||
gem 'web-console'
|
gem 'web-console'
|
||||||
|
|
||||||
# used in swagger build
|
# used in swagger build
|
||||||
@@ -162,11 +184,15 @@ group :development do
|
|||||||
|
|
||||||
# When we want to squash migrations
|
# When we want to squash migrations
|
||||||
gem 'squasher'
|
gem 'squasher'
|
||||||
|
|
||||||
|
# profiling
|
||||||
|
gem 'rack-mini-profiler', require: false
|
||||||
|
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
|
||||||
@@ -188,7 +214,7 @@ group :development, :test do
|
|||||||
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
|
||||||
@@ -199,11 +225,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'
|
|
||||||
|
|
||||||
# need for google auth
|
|
||||||
gem 'omniauth'
|
|
||||||
gem 'omniauth-google-oauth2'
|
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
|
||||||
|
|||||||
+381
-340
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
|||||||
|
# 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:
|
||||||
|
overmind start -f Procfile.dev
|
||||||
|
|
||||||
|
docker:
|
||||||
|
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
|
||||||
|
|
||||||
|
.PHONY: setup db_create db_migrate db_seed db console server burn docker run
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
release: bundle exec rails db:chatwoot_prepare
|
release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare && echo $SOURCE_VERSION > .git_sha
|
||||||
web: 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 sidekiq -C config/sidekiq.yml
|
worker: bundle exec rails ip_lookup:setup && 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
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
//= link administrate/application.css
|
//= link administrate/application.css
|
||||||
//= link administrate/application.js
|
//= link administrate/application.js
|
||||||
//= link dashboardChart.js
|
//= link dashboardChart.js
|
||||||
|
//= link secretField.js
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// eslint-disable-next-line
|
||||||
|
function toggleSecretField(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const toggler = e.currentTarget;
|
||||||
|
const secretField = toggler.parentElement;
|
||||||
|
const textElement = secretField.querySelector('[data-secret-masked]');
|
||||||
|
|
||||||
|
if (!textElement) return;
|
||||||
|
|
||||||
|
if (textElement.dataset.secretMasked === 'false') {
|
||||||
|
textElement.textContent = '•'.repeat(10);
|
||||||
|
textElement.dataset.secretMasked = 'true';
|
||||||
|
toggler.querySelector('svg use').setAttribute('xlink:href', '#eye-show');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textElement.textContent = secretField.dataset.secretText;
|
||||||
|
textElement.dataset.secretMasked = 'false';
|
||||||
|
toggler.querySelector('svg use').setAttribute('xlink:href', '#eye-hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
function copySecretField(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const toggler = e.currentTarget;
|
||||||
|
const secretField = toggler.parentElement;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(secretField.dataset.secretText);
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
@import 'utilities/text-color';
|
@import 'utilities/text-color';
|
||||||
|
|
||||||
@import 'selectize';
|
@import 'selectize';
|
||||||
@import 'datetime_picker';
|
|
||||||
|
|
||||||
@import 'library/clearfix';
|
@import 'library/clearfix';
|
||||||
@import 'library/data-label';
|
@import 'library/data-label';
|
||||||
|
|||||||
@@ -43,3 +43,20 @@
|
|||||||
.cell-label--number {
|
.cell-label--number {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-data__secret-field {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,7 +116,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
result = {}
|
result = {}
|
||||||
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
||||||
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo
|
if e.message.include?('2018218')
|
||||||
|
Rails.logger.warn e
|
||||||
|
else
|
||||||
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo
|
||||||
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
result = {}
|
result = {}
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
|
|||||||
@@ -63,28 +63,42 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
@conversation ||= Conversation.where(
|
||||||
|
"additional_attributes ->> 'type' = 'instagram_direct_message'"
|
||||||
|
).find_by(conversation_params) || 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 +106,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
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ class Messages::MessageBuilder
|
|||||||
return unless @conversation.inbox&.inbox_type == 'Email'
|
return unless @conversation.inbox&.inbox_type == 'Email'
|
||||||
|
|
||||||
cc_emails = []
|
cc_emails = []
|
||||||
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails].present?
|
cc_emails = @params[:cc_emails].gsub(/\s+/, '').split(',') if @params[:cc_emails].present?
|
||||||
|
|
||||||
bcc_emails = []
|
bcc_emails = []
|
||||||
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails].present?
|
bcc_emails = @params[:bcc_emails].gsub(/\s+/, '').split(',') if @params[:bcc_emails].present?
|
||||||
|
|
||||||
all_email_addresses = cc_emails + bcc_emails
|
all_email_addresses = cc_emails + bcc_emails
|
||||||
validate_email_addresses(all_email_addresses)
|
validate_email_addresses(all_email_addresses)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class V2::ReportBuilder
|
|||||||
:created_at,
|
:created_at,
|
||||||
default_value: 0,
|
default_value: 0,
|
||||||
range: range,
|
range: range,
|
||||||
permit: %w[day week month year],
|
permit: %w[day week month year hour],
|
||||||
time_zone: @timezone
|
time_zone: @timezone
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@agent.current_account_user.destroy!
|
@agent.current_account_user.destroy!
|
||||||
|
delete_user_record(@agent)
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -74,4 +75,8 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||||||
def validate_limit
|
def validate_limit
|
||||||
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
|
render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_user_record(agent)
|
||||||
|
DeleteObjectJob.perform_later(agent) if agent.reload.account_users.blank?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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, :attach_file, :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?
|
||||||
@@ -43,6 +47,11 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||||||
render json: { file_url: url_for(file_blob) }
|
render json: { file_url: url_for(file_blob) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reorder
|
||||||
|
Article.update_positions(params[:positions_hash])
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_article
|
def fetch_article
|
||||||
@@ -55,9 +64,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
def article_params
|
def article_params
|
||||||
params.require(:article).permit(
|
params.require(:article).permit(
|
||||||
:title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title,
|
:title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title,
|
||||||
:description,
|
:description,
|
||||||
{ tags: [] }]
|
{ tags: [] }]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -28,8 +30,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
automation_rule_update
|
automation_rule_update
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -119,11 +119,12 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contacts_with_conversation_count(contacts)
|
def fetch_contacts_with_conversation_count(contacts)
|
||||||
contacts_with_conversation_count = filtrate(contacts).left_outer_joins(:conversations)
|
conversation_count_sub_query = 'SELECT COUNT(*) FROM "conversations" WHERE "conversations"."contact_id" = "contacts"."id"'
|
||||||
.select('contacts.*, COUNT(conversations.id) as conversations_count')
|
contacts_with_conversation_count = filtrate(contacts)
|
||||||
.group('contacts.id')
|
.select("contacts.*, (#{conversation_count_sub_query}) as conversations_count")
|
||||||
.includes([{ avatar_attachment: [:blob] }])
|
.group('contacts.id')
|
||||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
.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_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,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 +35,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 +68,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
|
||||||
|
|
||||||
@@ -111,11 +116,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?
|
||||||
@@ -142,6 +142,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
# and deprecate the support of passing only source_id as the param
|
# and deprecate the support of passing only source_id as the param
|
||||||
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||||
authorize @contact_inbox.inbox, :show?
|
authorize @contact_inbox.inbox, :show?
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
render json: { error: 'source_id should be unique' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@inbox.destroy!
|
::DeleteObjectJob.perform_later(@inbox) if @inbox.present?
|
||||||
head :ok
|
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -114,10 +114,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]
|
||||||
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::AppsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_admin_authorization?
|
before_action :check_admin_authorization?, except: [:index, :show]
|
||||||
before_action :fetch_apps, only: [:index]
|
before_action :fetch_apps, only: [:index]
|
||||||
before_action :fetch_app, only: [:show]
|
before_action :fetch_app, only: [:show]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,8 +22,16 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||||||
@macro
|
@macro
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def update
|
||||||
head :not_found if @macro.nil?
|
ActiveRecord::Base.transaction do
|
||||||
|
@macro.update!(macros_with_user)
|
||||||
|
@macro.set_visibility(current_user, permitted_params)
|
||||||
|
process_attachments
|
||||||
|
@macro.save!
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error e
|
||||||
|
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -37,18 +49,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
@macro.update!(macros_with_user)
|
|
||||||
@macro.set_visibility(current_user, permitted_params)
|
|
||||||
process_attachments
|
|
||||||
@macro.save!
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error e
|
|
||||||
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
|
|||||||
redirect_url = microsoft_client.auth_code.authorize_url(
|
redirect_url = microsoft_client.auth_code.authorize_url(
|
||||||
{
|
{
|
||||||
redirect_uri: "#{base_url}/microsoft/callback",
|
redirect_uri: "#{base_url}/microsoft/callback",
|
||||||
scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid',
|
scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile',
|
||||||
prompt: 'consent'
|
prompt: 'consent'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if redirect_url
|
if redirect_url
|
||||||
|
email = email.downcase
|
||||||
::Redis::Alfred.setex(email, Current.account.id, 5.minutes)
|
::Redis::Alfred.setex(email, Current.account.id, 5.minutes)
|
||||||
render json: { success: true, url: redirect_url }
|
render json: { success: true, url: redirect_url }
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
class Api::V1::Accounts::SearchController < Api::V1::Accounts::BaseController
|
||||||
|
def index
|
||||||
|
@result = search('all')
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversations
|
||||||
|
@result = search('Conversation')
|
||||||
|
end
|
||||||
|
|
||||||
|
def contacts
|
||||||
|
@result = search('Contact')
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages
|
||||||
|
@result = search('Message')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def search(search_type)
|
||||||
|
SearchService.new(
|
||||||
|
current_user: Current.user,
|
||||||
|
current_account: Current.account,
|
||||||
|
search_type: search_type,
|
||||||
|
params: params
|
||||||
|
).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
class Api::V1::AccountsController < Api::BaseController
|
class Api::V1::AccountsController < Api::BaseController
|
||||||
include AuthHelper
|
include AuthHelper
|
||||||
|
include CacheKeysHelper
|
||||||
|
|
||||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||||
only: [:create], raise: false
|
only: [:create], raise: false
|
||||||
@@ -13,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],
|
||||||
@@ -30,9 +36,8 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def cache_keys
|
||||||
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
render json: { cache_keys: get_cache_keys }, status: :ok
|
||||||
render 'api/v1/accounts/show', format: :json
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -47,6 +52,14 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def get_cache_keys
|
||||||
|
{
|
||||||
|
label: fetch_value_for_key(params[:id], Label.name.underscore),
|
||||||
|
inbox: fetch_value_for_key(params[:id], Inbox.name.underscore),
|
||||||
|
team: fetch_value_for_key(params[:id], Team.name.underscore)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_account
|
def fetch_account
|
||||||
@account = current_user.accounts.find(params[:id])
|
@account = current_user.accounts.find(params[:id])
|
||||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||||
|
|||||||
@@ -65,6 +65,16 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_custom_attributes
|
||||||
|
conversation.update!(custom_attributes: permitted_params[:custom_attributes])
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_custom_attributes
|
||||||
|
conversation.custom_attributes = conversation.custom_attributes.excluding(params[:custom_attribute])
|
||||||
|
conversation.save!
|
||||||
|
render json: conversation
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def trigger_typing_event(event)
|
def trigger_typing_event(event)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ 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 }
|
params: { email: contact_email, name: contact_name }
|
||||||
).perform
|
).perform
|
||||||
else
|
else
|
||||||
@message.update!(message_update_params[:message])
|
@message.update!(message_update_params[:message])
|
||||||
@@ -48,7 +48,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 +63,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
|
||||||
@@ -32,6 +34,14 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||||||
generate_csv('teams_report', 'api/v2/accounts/reports/teams')
|
generate_csv('teams_report', 'api/v2/accounts/reports/teams')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation_traffic
|
||||||
|
@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')
|
||||||
|
end
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
return head :unprocessable_entity if params[:type].blank?
|
return head :unprocessable_entity if params[:type].blank?
|
||||||
|
|
||||||
@@ -62,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
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ class DashboardController < ActionController::Base
|
|||||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||||
FACEBOOK_API_VERSION: 'v14.0',
|
FACEBOOK_API_VERSION: 'v14.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
|
||||||
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: []
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Microsoft::CallbacksController < ApplicationController
|
|||||||
)
|
)
|
||||||
|
|
||||||
inbox = find_or_create_inbox
|
inbox = find_or_create_inbox
|
||||||
::Redis::Alfred.delete(users_data['email'])
|
::Redis::Alfred.delete(users_data['email'].downcase)
|
||||||
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e).capture_exception
|
ChatwootExceptionTracker.new(e).capture_exception
|
||||||
@@ -31,7 +31,7 @@ class Microsoft::CallbacksController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_id
|
def account_id
|
||||||
::Redis::Alfred.get(users_data['email'])
|
::Redis::Alfred.get(users_data['email'].downcase)
|
||||||
end
|
end
|
||||||
|
|
||||||
def account
|
def account
|
||||||
@@ -45,13 +45,18 @@ class Microsoft::CallbacksController < ApplicationController
|
|||||||
channel_email.inbox
|
channel_email.inbox
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fallback name, for when name field is missing from users_data
|
||||||
|
def fallback_name
|
||||||
|
users_data['email'].split('@').first.parameterize.titleize
|
||||||
|
end
|
||||||
|
|
||||||
def create_microsoft_channel_with_inbox
|
def create_microsoft_channel_with_inbox
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
||||||
account.inboxes.create!(
|
account.inboxes.create!(
|
||||||
account: account,
|
account: account,
|
||||||
channel: channel_email,
|
channel: channel_email,
|
||||||
name: users_data['name']
|
name: users_data['name'] || fallback_name
|
||||||
)
|
)
|
||||||
channel_email
|
channel_email
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
render json: { error: 'You cannot update the CSAT survey after 14 days' }, status: :unprocessable_entity and return if check_csat_locked
|
||||||
|
|
||||||
@message.update!(message_update_params)
|
@message.update!(message_update_params)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error
|
render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error
|
||||||
@@ -43,7 +45,7 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def message_update_params
|
def message_update_params
|
||||||
params.permit(submitted_values: [:name, :title, :value])
|
params.permit(submitted_values: [:name, :title, :value, { csat_survey_response: [:feedback_message, :rating] }])
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
@@ -64,4 +66,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
|||||||
message_type: :incoming
|
message_type: :incoming
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_csat_locked
|
||||||
|
(Time.zone.now.to_date - @message.created_at.to_date).to_i > 14 and @message.content_type == 'input_csat'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
class Public::Api::V1::Portals::ArticlesController < PublicController
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
@@ -15,21 +16,30 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_article
|
def set_article
|
||||||
@article = @category.articles.find(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
|
||||||
|
|
||||||
def set_category
|
def set_category
|
||||||
@category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present?
|
return if permitted_params[:category_slug].blank?
|
||||||
|
|
||||||
|
@category = @portal.categories.find_by!(
|
||||||
|
slug: permitted_params[:category_slug],
|
||||||
|
locale: permitted_params[:locale]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal
|
def portal
|
||||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:query)
|
params.permit(:query, :locale)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:slug, :category_slug, :locale, :id, :article_slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_article_content(content)
|
def render_article_content(content)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
class Public::Api::V1::Portals::BaseController < PublicController
|
||||||
|
around_action :set_locale
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_locale(&)
|
||||||
|
switch_locale_with_portal(&) if params[:locale].present?
|
||||||
|
switch_locale_with_article(&) if params[:article_slug].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def switch_locale_with_portal(&)
|
||||||
|
locale_without_variant = params[:locale].split('_')[0]
|
||||||
|
is_locale_available = I18n.available_locales.map(&:to_s).include?(params[:locale])
|
||||||
|
is_locale_variant_available = I18n.available_locales.map(&:to_s).include?(locale_without_variant)
|
||||||
|
if is_locale_available
|
||||||
|
@locale = params[:locale]
|
||||||
|
elsif is_locale_variant_available
|
||||||
|
@locale = locale_without_variant
|
||||||
|
end
|
||||||
|
|
||||||
|
I18n.with_locale(@locale, &)
|
||||||
|
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
|
||||||
|
end
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
class Public::Api::V1::Portals::CategoriesController < PublicController
|
class Public::Api::V1::Portals::CategoriesController < 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, only: [:show]
|
before_action :set_category, only: [:show]
|
||||||
layout 'portal'
|
layout 'portal'
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@categories = @portal.categories
|
@categories = @portal.categories.order(position: :asc)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class Public::Api::V1::PortalsController < PublicController
|
class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseController
|
||||||
before_action :ensure_custom_domain_request, only: [:show]
|
before_action :ensure_custom_domain_request, only: [:show]
|
||||||
before_action :portal
|
before_action :portal
|
||||||
before_action :redirect_to_portal_with_locale, only: [:show]
|
before_action :redirect_to_portal_with_locale, only: [:show]
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ 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 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
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
||||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||||
# For example, you may want to send an email after a foo is updated.
|
# For example, you may want to send an email after a foo is updated.
|
||||||
|
|
||||||
|
def create
|
||||||
|
resource = resource_class.new(resource_params)
|
||||||
|
authorize_resource(resource)
|
||||||
|
|
||||||
|
if resource.save
|
||||||
|
redirect_to super_admin_user_path(resource), notice: translate_with_resource('create.success')
|
||||||
|
else
|
||||||
|
notice = resource.errors.full_messages.first
|
||||||
|
redirect_to new_super_admin_user_path, notice: notice
|
||||||
|
end
|
||||||
|
end
|
||||||
#
|
#
|
||||||
# def update
|
# def update
|
||||||
# super
|
# super
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class WidgetsController < ActionController::Base
|
|||||||
before_action :set_global_config
|
before_action :set_global_config
|
||||||
before_action :set_web_widget
|
before_action :set_web_widget
|
||||||
before_action :ensure_account_is_active
|
before_action :ensure_account_is_active
|
||||||
|
before_action :ensure_location_is_supported
|
||||||
before_action :set_token
|
before_action :set_token
|
||||||
before_action :set_contact
|
before_action :set_contact
|
||||||
before_action :build_contact
|
before_action :build_contact
|
||||||
@@ -18,6 +19,9 @@ class WidgetsController < ActionController::Base
|
|||||||
|
|
||||||
def set_web_widget
|
def set_web_widget
|
||||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
Rails.logger.error('web widget does not exist')
|
||||||
|
render json: { error: 'web widget does not exist' }, status: :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_token
|
def set_token
|
||||||
@@ -37,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
|
||||||
@@ -51,6 +55,8 @@ class WidgetsController < ActionController::Base
|
|||||||
render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active?
|
render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_location_is_supported; end
|
||||||
|
|
||||||
def additional_attributes
|
def additional_attributes
|
||||||
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
|
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
|
||||||
{ created_at_ip: request.remote_ip }
|
{ created_at_ip: request.remote_ip }
|
||||||
@@ -67,3 +73,5 @@ class WidgetsController < ActionController::Base
|
|||||||
response.headers.delete('X-Frame-Options')
|
response.headers.delete('X-Frame-Options')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
WidgetsController.prepend_mod_with('WidgetsController')
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class AccessTokenDashboard < Administrate::BaseDashboard
|
|||||||
ATTRIBUTE_TYPES = {
|
ATTRIBUTE_TYPES = {
|
||||||
owner: Field::Polymorphic,
|
owner: Field::Polymorphic,
|
||||||
id: Field::Number,
|
id: Field::Number,
|
||||||
token: Field::String,
|
token: SecretField,
|
||||||
created_at: Field::DateTime,
|
created_at: Field::DateTime,
|
||||||
updated_at: Field::DateTime
|
updated_at: Field::DateTime
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
class AccountDrop < BaseDrop
|
class AccountDrop < BaseDrop
|
||||||
|
def name
|
||||||
|
@obj.try(:name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
class ContactDrop < BaseDrop
|
class ContactDrop < BaseDrop
|
||||||
|
def name
|
||||||
|
@obj.try(:name).try(:split).try(:map, &:capitalize).try(:join, ' ')
|
||||||
|
end
|
||||||
|
|
||||||
def email
|
def email
|
||||||
@obj.try(:email)
|
@obj.try(:email)
|
||||||
end
|
end
|
||||||
@@ -8,10 +12,10 @@ class ContactDrop < BaseDrop
|
|||||||
end
|
end
|
||||||
|
|
||||||
def first_name
|
def first_name
|
||||||
@obj.try(:name).try(:split).try(:first)
|
@obj.try(:name).try(:split).try(:first).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_name
|
def last_name
|
||||||
@obj.try(:name).try(:split).try(:last) if @obj.try(:name).try(:split).try(:size) > 1
|
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
class UserDrop < BaseDrop
|
class UserDrop < BaseDrop
|
||||||
|
def name
|
||||||
|
@obj.try(:name).try(:split).try(:map, &:capitalize).try(:join, ' ')
|
||||||
|
end
|
||||||
|
|
||||||
def available_name
|
def available_name
|
||||||
@obj.try(:available_name)
|
@obj.try(:available_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def first_name
|
def first_name
|
||||||
@obj.try(:name).try(:split).try(:first)
|
@obj.try(:name).try(:split).try(:first).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_name
|
def last_name
|
||||||
@obj.try(:name).try(:split).try(:last) if @obj.try(:name).try(:split).try(:size) > 1
|
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
require 'administrate/field/base'
|
||||||
|
|
||||||
|
class SecretField < Administrate::Field::String
|
||||||
|
end
|
||||||
@@ -5,7 +5,8 @@ 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'
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
|
|
||||||
# assumptions
|
# assumptions
|
||||||
@@ -53,9 +54,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
|
||||||
@@ -106,6 +108,8 @@ class ConversationFinder
|
|||||||
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 +124,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,
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
class EmailChannelFinder
|
class EmailChannelFinder
|
||||||
|
include EmailHelper
|
||||||
|
|
||||||
def initialize(email_object)
|
def initialize(email_object)
|
||||||
@email_object = email_object
|
@email_object = email_object
|
||||||
end
|
end
|
||||||
|
|
||||||
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|
|
||||||
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase)
|
normalized_email = normalize_email_with_plus_addressing(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
|
||||||
|
|||||||
@@ -21,12 +21,30 @@ class MessageFinder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def current_messages
|
def current_messages
|
||||||
if @params[:after].present?
|
if @params[:after].present? && @params[:before].present?
|
||||||
messages.reorder('created_at asc').where('id >= ?', @params[:before].to_i).limit(20)
|
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
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
module CacheKeysHelper
|
||||||
|
def get_prefixed_cache_key(account_id, key)
|
||||||
|
"idb-cache-key-account-#{account_id}-#{key}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_value_for_key(account_id, key)
|
||||||
|
prefixed_cache_key = get_prefixed_cache_key(account_id, key)
|
||||||
|
value_from_cache = Redis::Alfred.get(prefixed_cache_key)
|
||||||
|
|
||||||
|
return value_from_cache if value_from_cache.present?
|
||||||
|
|
||||||
|
# zero epoch time: 1970-01-01 00:00:00 UTC
|
||||||
|
'0000000000'
|
||||||
|
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)
|
||||||
|
|||||||
@@ -3,4 +3,33 @@ module EmailHelper
|
|||||||
domain = email.split('@').last
|
domain = email.split('@').last
|
||||||
domain.split('.').first
|
domain.split('.').first
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ref: https://www.rfc-editor.org/rfc/rfc5233.html
|
||||||
|
# This is not a mandatory requirement for email addresses, but it is a common practice.
|
||||||
|
# john+test@xyc.com is the same as john@xyc.com
|
||||||
|
def normalize_email_with_plus_addressing(email)
|
||||||
|
"#{email.split('@').first.split('+').first}@#{email.split('@').last}".downcase
|
||||||
|
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
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ module ReportingEventHelper
|
|||||||
from_in_inbox_timezone.working_time_until(to_in_inbox_timezone)
|
from_in_inbox_timezone.working_time_until(to_in_inbox_timezone)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def last_non_human_activity(conversation)
|
||||||
|
# check if a handoff event already exists
|
||||||
|
handoff_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_handoff').last
|
||||||
|
|
||||||
|
# if a handoff exists, last non human activity is when the handoff ended,
|
||||||
|
# otherwise it's when the conversation was created
|
||||||
|
handoff_event&.event_end_time || conversation.created_at
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def configure_working_hours(working_hours)
|
def configure_working_hours(working_hours)
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
|
<div
|
||||||
|
v-if="!authUIFlags.isFetching"
|
||||||
|
id="app"
|
||||||
|
class="app-wrapper app-root"
|
||||||
|
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||||
|
>
|
||||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<router-view />
|
<router-view />
|
||||||
@@ -22,6 +27,7 @@ import NetworkNotification from './components/NetworkNotification';
|
|||||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
import UpdateBanner from './components/app/UpdateBanner.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 {
|
import {
|
||||||
registerSubscription,
|
registerSubscription,
|
||||||
verifyServiceWorkerExistence,
|
verifyServiceWorkerExistence,
|
||||||
@@ -38,6 +44,8 @@ export default {
|
|||||||
WootSnackbarBox,
|
WootSnackbarBox,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mixins: [rtlMixin],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showAddAccountModal: false,
|
showAddAccountModal: false,
|
||||||
@@ -64,13 +72,6 @@ export default {
|
|||||||
if (!this.hasAccounts) {
|
if (!this.hasAccounts) {
|
||||||
this.showAddAccountModal = true;
|
this.showAddAccountModal = true;
|
||||||
}
|
}
|
||||||
verifyServiceWorkerExistence(registration =>
|
|
||||||
registration.pushManager.getSubscription().then(subscription => {
|
|
||||||
if (subscription) {
|
|
||||||
registerSubscription();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
currentAccountId() {
|
currentAccountId() {
|
||||||
if (this.currentAccountId) {
|
if (this.currentAccountId) {
|
||||||
@@ -96,8 +97,17 @@ export default {
|
|||||||
} = this.getAccount(this.currentAccountId);
|
} = this.getAccount(this.currentAccountId);
|
||||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||||
this.setLocale(locale);
|
this.setLocale(locale);
|
||||||
|
this.updateRTLDirectionView(locale);
|
||||||
this.latestChatwootVersion = latestChatwootVersion;
|
this.latestChatwootVersion = latestChatwootVersion;
|
||||||
vueActionCable.init(pubsubToken);
|
vueActionCable.init(pubsubToken);
|
||||||
|
|
||||||
|
verifyServiceWorkerExistence(registration =>
|
||||||
|
registration.pushManager.getSubscription().then(subscription => {
|
||||||
|
if (subscription) {
|
||||||
|
registerSubscription();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ class ApiClient {
|
|||||||
return `${this.baseUrl()}/${this.resource}`;
|
return `${this.baseUrl()}/${this.resource}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
get accountIdFromRoute() {
|
||||||
|
const isInsideAccountScopedURLs = window.location.pathname.includes(
|
||||||
|
'/app/accounts'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isInsideAccountScopedURLs) {
|
||||||
|
return window.location.pathname.split('/')[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
baseUrl() {
|
baseUrl() {
|
||||||
let url = this.apiVersion;
|
let url = this.apiVersion;
|
||||||
|
|
||||||
@@ -20,15 +33,8 @@ class ApiClient {
|
|||||||
url = `/enterprise${url}`;
|
url = `/enterprise${url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.accountScoped) {
|
if (this.options.accountScoped && this.accountIdFromRoute) {
|
||||||
const isInsideAccountScopedURLs = window.location.pathname.includes(
|
url = `${url}/accounts/${this.accountIdFromRoute}`;
|
||||||
'/app/accounts'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isInsideAccountScopedURLs) {
|
|
||||||
const accountId = window.location.pathname.split('/')[3];
|
|
||||||
url = `${url}/accounts/${accountId}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/* global axios */
|
||||||
|
import { DataManager } from '../helper/CacheHelper/DataManager';
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class CacheEnabledApiClient extends ApiClient {
|
||||||
|
constructor(resource, options = {}) {
|
||||||
|
super(resource, options);
|
||||||
|
this.dataManager = new DataManager(this.accountIdFromRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
get cacheModelName() {
|
||||||
|
throw new Error('cacheModelName is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
get(cache = false) {
|
||||||
|
if (cache) {
|
||||||
|
return this.getFromCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.get(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
extractDataFromResponse(response) {
|
||||||
|
return response.data.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
marshallData(dataToParse) {
|
||||||
|
return { data: { payload: dataToParse } };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFromCache() {
|
||||||
|
await this.dataManager.initDb();
|
||||||
|
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`/api/v1/accounts/${this.accountIdFromRoute}/cache_keys`
|
||||||
|
);
|
||||||
|
const cacheKeyFromApi = data.cache_keys[this.cacheModelName];
|
||||||
|
const isCacheValid = await this.validateCacheKey(cacheKeyFromApi);
|
||||||
|
|
||||||
|
let localData = [];
|
||||||
|
if (isCacheValid) {
|
||||||
|
localData = await this.dataManager.get({
|
||||||
|
modelName: this.cacheModelName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localData.length === 0) {
|
||||||
|
return this.refetchAndCommit(cacheKeyFromApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.marshallData(localData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refetchAndCommit(newKey = null) {
|
||||||
|
await this.dataManager.initDb();
|
||||||
|
const response = await axios.get(this.url);
|
||||||
|
this.dataManager.replace({
|
||||||
|
modelName: this.cacheModelName,
|
||||||
|
data: this.extractDataFromResponse(response),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.dataManager.setCacheKeys({
|
||||||
|
[this.cacheModelName]: newKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateCacheKey(cacheKeyFromApi) {
|
||||||
|
if (!this.dataManager.db) {
|
||||||
|
await this.dataManager.initDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachekey = await this.dataManager.getCacheKey(this.cacheModelName);
|
||||||
|
return cacheKeyFromApi === cachekey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CacheEnabledApiClient;
|
||||||
@@ -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();
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import endPoints from './endPoints';
|
import endPoints from './endPoints';
|
||||||
import { setAuthCredentials, clearCookiesOnLogout } from '../store/utils/api';
|
import {
|
||||||
|
setAuthCredentials,
|
||||||
|
clearCookiesOnLogout,
|
||||||
|
deleteIndexedDBOnLogout,
|
||||||
|
} from '../store/utils/api';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login(creds) {
|
login(creds) {
|
||||||
@@ -50,6 +54,7 @@ export default {
|
|||||||
axios
|
axios
|
||||||
.delete(urlData.url)
|
.delete(urlData.url)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
deleteIndexedDBOnLogout();
|
||||||
clearCookiesOnLogout();
|
clearCookiesOnLogout();
|
||||||
resolve(response);
|
resolve(response);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class CSATReportsAPI extends ApiClient {
|
|||||||
super('csat_survey_responses', { accountScoped: true });
|
super('csat_survey_responses', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ page, from, to, user_ids } = {}) {
|
get({ page, from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||||
return axios.get(this.url, {
|
return axios.get(this.url, {
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
@@ -14,24 +14,31 @@ class CSATReportsAPI extends ApiClient {
|
|||||||
until: to,
|
until: to,
|
||||||
sort: '-created_at',
|
sort: '-created_at',
|
||||||
user_ids,
|
user_ids,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
rating,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
download({ from, to, user_ids } = {}) {
|
download({ from, to, user_ids, inbox_id, team_id, rating } = {}) {
|
||||||
return axios.get(`${this.url}/download`, {
|
return axios.get(`${this.url}/download`, {
|
||||||
params: {
|
params: {
|
||||||
since: from,
|
since: from,
|
||||||
until: to,
|
until: to,
|
||||||
sort: '-created_at',
|
sort: '-created_at',
|
||||||
user_ids,
|
user_ids,
|
||||||
|
inbox_id,
|
||||||
|
team_id,
|
||||||
|
rating,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetrics({ from, to, user_ids } = {}) {
|
getMetrics({ from, to, user_ids, inbox_id, team_id } = {}) {
|
||||||
|
// no ratings for metrics
|
||||||
return axios.get(`${this.url}/metrics`, {
|
return axios.get(`${this.url}/metrics`, {
|
||||||
params: { since: from, until: to, user_ids },
|
params: { since: from, until: to, user_ids, inbox_id, team_id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ class ArticlesAPI extends PortalsAPI {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
|
||||||
|
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
|
||||||
|
positions_hash: reorderedGroup,
|
||||||
|
category_slug: categorySlug,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ArticlesAPI();
|
export default new ArticlesAPI();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ConversationApi extends ApiClient {
|
|||||||
labels,
|
labels,
|
||||||
teamId,
|
teamId,
|
||||||
conversationType,
|
conversationType,
|
||||||
|
sortBy,
|
||||||
}) {
|
}) {
|
||||||
return axios.get(this.url, {
|
return axios.get(this.url, {
|
||||||
params: {
|
params: {
|
||||||
@@ -24,6 +25,7 @@ class ConversationApi extends ApiClient {
|
|||||||
page,
|
page,
|
||||||
labels,
|
labels,
|
||||||
conversation_type: conversationType,
|
conversation_type: conversationType,
|
||||||
|
sort_by: sortBy,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -52,6 +54,12 @@ class ConversationApi extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
togglePriority({ conversationId, priority }) {
|
||||||
|
return axios.post(`${this.url}/${conversationId}/toggle_priority`, {
|
||||||
|
priority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
assignAgent({ conversationId, agentId }) {
|
assignAgent({ conversationId, agentId }) {
|
||||||
return axios.post(
|
return axios.post(
|
||||||
`${this.url}/${conversationId}/assignments?assignee_id=${agentId}`,
|
`${this.url}/${conversationId}/assignments?assignee_id=${agentId}`,
|
||||||
|
|||||||
@@ -75,10 +75,12 @@ class MessageApi extends ApiClient {
|
|||||||
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
|
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreviousMessages({ conversationId, before }) {
|
getPreviousMessages({ conversationId, after, before }) {
|
||||||
return axios.get(`${this.url}/${conversationId}/messages`, {
|
const params = { before };
|
||||||
params: { before },
|
if (after && Number(after) !== Number(before)) {
|
||||||
});
|
params.after = after;
|
||||||
|
}
|
||||||
|
return axios.get(`${this.url}/${conversationId}/messages`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
translateMessage(conversationId, messageId, targetLanguage) {
|
translateMessage(conversationId, messageId, targetLanguage) {
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
/* global axios */
|
/* global axios */
|
||||||
import ApiClient from './ApiClient';
|
import CacheEnabledApiClient from './CacheEnabledApiClient';
|
||||||
|
|
||||||
class Inboxes extends ApiClient {
|
class Inboxes extends CacheEnabledApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('inboxes', { accountScoped: true });
|
super('inboxes', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
get cacheModelName() {
|
||||||
|
return 'inbox';
|
||||||
|
}
|
||||||
|
|
||||||
getCampaigns(inboxId) {
|
getCampaigns(inboxId) {
|
||||||
return axios.get(`${this.url}/${inboxId}/campaigns`);
|
return axios.get(`${this.url}/${inboxId}/campaigns`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* global axios */
|
||||||
|
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
class OpenAIAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('integrations', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
processEvent({ type = 'rephrase', content, tone, conversationId, hookId }) {
|
||||||
|
let data = {
|
||||||
|
tone,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'reply_suggestion' || type === 'summarize') {
|
||||||
|
data = {
|
||||||
|
conversation_display_id: conversationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.post(`${this.url}/hooks/${hookId}/process_event`, {
|
||||||
|
event: {
|
||||||
|
name: type,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new OpenAIAPI();
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import ApiClient from './ApiClient';
|
import CacheEnabledApiClient from './CacheEnabledApiClient';
|
||||||
|
|
||||||
class LabelsAPI extends ApiClient {
|
class LabelsAPI extends CacheEnabledApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('labels', { accountScoped: true });
|
super('labels', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
get cacheModelName() {
|
||||||
|
return 'label';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new LabelsAPI();
|
export default new LabelsAPI();
|
||||||
|
|||||||
@@ -8,38 +8,39 @@ class ReportsAPI extends ApiClient {
|
|||||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||||
}
|
}
|
||||||
|
|
||||||
getReports(
|
getReports({
|
||||||
metric,
|
metric,
|
||||||
since,
|
from,
|
||||||
until,
|
to,
|
||||||
type = 'account',
|
type = 'account',
|
||||||
id,
|
id,
|
||||||
group_by,
|
groupBy,
|
||||||
business_hours
|
businessHours,
|
||||||
) {
|
}) {
|
||||||
return axios.get(`${this.url}`, {
|
return axios.get(`${this.url}`, {
|
||||||
params: {
|
params: {
|
||||||
metric,
|
metric,
|
||||||
since,
|
since: from,
|
||||||
until,
|
until: to,
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
group_by,
|
group_by: groupBy,
|
||||||
business_hours,
|
business_hours: businessHours,
|
||||||
timezone_offset: getTimeOffset(),
|
timezone_offset: getTimeOffset(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummary(since, until, type = 'account', id, group_by, business_hours) {
|
getSummary(since, until, type = 'account', id, groupBy, businessHours) {
|
||||||
return axios.get(`${this.url}/summary`, {
|
return axios.get(`${this.url}/summary`, {
|
||||||
params: {
|
params: {
|
||||||
since,
|
since,
|
||||||
until,
|
until,
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
group_by,
|
group_by: groupBy,
|
||||||
business_hours,
|
business_hours: businessHours,
|
||||||
|
timezone_offset: getTimeOffset(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,12 @@ class ReportsAPI extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConversationTrafficCSV() {
|
||||||
|
return axios.get(`${this.url}/conversation_traffic`, {
|
||||||
|
params: { timezone_offset: getTimeOffset() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getLabelReports({ from: since, to: until, businessHours }) {
|
getLabelReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/labels`, {
|
return axios.get(`${this.url}/labels`, {
|
||||||
params: { since, until, business_hours: businessHours },
|
params: { since, until, business_hours: businessHours },
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user