Compare commits
436
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
692c747ec2 | ||
|
|
09ceefe053 | ||
|
|
0685e04aae | ||
|
|
b6bf6bd414 | ||
|
|
9f70b93ff3 | ||
|
|
505ede2761 | ||
|
|
56bf543f7e | ||
|
|
216de4d123 | ||
|
|
ef90b7a3d8 | ||
|
|
58dd2633ba | ||
|
|
c62c512ec4 | ||
|
|
e086cb0d86 | ||
|
|
b4d37fa16b | ||
|
|
a9d42e8c7e | ||
|
|
81060a72a4 | ||
|
|
881d4bf644 | ||
|
|
9581264286 | ||
|
|
7c5c88b17a | ||
|
|
351cc940ce | ||
|
|
9c2147aced | ||
|
|
cb8f6f1a39 | ||
|
|
a92639e391 | ||
|
|
dafedddc1a | ||
|
|
9f905ce2e6 | ||
|
|
dca14ef82d | ||
|
|
ac249c75c4 | ||
|
|
41e269e873 | ||
|
|
b7a83dcbcd | ||
|
|
fd993feece | ||
|
|
773be6f8ec | ||
|
|
77e463990a | ||
|
|
8f6ff4bc41 | ||
|
|
ab56374b2f | ||
|
|
fa305d76e8 | ||
|
|
1de3ff1a6d | ||
|
|
106319406b | ||
|
|
a5e9cd94e1 | ||
|
|
27ac262a26 | ||
|
|
ae4c8d818f | ||
|
|
c031cb19d2 | ||
|
|
ebae547a60 | ||
|
|
cc47ccaa2c | ||
|
|
d53097f77d | ||
|
|
9911c5dc12 | ||
|
|
19aef3e94b | ||
|
|
23230e0143 | ||
|
|
f92cea144c | ||
|
|
e6cf8c39b7 | ||
|
|
c5c0845151 | ||
|
|
5d9fb55370 | ||
|
|
721a2f5052 | ||
|
|
7320957405 | ||
|
|
e4d072c79c | ||
|
|
71ee10c889 | ||
|
|
978a8a4cb2 | ||
|
|
cd06b2b337 | ||
|
|
6eb06377cc | ||
|
|
94892e7168 | ||
|
|
fe744abe26 | ||
|
|
a902b49bc5 | ||
|
|
9c07b6dd46 | ||
|
|
3b7b06dbec | ||
|
|
597a4164e8 | ||
|
|
678c00f254 | ||
|
|
7298002da7 | ||
|
|
96b781b7fd | ||
|
|
1ce5cbe275 | ||
|
|
c607f09be0 | ||
|
|
33a6ad9f7e | ||
|
|
e662336eaf | ||
|
|
0158a55845 | ||
|
|
657843960c | ||
|
|
fc6a22b072 | ||
|
|
227d99934e | ||
|
|
5036b28e45 | ||
|
|
8b5735c915 | ||
|
|
eb379e1849 | ||
|
|
6a630bc489 | ||
|
|
0d1b474e60 | ||
|
|
84f6cadae7 | ||
|
|
372e740c38 | ||
|
|
57dd979a14 | ||
|
|
c1d07a5471 | ||
|
|
98eddd0532 | ||
|
|
1b21e0d429 | ||
|
|
7776b74126 | ||
|
|
3abb887445 | ||
|
|
a9ec3ae098 | ||
|
|
53927b5a72 | ||
|
|
d67b91d2b0 | ||
|
|
a4fc28abc0 | ||
|
|
bee2a14620 | ||
|
|
168a4dc323 | ||
|
|
47c0decf1e | ||
|
|
8acc818f68 | ||
|
|
9e0468cd73 | ||
|
|
65e9cee019 | ||
|
|
bb3851a35c | ||
|
|
4368bdb2bc | ||
|
|
1b753720c1 | ||
|
|
39e27d2a23 | ||
|
|
45e630fc60 | ||
|
|
0c35a77d4b | ||
|
|
07ea9694a3 | ||
|
|
85043e7d88 | ||
|
|
d3c1fce761 | ||
|
|
33e98bf61a | ||
|
|
cae7cb7002 | ||
|
|
b8047f0912 | ||
|
|
de98e434d6 | ||
|
|
74e5e2163a | ||
|
|
9464d4d647 | ||
|
|
b7a7e5a0d3 | ||
|
|
b9c62b3fed | ||
|
|
d10525a714 | ||
|
|
390cd756e8 | ||
|
|
ee3f734b7b | ||
|
|
905ca94f71 | ||
|
|
53d42b15b8 | ||
|
|
17cb788193 | ||
|
|
f2115b15f7 | ||
|
|
0805f362d3 | ||
|
|
eeb0113dc5 | ||
|
|
648c4caca1 | ||
|
|
2eeec22868 | ||
|
|
ef50edb9e2 | ||
|
|
3ed80fa867 | ||
|
|
cf664ca2a0 | ||
|
|
485c561b18 | ||
|
|
766698cb3a | ||
|
|
082793290a | ||
|
|
12916ceca6 | ||
|
|
3c952e6a4a | ||
|
|
59184122f7 | ||
|
|
381423b1ae | ||
|
|
b7c9f779ad | ||
|
|
904d76420d | ||
|
|
fa907840c7 | ||
|
|
3760f206e8 | ||
|
|
a861257f73 | ||
|
|
143299f138 | ||
|
|
232369cd5c | ||
|
|
4b40c61201 | ||
|
|
834c219b9b | ||
|
|
d0cd1c8887 | ||
|
|
682a2aea1c | ||
|
|
1dc66db516 | ||
|
|
bc04d81a5a | ||
|
|
d2c5c2f9a3 | ||
|
|
381fda270a | ||
|
|
b3c9d1f1a5 | ||
|
|
fd4376d062 | ||
|
|
a8f053921b | ||
|
|
8e15ada164 | ||
|
|
51093fd543 | ||
|
|
48e638cc56 | ||
|
|
e6dfc159ed | ||
|
|
900c2de89a | ||
|
|
29106ebc4b | ||
|
|
a579684b34 | ||
|
|
c29a9ad062 | ||
|
|
0ac015ce7a | ||
|
|
19474e0074 | ||
|
|
ce8190dacf | ||
|
|
aacf326ca1 | ||
|
|
e39c14460b | ||
|
|
5f6e17f307 | ||
|
|
4bf23adcf5 | ||
|
|
1f4d860d9d | ||
|
|
fdbb3bf4b1 | ||
|
|
c899cc825d | ||
|
|
eb972684b3 | ||
|
|
896473f03e | ||
|
|
8c43edbd50 | ||
|
|
4c4eb4bd90 | ||
|
|
88b8597e4d | ||
|
|
76fe2f9bb8 | ||
|
|
818424259f | ||
|
|
e67f8824d9 | ||
|
|
8f1a1e0905 | ||
|
|
1b6360d9d3 | ||
|
|
8b7f10fe61 | ||
|
|
4958b3e7b6 | ||
|
|
e467d15f2c | ||
|
|
d1e7e75638 | ||
|
|
4aab63b7df | ||
|
|
acf24649b0 | ||
|
|
316cf35795 | ||
|
|
52a5a59ddb | ||
|
|
22db201a08 | ||
|
|
ea7898d113 | ||
|
|
e7e14f01e4 | ||
|
|
7d6085cefd | ||
|
|
10076c6a3e | ||
|
|
1c44445088 | ||
|
|
d305c5fd0f | ||
|
|
1577288843 | ||
|
|
63b3686746 | ||
|
|
810f950e16 | ||
|
|
dd09d560d5 | ||
|
|
c60c523851 | ||
|
|
22c2235d90 | ||
|
|
35c26367da | ||
|
|
aaf4fee9a6 | ||
|
|
3c21f62485 | ||
|
|
50b2ca014e | ||
|
|
d731c972ad | ||
|
|
5845881b08 | ||
|
|
64138ef220 | ||
|
|
046ce68a45 | ||
|
|
e34ab5957f | ||
|
|
75a5492840 | ||
|
|
56fbbe92b4 | ||
|
|
dc4e13b300 | ||
|
|
2c7f93978e | ||
|
|
6e30064421 | ||
|
|
68062216e4 | ||
|
|
8a8f325f64 | ||
|
|
cf90b898f4 | ||
|
|
1f4cb0e1ca | ||
|
|
c6ecf80dff | ||
|
|
c29b77c214 | ||
|
|
c301845ade | ||
|
|
78fab7897d | ||
|
|
f48e014694 | ||
|
|
206433db32 | ||
|
|
1ddb73ea97 | ||
|
|
b5071e61d7 | ||
|
|
a80eff9aa3 | ||
|
|
a59fb90785 | ||
|
|
29478880ff | ||
|
|
c9f8cdb51b | ||
|
|
a2ebf92932 | ||
|
|
00eb5b152a | ||
|
|
0b2f539ad0 | ||
|
|
1ab3966b75 | ||
|
|
552d967c81 | ||
|
|
e5c8a68a76 | ||
|
|
15f50635e7 | ||
|
|
cf0d6dd7c6 | ||
|
|
fd1813949a | ||
|
|
6c480098f7 | ||
|
|
ae649a5b3f | ||
|
|
34675da6fa | ||
|
|
075b0292b5 | ||
|
|
0e9825f298 | ||
|
|
bf883794a4 | ||
|
|
3adaa2d602 | ||
|
|
60a312ace5 | ||
|
|
8c9a351c84 | ||
|
|
d28f512de4 | ||
|
|
376de685fb | ||
|
|
890515edfd | ||
|
|
79412ba2c6 | ||
|
|
03ebb6947b | ||
|
|
c53591f049 | ||
|
|
db9a32a4c0 | ||
|
|
2ae9bbb24e | ||
|
|
8dfe193461 | ||
|
|
27239ae14a | ||
|
|
80ff5e2d0a | ||
|
|
38240a68aa | ||
|
|
89710b406e | ||
|
|
f002870c6a | ||
|
|
b1a68759cf | ||
|
|
bf49a17efa | ||
|
|
6b0d1d77d3 | ||
|
|
db33b7d1dc | ||
|
|
76711d95ff | ||
|
|
17faa6c3b2 | ||
|
|
c1ac354c9b | ||
|
|
449503bb94 | ||
|
|
aad18e1ca4 | ||
|
|
fdc1123b18 | ||
|
|
0974b1e705 | ||
|
|
bc6e3e598f | ||
|
|
683f9d9a28 | ||
|
|
f9bf0883c6 | ||
|
|
ad8ba299c9 | ||
|
|
3ae64822e1 | ||
|
|
a49aaee9cf | ||
|
|
2d1f70eb79 | ||
|
|
4fc5f765de | ||
|
|
7efc2726d9 | ||
|
|
29a778697c | ||
|
|
90649e72bb | ||
|
|
1904ec7df4 | ||
|
|
96add30331 | ||
|
|
9c7148e2ad | ||
|
|
31c709be5c | ||
|
|
e750ee6d28 | ||
|
|
5b3f9ac1cd | ||
|
|
6c8dacfa0d | ||
|
|
feead30b0b | ||
|
|
4244945e7b | ||
|
|
17725e4dd0 | ||
|
|
d55bb63aa0 | ||
|
|
6e22695b85 | ||
|
|
146e46d79f | ||
|
|
0af27a2387 | ||
|
|
7380f0e7ce | ||
|
|
2c5a0abcce | ||
|
|
f23e33c1a9 | ||
|
|
5588d6e344 | ||
|
|
59ace66c6f | ||
|
|
decef1ad52 | ||
|
|
0e19a4196b | ||
|
|
04dd65687b | ||
|
|
5f503b1a57 | ||
|
|
5f6e974531 | ||
|
|
a023eabf59 | ||
|
|
cb1e25a487 | ||
|
|
bcf7c6c45a | ||
|
|
b411b6ca21 | ||
|
|
cd217a3f86 | ||
|
|
6c79642450 | ||
|
|
201d476720 | ||
|
|
c1a2e67c9e | ||
|
|
1948bef9ac | ||
|
|
5486f39f7e | ||
|
|
f018b0013c | ||
|
|
950f085e80 | ||
|
|
268e26625b | ||
|
|
7041d86e4d | ||
|
|
78398ee800 | ||
|
|
7b09b02737 | ||
|
|
af7631d9f1 | ||
|
|
e0f33e62af | ||
|
|
5d224f2e04 | ||
|
|
4cb981d81b | ||
|
|
f891cfbf47 | ||
|
|
e54c8a857d | ||
|
|
66229b0d6b | ||
|
|
3eb1d49577 | ||
|
|
677888bcde | ||
|
|
abbb4180ea | ||
|
|
d1cbc56d67 | ||
|
|
b579ff832e | ||
|
|
39d0748a5b | ||
|
|
b4d20689b7 | ||
|
|
3b84b0fc47 | ||
|
|
23ea829510 | ||
|
|
ebe9daea00 | ||
|
|
36b6c0cb9c | ||
|
|
16c36a78f0 | ||
|
|
4999f7ed23 | ||
|
|
e8f4ac632f | ||
|
|
3c7854ccdf | ||
|
|
1e70223ed7 | ||
|
|
f34cd3ea61 | ||
|
|
68b8d721a7 | ||
|
|
d876419b27 | ||
|
|
8455186e9f | ||
|
|
b6584ec68b | ||
|
|
29f18c7f18 | ||
|
|
11b27f9805 | ||
|
|
653e0335c0 | ||
|
|
013dab7da6 | ||
|
|
2ba81830f3 | ||
|
|
dfba4770bd | ||
|
|
63e702ca12 | ||
|
|
394fece846 | ||
|
|
24fbab94c3 | ||
|
|
61e03fa33a | ||
|
|
6c4b92f7f6 | ||
|
|
f023325b0e | ||
|
|
dd05482cb0 | ||
|
|
d94108bf3f | ||
|
|
ab872beb1d | ||
|
|
d02cfff4cc | ||
|
|
736f269c49 | ||
|
|
b067acfd4a | ||
|
|
232fa6fd18 | ||
|
|
483d55df91 | ||
|
|
3e54d3654b | ||
|
|
b6831d464e | ||
|
|
8c67ea56e7 | ||
|
|
54bc4c23df | ||
|
|
58f47eb02c | ||
|
|
4fc62ed9ae | ||
|
|
4a89bab23a | ||
|
|
7438f3b157 | ||
|
|
9551dbc25f | ||
|
|
2e54f4a67c | ||
|
|
6d9b2a6b33 | ||
|
|
35a9acf099 | ||
|
|
b9694a0818 | ||
|
|
7416bbb25e | ||
|
|
b94c89ebf1 | ||
|
|
78ce8a4652 | ||
|
|
04c874fe35 | ||
|
|
eabe548520 | ||
|
|
f96b54e57f | ||
|
|
d3bf828524 | ||
|
|
c20415f3ba | ||
|
|
6b2ed4fe79 | ||
|
|
eca9ca90f1 | ||
|
|
9018ec9678 | ||
|
|
e55fe33f1b | ||
|
|
3b0788c3be | ||
|
|
626e67b859 | ||
|
|
28db18ebbd | ||
|
|
b28721e10b | ||
|
|
f77db4d814 | ||
|
|
62d8ec7edb | ||
|
|
7b09fa4a03 | ||
|
|
980013abae | ||
|
|
5c729ccea5 | ||
|
|
ae52ca87aa | ||
|
|
415bb23c37 | ||
|
|
9e173fca3c | ||
|
|
bd918ee506 | ||
|
|
7c9884b853 | ||
|
|
7ffa669c5c | ||
|
|
0bc20873f6 | ||
|
|
88de3359a5 | ||
|
|
a3d008da06 | ||
|
|
cbae95422d | ||
|
|
081c845c56 | ||
|
|
1e54d6deb3 | ||
|
|
bb5c6a416a | ||
|
|
6a07251007 | ||
|
|
759a66dd21 | ||
|
|
8189dd932c | ||
|
|
b6ba0f343e | ||
|
|
86ca90aa15 | ||
|
|
26e8877cd9 | ||
|
|
1b63adfb2e | ||
|
|
e27274a5a8 | ||
|
|
e5c198f839 | ||
|
|
72ba429159 | ||
|
|
3227cf7ff3 | ||
|
|
f0740120a8 | ||
|
|
3ea54065b1 | ||
|
|
6f19546c3c | ||
|
|
5c9ab21617 |
@@ -2,7 +2,7 @@
|
||||
# It has the dependencies already installed so that codespace will boot up fast
|
||||
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||
|
||||
# Do the set up required for chatwoot app
|
||||
# Do the set up required for chatwoot app
|
||||
WORKDIR /workspace
|
||||
COPY . /workspace
|
||||
RUN yarn && gem install bundler && bundle install
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
|
||||
ARG VARIANT=ubuntu-20.04
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}
|
||||
ARG VARIANT
|
||||
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
|
||||
|
||||
ARG NODE_VERSION
|
||||
ARG RUBY_VERSION
|
||||
ARG USER_UID
|
||||
ARG USER_GID
|
||||
|
||||
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
||||
groupmod --gid $USER_GID vscode \
|
||||
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
||||
@@ -27,10 +31,10 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
tmux \
|
||||
zsh \
|
||||
git-flow \
|
||||
npm
|
||||
npm \
|
||||
libyaml-dev
|
||||
|
||||
# Install rbenv and ruby
|
||||
ARG RUBY_VERSION="3.2.2"
|
||||
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
|
||||
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
|
||||
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
||||
@@ -46,7 +50,7 @@ RUN rbenv install $RUBY_VERSION && \
|
||||
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
|
||||
&& gunzip overmind.gz \
|
||||
&& sudo mv overmind /usr/local/bin \
|
||||
&& chmod +x /usr/local/bin/overmind
|
||||
&& chmod +x /usr/local/bin/overmind
|
||||
|
||||
|
||||
# Install gh
|
||||
@@ -56,7 +60,7 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | s
|
||||
&& sudo apt install gh
|
||||
|
||||
|
||||
# Do the set up required for chatwoot app
|
||||
# Do the set up required for chatwoot app
|
||||
WORKDIR /workspace
|
||||
COPY . /workspace
|
||||
|
||||
@@ -65,8 +69,7 @@ COPY Gemfile Gemfile.lock ./
|
||||
RUN gem install bundler && bundle install
|
||||
|
||||
# set up node js
|
||||
RUN npm install npm@latest -g && \
|
||||
npm install n -g && \
|
||||
n latest
|
||||
RUN npm install n -g && \
|
||||
n $NODE_VERSION
|
||||
RUN npm install --global yarn
|
||||
RUN yarn
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/python-3-postgres/.devcontainer/docker-compose.yml
|
||||
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/ruby-rails/.devcontainer/devcontainer.json
|
||||
#
|
||||
#
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
base:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile.base
|
||||
args:
|
||||
VARIANT: "ubuntu-22.04"
|
||||
NODE_VERSION: "20.9.0"
|
||||
RUBY_VERSION: "3.2.2"
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: "1000"
|
||||
USER_GID: "1000"
|
||||
image: base:latest
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
args:
|
||||
# Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
|
||||
VARIANT: 3
|
||||
# [Choice] Install Node.js
|
||||
INSTALL_NODE: "true"
|
||||
NODE_VERSION: "lts/*"
|
||||
VARIANT: "ubuntu-22.04"
|
||||
NODE_VERSION: "20.9.0"
|
||||
RUBY_VERSION: "3.2.2"
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: 1000
|
||||
USER_GID: 1000
|
||||
USER_UID: "1000"
|
||||
USER_GID: "1000"
|
||||
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:db
|
||||
|
||||
@@ -49,8 +60,7 @@ services:
|
||||
restart: unless-stopped
|
||||
image: mailhog/mailhog
|
||||
network_mode: service:db
|
||||
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
|
||||
+20
-2
@@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
# Mail Incoming
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
# Set this to appropriate ingress channel with regards to incoming emails
|
||||
# Set this to the appropriate ingress channel with regards to incoming emails
|
||||
# Possible values are :
|
||||
# relay for Exim, Postfix, Qmail
|
||||
# mailgun for Mailgun
|
||||
@@ -91,10 +91,20 @@ MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
RAILS_INBOUND_EMAIL_SERVICE=
|
||||
# Use one of the following based on the email ingress service
|
||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||
# Set this to a password of your choice and use it in the Inbound webhook
|
||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||
|
||||
MAILGUN_INGRESS_SIGNING_KEY=
|
||||
MANDRILL_INGRESS_API_KEY=
|
||||
|
||||
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
|
||||
# Inbound webhook URL format:
|
||||
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
|
||||
# Note: Replace the values inside the brackets; do not include the brackets themselves.
|
||||
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
|
||||
# For Postmark
|
||||
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
|
||||
|
||||
# Storage
|
||||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
@@ -176,6 +186,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
# MICROSOFT CLARITY
|
||||
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||
|
||||
# GOOGLE_TAG_MANAGER
|
||||
# GOOGLE_TAG = GTM-XXXXXXX
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
@@ -199,7 +212,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=true
|
||||
# RACK_ATTACK_IP_LIMIT=3000
|
||||
# RACK_ATTACK_LIMIT=300
|
||||
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
@@ -240,3 +253,8 @@ AZURE_APP_SECRET=
|
||||
|
||||
# Sentiment analysis model file path
|
||||
SENTIMENT_FILE_PATH=
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
'prettier',
|
||||
'plugin:vue/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'plugin:cypress/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
## All javascript files should be reviewed by pranav before merging
|
||||
*.js @pranavrajs
|
||||
*.vue @pranavrajs
|
||||
|
||||
|
||||
## All enterprise related files should be reviewed by sojan before merging
|
||||
/enterprise/* @sojan-official
|
||||
@@ -0,0 +1,45 @@
|
||||
## github action to check deployment success
|
||||
## curl the deployment url and check for 200 status
|
||||
## deployment url will be of the form chatwoot-pr-<pr_number>.herokuapp.com
|
||||
name: Deploy Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
deployment_check:
|
||||
name: Check Deployment
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install jq
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Print Deployment URL
|
||||
run: echo "https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com"
|
||||
- name: Check Deployment Status
|
||||
run: |
|
||||
max_attempts=10
|
||||
attempt=1
|
||||
status_code=0
|
||||
echo "Waiting for review app to be deployed/redeployed, trying in 10 minutes..."
|
||||
sleep 600
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
response=$(curl -s -o /dev/null -w "%{http_code}" https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
|
||||
status_code=$(echo $response | head -n 1)
|
||||
if [ $status_code -eq 200 ]; then
|
||||
body=$(curl -s https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
|
||||
if echo "$body" | jq -e '.version and .timestamp and .queue_services == "ok" and .data_services == "ok"' > /dev/null; then
|
||||
echo "Deployment successful"
|
||||
exit 0
|
||||
else
|
||||
echo "Deployment status unknown, retrying in 3 minutes..."
|
||||
sleep 180
|
||||
fi
|
||||
else
|
||||
echo "Waiting for review app to be ready, retrying in 3 minutes..."
|
||||
sleep 180
|
||||
attempt=$((attempt + 1))
|
||||
fi
|
||||
done
|
||||
echo "Deployment failed after $max_attempts attempts"
|
||||
exit 1
|
||||
fi
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for log lines and calculate percentage
|
||||
run: |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# # This action will try to install and setup
|
||||
# # chatwoot on an Ubuntu 20.04 machine using
|
||||
# # the linux installer script.
|
||||
# #
|
||||
# #
|
||||
# # This is set to run daily at midnight.
|
||||
# #
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
sudo ./install.sh --install < input
|
||||
|
||||
# disabling http verify for now as http
|
||||
# disabling http verify for now as http
|
||||
# access to port 3000 fails in gh action env
|
||||
# - name: Verify
|
||||
# if: always()
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
# curl http://localhost:3000/api
|
||||
|
||||
- name: Upload chatwoot setup log file as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: chatwoot-setup-log-file
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
@@ -19,5 +19,6 @@ jobs:
|
||||
|
||||
- name: Build the Codespace Base Image
|
||||
run: |
|
||||
docker build . -t ghcr.io/chatwoot/chatwoot_codespace:latest -f .devcontainer/Dockerfile.base
|
||||
docker-compose -f .devcontainer/docker-compose.yml build base
|
||||
docker tag base:latest ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||
docker push ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
@@ -76,11 +76,11 @@ jobs:
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
env:
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
@@ -73,11 +73,12 @@ jobs:
|
||||
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
|
||||
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
|
||||
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
|
||||
spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \
|
||||
--profile=10 \
|
||||
--format documentation
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
|
||||
- name: Run asset compile
|
||||
run: bundle exec rake assets:precompile
|
||||
env:
|
||||
|
||||
+23
-70
@@ -2,7 +2,8 @@ require:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
inherit_from: .rubocop_todo.yml
|
||||
- ./rubocop/use_from_email.rb
|
||||
- ./rubocop/custom_cop_location.rb
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 150
|
||||
@@ -12,7 +13,8 @@ Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 19
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
@@ -50,6 +52,7 @@ Lint/OrAssignmentToConstant:
|
||||
Exclude:
|
||||
- 'lib/redis/config.rb'
|
||||
Metrics/BlockLength:
|
||||
Max: 30
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
- '**/routes.rb'
|
||||
@@ -102,84 +105,31 @@ RSpec/FactoryBot/SyntaxMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Metrics/MethodLength:
|
||||
Naming/MemoizedInstanceVariableName:
|
||||
Exclude:
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Exclude:
|
||||
- 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
|
||||
- 'app/models/message.rb'
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
- 'app/builders/account_builder.rb'
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Metrics/AbcSize:
|
||||
Max: 26
|
||||
Exclude:
|
||||
- 'app/controllers/concerns/auth_helper.rb'
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
|
||||
- 'app/controllers/api/v1/accounts/inboxes_controller.rb'
|
||||
- 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
Exclude:
|
||||
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
|
||||
Rails/ReversibleMigration:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
- 'db/migrate/20161025070645_remchannel.rb'
|
||||
- 'db/migrate/20161025070645_remchannel.rb'
|
||||
- 'db/migrate/20161110102609_removeinboxid.rb'
|
||||
- 'db/migrate/20170519091539_add_avatar_to_fb.rb'
|
||||
- 'db/migrate/20191020085608_rename_old_tables.rb'
|
||||
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
|
||||
Rails/BulkChangeTable:
|
||||
Exclude:
|
||||
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
|
||||
- 'db/migrate/20200121190901_create_account_users.rb'
|
||||
- 'db/migrate/20170211092540_notnullableusers.rb'
|
||||
- 'db/migrate/20170403095203_contactadder.rb'
|
||||
- 'db/migrate/20170406104018_add_default_status_conv.rb'
|
||||
- 'db/migrate/20170511134418_latlong.rb'
|
||||
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
|
||||
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
|
||||
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
- 'app/models/contact.rb'
|
||||
- 'app/models/integrations/hook.rb'
|
||||
- 'app/models/canned_response.rb'
|
||||
- 'app/models/telegram_bot.rb'
|
||||
Rails/RenderInline:
|
||||
Exclude:
|
||||
- 'app/controllers/swagger_controller.rb'
|
||||
Performance/CollectionLiteralInLoop:
|
||||
Exclude:
|
||||
- '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
|
||||
@@ -187,9 +137,21 @@ RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
# we should bring this down
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 14
|
||||
|
||||
# custom rules
|
||||
UseFromEmail:
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- 'app/models/user.rb'
|
||||
- 'app/models/contact.rb'
|
||||
|
||||
CustomCopLocation:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
@@ -203,13 +165,4 @@ AllCops:
|
||||
- 'config/environments/**/*'
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- 'db/migrate/20200225162150_init_schema.rb'
|
||||
- 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.rb'
|
||||
- 'db/migrate/20210611180221_add_service_name_to_active_storage_blobs.active_storage.rb'
|
||||
- db/migrate/20200309213132_add_account_id_to_agent_bot_inboxes.rb
|
||||
- db/migrate/20200331095710_add_identifier_to_contact.rb
|
||||
- db/migrate/20200429082655_add_medium_to_twilio_sms.rb
|
||||
- db/migrate/20200503151130_add_account_feature_flag.rb
|
||||
- db/migrate/20200927135222_add_last_activity_at_to_conversation.rb
|
||||
- db/migrate/20210306170117_add_last_activity_at_to_contacts.rb
|
||||
- db/migrate/20220809104508_revert_cascading_indexes.rb
|
||||
- 'db/migrate/20230426130150_init_schema.rb'
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2019-10-23 16:47:02 +0530 using RuboCop version 0.73.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 1
|
||||
Lint/DuplicateMethods:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/reports_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Lint/RescueException:
|
||||
Exclude:
|
||||
- 'app/builders/messages/message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/reports_controller.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Configuration parameters: AllowKeywordBlockArguments.
|
||||
Lint/UnderscorePrefixedVariableName:
|
||||
Exclude:
|
||||
- 'app/models/account.rb'
|
||||
- 'deploy/before_symlink.rb'
|
||||
|
||||
# Offense count: 18
|
||||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/facebook_indicators_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
- 'app/listeners/reporting_listener.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/facebook_page.rb'
|
||||
|
||||
# Offense count: 14
|
||||
Metrics/AbcSize:
|
||||
Max: 26
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
# ExcludedMethods: refine
|
||||
Metrics/BlockLength:
|
||||
Max: 30
|
||||
|
||||
# Offense count: 2
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 7
|
||||
|
||||
# Offense count: 10
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
Metrics/MethodLength:
|
||||
Max: 19
|
||||
|
||||
# Offense count: 1
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 8
|
||||
|
||||
# Offense count: 6
|
||||
Naming/AccessorMethodName:
|
||||
Exclude:
|
||||
- 'app/builders/report_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts_controller.rb'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
|
||||
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
|
||||
Naming/MemoizedInstanceVariableName:
|
||||
Exclude:
|
||||
- 'app/controllers/api/base_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations_controller.rb'
|
||||
- 'app/controllers/api/v1/webhooks_controller.rb'
|
||||
- 'app/controllers/application_controller.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'lib/integrations/widget/outgoing_message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: MaxKeyValuePairs.
|
||||
Performance/RedundantMerge:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/models/message.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Performance/StringReplacement:
|
||||
Exclude:
|
||||
- 'lib/events/base.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Configuration parameters: Prefixes.
|
||||
# Prefixes: when, with, without
|
||||
RSpec/ContextWording:
|
||||
Exclude:
|
||||
- 'spec/models/contact_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
RSpec/DescribeClass:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
RSpec/DescribeSymbol:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowConsecutiveOneLiners.
|
||||
RSpec/EmptyLineAfterExample:
|
||||
Exclude:
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Max.
|
||||
RSpec/ExampleLength:
|
||||
Exclude:
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: single_line_only, single_statement_only, disallow
|
||||
RSpec/ImplicitSubject:
|
||||
Exclude:
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Configuration parameters: AggregateFailuresByDefault.
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: not_to, to_not
|
||||
RSpec/NotToNot:
|
||||
Exclude:
|
||||
- 'spec/mailers/confirmation_instructions_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
||||
RSpec/VerifiedDoubles:
|
||||
Exclude:
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
Rails/ActiveRecordAliases:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/agents_controller.rb'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/canned_responses_controller.rb'
|
||||
- 'app/controllers/api/v1/contacts_controller.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
Rails/BelongsTo:
|
||||
Exclude:
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/EnumHash:
|
||||
Exclude:
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/HasManyOrHasOneDependent:
|
||||
Exclude:
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/InverseOf:
|
||||
Exclude:
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/controllers/**/*.rb
|
||||
Rails/LexicallyScopedActionFilter:
|
||||
Exclude:
|
||||
- 'app/controllers/home_controller.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb
|
||||
Rails/Output:
|
||||
Exclude:
|
||||
- 'app/bot/bot.rb'
|
||||
- 'app/builders/account_builder.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: strict, flexible
|
||||
Rails/TimeZone:
|
||||
Exclude:
|
||||
- 'app/builders/report_builder.rb'
|
||||
- 'lib/reports/update_account_identity.rb'
|
||||
- 'lib/reports/update_agent_identity.rb'
|
||||
- 'lib/reports/update_identity.rb'
|
||||
- 'spec/models/conversation_spec.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/Validation:
|
||||
Exclude:
|
||||
- 'app/models/canned_response.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/facebook_page.rb'
|
||||
- 'app/models/telegram_bot.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
# Offense count: 15
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
||||
# SupportedStyles: nested, compact
|
||||
Style/ClassAndModuleChildren:
|
||||
Exclude:
|
||||
- 'app/builders/messages/message_builder.rb'
|
||||
- 'app/controllers/api/v1/inbox_members_controller.rb'
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
- 'app/models/channel/web_widget.rb'
|
||||
- 'app/presenters/conversations/event_data_presenter.rb'
|
||||
- 'app/services/facebook/send_reply_service.rb'
|
||||
- 'lib/integrations/facebook/delivery_status.rb'
|
||||
- 'lib/integrations/facebook/message_creator.rb'
|
||||
- 'lib/integrations/facebook/message_parser.rb'
|
||||
- 'lib/integrations/widget/incoming_message_builder.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Style/CommentedKeyword:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations/assignments_controller.rb'
|
||||
- 'app/controllers/api/v1/conversations/labels_controller.rb'
|
||||
- 'app/controllers/api/v1/labels_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowIfModifier.
|
||||
Style/IfInsideElse:
|
||||
Exclude:
|
||||
- 'app/finders/conversation_finder.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Style/MixinUsage:
|
||||
Exclude:
|
||||
- 'app/bot/bot.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
|
||||
# SupportedStyles: predicate, comparison
|
||||
Style/NumericPredicate:
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'app/controllers/api/v1/callbacks_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: implicit, explicit
|
||||
Style/RescueStandardError:
|
||||
Exclude:
|
||||
- 'app/models/channel/facebook_page.rb'
|
||||
@@ -3,8 +3,8 @@ source 'https://rubygems.org'
|
||||
ruby '3.2.2'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
gem 'rails', '~> 7.0.8.0'
|
||||
gem 'rack-cors', '2.0.0', require: 'rack/cors'
|
||||
gem 'rails', '~> 7.0.8.1'
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', require: false
|
||||
|
||||
@@ -15,7 +15,7 @@ gem 'browser'
|
||||
gem 'hashie'
|
||||
gem 'jbuilder'
|
||||
gem 'kaminari'
|
||||
gem 'responders'
|
||||
gem 'responders', '>= 3.1.1'
|
||||
gem 'rest-client'
|
||||
gem 'telephone_number'
|
||||
gem 'time_diff'
|
||||
@@ -39,6 +39,8 @@ gem 'rack-attack', '>= 6.7.0'
|
||||
gem 'down'
|
||||
# authentication type to fetch and send mail over oauth2.0
|
||||
gem 'gmail_xoauth'
|
||||
# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2
|
||||
gem 'net-smtp', '~> 0.3.4'
|
||||
# Prevent CSV injection
|
||||
gem 'csv-safe'
|
||||
|
||||
@@ -67,16 +69,16 @@ gem 'webpacker'
|
||||
gem 'barnes'
|
||||
|
||||
##--- gems for authentication & authorization ---##
|
||||
gem 'devise'
|
||||
gem 'devise', '>= 4.9.3'
|
||||
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||
gem 'devise_token_auth'
|
||||
# authorization
|
||||
gem 'jwt'
|
||||
gem 'pundit'
|
||||
# super admin
|
||||
gem 'administrate', '>= 0.19.0'
|
||||
gem 'administrate-field-active_storage'
|
||||
gem 'administrate-field-belongs_to_search'
|
||||
gem 'administrate', '>= 0.20.1'
|
||||
gem 'administrate-field-active_storage', '>= 1.0.1'
|
||||
gem 'administrate-field-belongs_to_search', '>= 0.9.0'
|
||||
|
||||
##--- gems for pubsub service ---##
|
||||
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||
@@ -92,7 +94,7 @@ gem 'twitty', '~> 0.1.5'
|
||||
# facebook client
|
||||
gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client', '~> 2.0.0'
|
||||
gem 'slack-ruby-client', '~> 2.2.0'
|
||||
# for dialogflow integrations
|
||||
gem 'google-cloud-dialogflow-v2'
|
||||
gem 'grpc'
|
||||
@@ -107,16 +109,16 @@ gem 'google-cloud-translate-v3'
|
||||
gem 'ddtrace', require: false
|
||||
gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.11.0', require: false
|
||||
gem 'sentry-rails', '>= 5.14.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.11.0', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.14.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.1.3'
|
||||
gem 'sidekiq', '>= 7.2.1'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron', '>= 1.10.1'
|
||||
gem 'sidekiq-cron', '>= 1.12.0'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'fcm'
|
||||
@@ -154,15 +156,15 @@ gem 'stripe'
|
||||
gem 'faker'
|
||||
|
||||
# Include logrange conditionally in intializer using env variable
|
||||
gem 'lograge', '~> 0.13.0', require: false
|
||||
gem 'lograge', '~> 0.14.0', require: false
|
||||
|
||||
# worked with microsoft refresh token
|
||||
gem 'omniauth-oauth2'
|
||||
|
||||
gem 'audited', '~> 5.4', '>= 5.4.0'
|
||||
gem 'audited', '~> 5.4', '>= 5.4.1'
|
||||
|
||||
# need for google auth
|
||||
gem 'omniauth'
|
||||
gem 'omniauth', '>= 2.1.2'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||
|
||||
@@ -198,8 +200,10 @@ group :development do
|
||||
gem 'squasher'
|
||||
|
||||
# profiling
|
||||
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
||||
gem 'rack-mini-profiler', '>= 3.2.0', require: false
|
||||
gem 'stackprof'
|
||||
# Should install the associated chrome extension to view query logs
|
||||
gem 'meta_request'
|
||||
end
|
||||
|
||||
group :test do
|
||||
@@ -222,7 +226,7 @@ group :development, :test do
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'climate_control'
|
||||
gem 'debug', '~> 1.8'
|
||||
gem 'factory_bot_rails'
|
||||
gem 'factory_bot_rails', '>= 6.4.3'
|
||||
gem 'listen'
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
|
||||
+154
-146
@@ -33,70 +33,70 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actioncable (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionmailbox (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activejob (= 7.0.8.1)
|
||||
activerecord (= 7.0.8.1)
|
||||
activestorage (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionmailer (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
actionview (= 7.0.8.1)
|
||||
activejob (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionpack (7.0.8.1)
|
||||
actionview (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actiontext (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activerecord (= 7.0.8.1)
|
||||
activestorage (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionview (7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activejob (7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activerecord (7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activemodel (7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
activerecord (7.0.8.1)
|
||||
activemodel (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
activerecord-import (1.4.1)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activestorage (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activejob (= 7.0.8.1)
|
||||
activerecord (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.8)
|
||||
activesupport (7.0.8.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -105,30 +105,30 @@ GEM
|
||||
activerecord (>= 6.0, < 7.1)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
administrate (0.19.0)
|
||||
actionpack (>= 5.0)
|
||||
actionview (>= 5.0)
|
||||
activerecord (>= 5.0)
|
||||
jquery-rails (>= 4.0)
|
||||
kaminari (>= 1.0)
|
||||
administrate (0.20.1)
|
||||
actionpack (>= 6.0, < 8.0)
|
||||
actionview (>= 6.0, < 8.0)
|
||||
activerecord (>= 6.0, < 8.0)
|
||||
jquery-rails (~> 4.6.0)
|
||||
kaminari (~> 1.2.2)
|
||||
sassc-rails (~> 2.1)
|
||||
selectize-rails (~> 0.6)
|
||||
administrate-field-active_storage (0.4.2)
|
||||
administrate-field-active_storage (1.0.1)
|
||||
administrate (>= 0.2.2)
|
||||
rails (>= 7.0)
|
||||
administrate-field-belongs_to_search (0.8.0)
|
||||
administrate-field-belongs_to_search (0.9.0)
|
||||
administrate (>= 0.3, < 1.0)
|
||||
jbuilder (~> 2)
|
||||
rails (>= 4.2, < 7.1)
|
||||
rails (>= 4.2, < 7.2)
|
||||
selectize-rails (~> 0.6)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_extras (7.1.0)
|
||||
audited (5.4.0)
|
||||
activerecord (>= 5.0, < 7.2)
|
||||
request_store (~> 1.2)
|
||||
audited (5.4.1)
|
||||
activerecord (>= 5.0, < 7.7)
|
||||
activesupport (>= 5.0, < 7.7)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.760.0)
|
||||
aws-sdk-core (3.171.1)
|
||||
@@ -148,7 +148,8 @@ GEM
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.18)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.19)
|
||||
bindex (0.8.1)
|
||||
blingfire (0.1.8)
|
||||
bootsnap (1.16.0)
|
||||
@@ -168,13 +169,13 @@ GEM
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.10)
|
||||
concurrent-ruby (1.2.2)
|
||||
concurrent-ruby (1.2.3)
|
||||
connection_pool (2.4.1)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
csv-safe (3.2.1)
|
||||
cypress-on-rails (1.13.1)
|
||||
cypress-on-rails (1.16.0)
|
||||
rack
|
||||
database_cleaner (2.0.2)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
@@ -182,7 +183,7 @@ GEM
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.3.3)
|
||||
date (3.3.4)
|
||||
ddtrace (1.11.1)
|
||||
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
|
||||
libdatadog (~> 2.0.0.1.0)
|
||||
@@ -193,7 +194,7 @@ GEM
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
declarative (0.0.20)
|
||||
devise (4.9.2)
|
||||
devise (4.9.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
@@ -229,10 +230,10 @@ GEM
|
||||
facebook-messenger (2.0.1)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
rack (>= 1.4.5)
|
||||
factory_bot (6.2.1)
|
||||
factory_bot (6.4.5)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
factory_bot_rails (6.4.3)
|
||||
factory_bot (~> 6.4)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
@@ -255,13 +256,13 @@ GEM
|
||||
fcm (1.0.8)
|
||||
faraday (>= 1.0.0, < 3.0)
|
||||
googleauth (~> 1)
|
||||
ffi (1.15.5)
|
||||
ffi (1.16.3)
|
||||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
rake
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.2)
|
||||
fugit (1.8.1)
|
||||
fugit (1.9.0)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.18.0)
|
||||
@@ -273,10 +274,10 @@ GEM
|
||||
googleauth (~> 1.0)
|
||||
grpc (~> 1.36)
|
||||
geocoder (1.8.1)
|
||||
gli (2.21.0)
|
||||
gli (2.21.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
gmail_xoauth (0.4.2)
|
||||
gmail_xoauth (0.4.3)
|
||||
oauth (>= 0.3.6)
|
||||
google-apis-core (0.11.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
@@ -315,16 +316,16 @@ GEM
|
||||
google-cloud-translate-v3 (0.6.0)
|
||||
gapic-common (>= 0.17.1, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.22.3)
|
||||
google-protobuf (3.22.3-arm64-darwin)
|
||||
google-protobuf (3.22.3-x86_64-darwin)
|
||||
google-protobuf (3.22.3-x86_64-linux)
|
||||
google-protobuf (3.25.2)
|
||||
google-protobuf (3.25.2-arm64-darwin)
|
||||
google-protobuf (3.25.2-x86_64-darwin)
|
||||
google-protobuf (3.25.2-x86_64-linux)
|
||||
googleapis-common-protos (1.4.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.6.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (1.11.0)
|
||||
google-protobuf (~> 3.18)
|
||||
googleauth (1.5.2)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
@@ -334,13 +335,13 @@ GEM
|
||||
signet (>= 0.16, < 2.a)
|
||||
groupdate (6.2.1)
|
||||
activesupport (>= 5.2)
|
||||
grpc (1.54.0)
|
||||
grpc (1.54.3)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.54.0-x86_64-darwin)
|
||||
grpc (1.54.3-x86_64-darwin)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.54.0-x86_64-linux)
|
||||
grpc (1.54.3-x86_64-linux)
|
||||
google-protobuf (~> 3.21)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
@@ -434,12 +435,12 @@ GEM
|
||||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
lograge (0.13.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.21.3)
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -450,14 +451,17 @@ GEM
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
meta_request (0.7.4)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 7.1)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
minitest (5.20.0)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.21.2)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
@@ -468,29 +472,30 @@ GEM
|
||||
activerecord (>= 5.2)
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.3.7)
|
||||
net-imap (0.4.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.3.3)
|
||||
net-smtp (0.3.4)
|
||||
net-protocol
|
||||
netrc (0.11.0)
|
||||
newrelic-sidekiq-metrics (1.6.1)
|
||||
newrelic_rpm (~> 8)
|
||||
newrelic-sidekiq-metrics (1.6.2)
|
||||
newrelic_rpm (>= 8.0.0)
|
||||
sidekiq
|
||||
newrelic_rpm (8.16.0)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.4)
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.4-arm64-darwin)
|
||||
nokogiri (1.16.2-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.4-x86_64-darwin)
|
||||
nokogiri (1.16.2-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.4-x86_64-linux)
|
||||
nokogiri (1.16.2-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
numo-narray (0.9.2.1)
|
||||
oauth (1.1.0)
|
||||
@@ -506,7 +511,7 @@ GEM
|
||||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
omniauth (2.1.1)
|
||||
omniauth (2.1.2)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
@@ -548,40 +553,42 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (5.0.1)
|
||||
puma (6.3.1)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.1)
|
||||
rack (2.2.8)
|
||||
racc (1.7.3)
|
||||
rack (2.2.8.1)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.1)
|
||||
rack-contrib (2.4.0)
|
||||
rack (< 4)
|
||||
rack-cors (2.0.0)
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.1.1)
|
||||
rack-mini-profiler (3.2.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.0.6)
|
||||
rack
|
||||
rack-protection (3.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.6)
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rails (7.0.8)
|
||||
actioncable (= 7.0.8)
|
||||
actionmailbox (= 7.0.8)
|
||||
actionmailer (= 7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actiontext (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rails (7.0.8.1)
|
||||
actioncable (= 7.0.8.1)
|
||||
actionmailbox (= 7.0.8.1)
|
||||
actionmailer (= 7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
actiontext (= 7.0.8.1)
|
||||
actionview (= 7.0.8.1)
|
||||
activejob (= 7.0.8.1)
|
||||
activemodel (= 7.0.8.1)
|
||||
activerecord (= 7.0.8.1)
|
||||
activestorage (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.8)
|
||||
railties (= 7.0.8.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -589,21 +596,21 @@ GEM
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
railties (7.0.8.1)
|
||||
actionpack (= 7.0.8.1)
|
||||
activesupport (= 7.0.8.1)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rake (13.1.0)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redis (5.0.6)
|
||||
redis-client (>= 0.9.0)
|
||||
redis-client (0.17.0)
|
||||
redis-client (0.19.1)
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
@@ -616,7 +623,7 @@ GEM
|
||||
uber (< 0.2.0)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.0)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rest-client (2.1.0)
|
||||
@@ -702,23 +709,23 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.11.0)
|
||||
sentry-rails (5.14.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.11.0)
|
||||
sentry-ruby (5.11.0)
|
||||
sentry-ruby (~> 5.14.0)
|
||||
sentry-ruby (5.14.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.11.0)
|
||||
sentry-ruby (~> 5.11.0)
|
||||
sentry-sidekiq (5.14.0)
|
||||
sentry-ruby (~> 5.14.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.1.3)
|
||||
sidekiq (7.2.1)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.14.0)
|
||||
sidekiq-cron (1.10.1)
|
||||
redis-client (>= 0.19.0)
|
||||
sidekiq-cron (1.12.0)
|
||||
fugit (~> 1.8)
|
||||
globalid (>= 1.0.1)
|
||||
sidekiq (>= 6)
|
||||
@@ -732,13 +739,12 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (2.0.0)
|
||||
slack-ruby-client (2.2.0)
|
||||
faraday (>= 2.0)
|
||||
faraday-mashify
|
||||
faraday-multipart
|
||||
gli
|
||||
hashie
|
||||
websocket-driver
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
@@ -746,7 +752,7 @@ GEM
|
||||
spring-watcher-listen (2.1.0)
|
||||
listen (>= 2.7, < 4.0)
|
||||
spring (>= 4)
|
||||
sprockets (4.2.0)
|
||||
sprockets (4.2.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (>= 2.2.4, < 4)
|
||||
sprockets-rails (3.4.2)
|
||||
@@ -759,12 +765,12 @@ GEM
|
||||
stripe (8.5.0)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.2.2)
|
||||
tilt (2.2.0)
|
||||
thor (1.3.0)
|
||||
tilt (2.3.0)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
timeout (0.4.0)
|
||||
timeout (0.4.1)
|
||||
trailblazer-option (0.1.2)
|
||||
twilio-ruby (5.77.0)
|
||||
faraday (>= 0.9, < 3.0)
|
||||
@@ -788,7 +794,7 @@ GEM
|
||||
valid_email2 (4.0.6)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
version_gem (1.1.2)
|
||||
version_gem (1.1.3)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.1)
|
||||
@@ -817,7 +823,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.6.11)
|
||||
zeitwerk (2.6.12)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -834,12 +840,12 @@ DEPENDENCIES
|
||||
active_record_query_trace
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate (>= 0.19.0)
|
||||
administrate-field-active_storage
|
||||
administrate-field-belongs_to_search
|
||||
administrate (>= 0.20.1)
|
||||
administrate-field-active_storage (>= 1.0.1)
|
||||
administrate-field-belongs_to_search (>= 0.9.0)
|
||||
annotate
|
||||
attr_extras
|
||||
audited (~> 5.4, >= 5.4.0)
|
||||
audited (~> 5.4, >= 5.4.1)
|
||||
aws-sdk-s3
|
||||
azure-storage-blob!
|
||||
barnes
|
||||
@@ -856,7 +862,7 @@ DEPENDENCIES
|
||||
database_cleaner
|
||||
ddtrace
|
||||
debug (~> 1.8)
|
||||
devise
|
||||
devise (>= 4.9.3)
|
||||
devise-secure_password!
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
@@ -864,7 +870,7 @@ DEPENDENCIES
|
||||
elastic-apm
|
||||
email_reply_trimmer
|
||||
facebook-messenger
|
||||
factory_bot_rails
|
||||
factory_bot_rails (>= 6.4.3)
|
||||
faker
|
||||
fcm
|
||||
flag_shih_tzu
|
||||
@@ -892,13 +898,15 @@ DEPENDENCIES
|
||||
line-bot-api
|
||||
liquid
|
||||
listen
|
||||
lograge (~> 0.13.0)
|
||||
lograge (~> 0.14.0)
|
||||
maxminddb
|
||||
meta_request
|
||||
mock_redis
|
||||
neighbor
|
||||
newrelic-sidekiq-metrics
|
||||
net-smtp (~> 0.3.4)
|
||||
newrelic-sidekiq-metrics (>= 1.6.2)
|
||||
newrelic_rpm
|
||||
omniauth
|
||||
omniauth (>= 2.1.2)
|
||||
omniauth-google-oauth2
|
||||
omniauth-oauth2
|
||||
omniauth-rails_csrf_protection (~> 1.0)
|
||||
@@ -910,13 +918,13 @@ DEPENDENCIES
|
||||
puma
|
||||
pundit
|
||||
rack-attack (>= 6.7.0)
|
||||
rack-cors
|
||||
rack-mini-profiler (>= 3.1.1)
|
||||
rack-cors (= 2.0.0)
|
||||
rack-mini-profiler (>= 3.2.0)
|
||||
rack-timeout
|
||||
rails (~> 7.0.8.0)
|
||||
rails (~> 7.0.8.1)
|
||||
redis
|
||||
redis-namespace
|
||||
responders
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
rspec-rails
|
||||
@@ -928,14 +936,14 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.11.0)
|
||||
sentry-rails (>= 5.14.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.11.0)
|
||||
sentry-sidekiq (>= 5.14.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.1.3)
|
||||
sidekiq-cron (>= 1.10.1)
|
||||
sidekiq (>= 7.2.1)
|
||||
sidekiq-cron (>= 1.12.0)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client (~> 2.0.0)
|
||||
slack-ruby-client (~> 2.2.0)
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017-2021 Chatwoot Inc.
|
||||
Copyright (c) 2017-2024 Chatwoot Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||
frontend: bin/webpack-dev-server
|
||||
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<p align="center">
|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
|
||||
</p>
|
||||
___
|
||||
|
||||
<p align="center">
|
||||
# Chatwoot
|
||||
|
||||
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
<p>
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
@@ -13,9 +15,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
___
|
||||
|
||||
<p align="center">
|
||||
<p>
|
||||
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
||||
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
||||
@@ -23,15 +23,13 @@ ___
|
||||
<img src="https://img.shields.io/github/commit-activity/m/chatwoot/chatwoot" alt="Commits-per-month">
|
||||
<a title="Crowdin" target="_self" href="https://chatwoot.crowdin.com/chatwoot"><img src="https://badges.crowdin.net/e/37ced7eba411064bd792feb3b7a28b16/localized.svg"></a>
|
||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/discord/647412545203994635" alt="Discord"></a>
|
||||
<a href="https://huntr.dev/bounties/disclose"><img src="https://cdn.huntr.dev/huntr_security_badge_mono.svg" alt="Huntr"></a>
|
||||
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fuptime.json" alt="uptime"></a>
|
||||
<a href="https://status.chatwoot.com"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fchatwoot%2Fstatus%2Fmaster%2Fapi%2Fchatwoot%2Fresponse-time.json" alt="response time"></a>
|
||||
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
|
||||
</p>
|
||||
|
||||
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
||||
|
||||
@@ -119,4 +117,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
|
||||
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
|
||||
*Chatwoot* © 2017-2023, Chatwoot Inc - Released under the MIT License.
|
||||
*Chatwoot* © 2017-2024, Chatwoot Inc - Released under the MIT License.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.1.0
|
||||
3.3.1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.4.0
|
||||
2.7.0
|
||||
|
||||
@@ -72,6 +72,11 @@
|
||||
"scripts": {
|
||||
"test": "bundle exec rake test"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"scripts": {
|
||||
"postdeploy": "bundle exec rails db:seed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class ContactIdentifyAction
|
||||
def existing_email_contact
|
||||
return if params[:email].blank?
|
||||
|
||||
@existing_email_contact ||= account.contacts.find_by(email: params[:email])
|
||||
@existing_email_contact ||= account.contacts.from_email(params[:email])
|
||||
end
|
||||
|
||||
def existing_phone_number_contact
|
||||
@@ -104,7 +104,7 @@ class ContactIdentifyAction
|
||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||
@contact.discard_invalid_attrs if discard_invalid_attrs
|
||||
@contact.save!
|
||||
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? && !@contact.avatar.attached?
|
||||
end
|
||||
|
||||
def merge_contact(base_contact, merge_contact)
|
||||
|
||||
@@ -12,6 +12,7 @@ class ContactMergeAction
|
||||
merge_conversations
|
||||
merge_messages
|
||||
merge_contact_inboxes
|
||||
merge_contact_notes
|
||||
merge_and_remove_mergee_contact
|
||||
end
|
||||
@base_contact
|
||||
@@ -33,6 +34,10 @@ class ContactMergeAction
|
||||
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
|
||||
end
|
||||
|
||||
def merge_contact_notes
|
||||
Note.where(contact_id: @mergee_contact.id, account_id: @mergee_contact.account_id).update(contact_id: @base_contact.id)
|
||||
end
|
||||
|
||||
def merge_messages
|
||||
Message.where(sender: @mergee_contact).update(sender: @base_contact)
|
||||
end
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
@import 'components/flashes';
|
||||
@import 'components/form-actions';
|
||||
@import 'components/main-content';
|
||||
@import 'components/navigation';
|
||||
@import 'components/pagination';
|
||||
@import 'components/search';
|
||||
@import 'components/reports';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
html {
|
||||
background-color: $color-white;
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
.attribute-data {
|
||||
float: left;
|
||||
margin-bottom: $base-spacing;
|
||||
margin-left: 2rem;
|
||||
width: calc(84% - 1rem);
|
||||
margin-left: 1.25rem;
|
||||
width: calc(84% - 0.625rem);
|
||||
}
|
||||
|
||||
.attribute--nested {
|
||||
|
||||
@@ -9,22 +9,22 @@
|
||||
|
||||
.field-unit__label {
|
||||
float: left;
|
||||
margin-left: 1rem;
|
||||
margin-left: 0.625rem;
|
||||
text-align: right;
|
||||
width: calc(15% - 1rem);
|
||||
width: calc(15% - 0.625rem);
|
||||
}
|
||||
|
||||
.field-unit__field {
|
||||
float: left;
|
||||
margin-left: 2rem;
|
||||
max-width: 50rem;
|
||||
margin-left: 1.25rem;
|
||||
max-width: 31.15rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-unit--nested {
|
||||
border: $base-border;
|
||||
margin-left: 7.5%;
|
||||
max-width: 60rem;
|
||||
max-width: 37.5rem;
|
||||
padding: $small-spacing;
|
||||
width: 100%;
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.form-actions {
|
||||
margin-left: calc(15% + 2rem);
|
||||
margin-left: calc(15% + 1.25rem);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
table {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
form {
|
||||
margin-top: $space-two;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content__header {
|
||||
@@ -20,7 +24,7 @@
|
||||
background-color: $color-white;
|
||||
border-bottom: 1px solid $color-border;
|
||||
display: flex;
|
||||
min-height: 5.6rem;
|
||||
min-height: 3.5rem;
|
||||
padding: $space-small $space-normal;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
.logo-brand {
|
||||
margin-bottom: $space-normal;
|
||||
padding: $space-normal $space-smaller $space-small;
|
||||
text-align: left;
|
||||
|
||||
img {
|
||||
margin-bottom: $space-smaller;
|
||||
max-height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation {
|
||||
background: $white;
|
||||
border-right: 1px solid $color-border;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-medium;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: $space-normal;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 21rem;
|
||||
z-index: 1023;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: $font-size-small;
|
||||
|
||||
a {
|
||||
color: $color-gray;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
i {
|
||||
min-width: $space-medium;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: $space-slab;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation__link {
|
||||
background-color: transparent;
|
||||
color: $color-gray;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
margin-bottom: $space-smaller;
|
||||
padding: $space-small;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.navigation__link--active {
|
||||
background-color: $color-background;
|
||||
border-radius: $base-border-radius;
|
||||
color: $blue;
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout {
|
||||
bottom: $space-normal;
|
||||
left: $space-normal;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-small;
|
||||
padding-top: $space-smaller;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
.search {
|
||||
margin-left: auto;
|
||||
margin-right: 2rem;
|
||||
max-width: 44rem;
|
||||
margin-right: 1.25rem;
|
||||
max-width: 27.5rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
padding: 4px 12px;
|
||||
|
||||
.icon-container {
|
||||
margin-right: 4px;
|
||||
margin-right: 2px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Typography
|
||||
$base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif !default;
|
||||
$heading-font-family: $base-font-family !default;
|
||||
|
||||
$base-font-size: 14px !default;
|
||||
$base-font-size: 16px !default;
|
||||
|
||||
$base-line-height: 1.5 !default;
|
||||
$heading-line-height: 1.2 !default;
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
// Font sizes
|
||||
$font-size-nano: 0.8rem;
|
||||
$font-size-micro: 1.0rem;
|
||||
$font-size-mini: 1.2rem;
|
||||
$font-size-small: 1.4rem;
|
||||
$font-size-default: 1.6rem;
|
||||
$font-size-medium: 1.8rem;
|
||||
$font-size-large: 2.2rem;
|
||||
$font-size-big: 2.4rem;
|
||||
$font-size-bigger: 3.0rem;
|
||||
$font-size-mega: 3.4rem;
|
||||
$font-size-giga: 4.0rem;
|
||||
$font-size-nano: 0.5rem;
|
||||
$font-size-micro: 0.675rem;
|
||||
$font-size-mini: 0.75rem;
|
||||
$font-size-small: 0.875rem;
|
||||
$font-size-default: 1rem;
|
||||
$font-size-medium: 1.125rem;
|
||||
$font-size-large: 1.375rem;
|
||||
$font-size-big: 1.5rem;
|
||||
$font-size-bigger: 1.75rem;
|
||||
$font-size-mega: 2.125rem;
|
||||
$font-size-giga: 2.5rem;
|
||||
|
||||
// spaces
|
||||
$zero: 0;
|
||||
$space-micro: 0.2rem;
|
||||
$space-smaller: 0.4rem;
|
||||
$space-small: 0.8rem;
|
||||
$space-one: 1rem;
|
||||
$space-slab: 1.2rem;
|
||||
$space-normal: 1.6rem;
|
||||
$space-two: 2.0rem;
|
||||
$space-medium: 2.4rem;
|
||||
$space-large: 3.2rem;
|
||||
$space-larger: 4.8rem;
|
||||
$space-jumbo: 6.4rem;
|
||||
$space-mega: 10.0rem;
|
||||
$space-micro: 0.125rem;
|
||||
$space-smaller: 0.25rem;
|
||||
$space-small: 0.5rem;
|
||||
$space-one: 0.675rem;
|
||||
$space-slab: 0.75rem;
|
||||
$space-normal: 1rem;
|
||||
$space-two: 1.25rem;
|
||||
$space-medium: 1.5rem;
|
||||
$space-large: 2rem;
|
||||
$space-larger: 3rem;
|
||||
$space-jumbo: 4rem;
|
||||
$space-mega: 6.25rem;
|
||||
|
||||
// font-weight
|
||||
$font-weight-feather: 100;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
|
||||
pattr_initialize [:account_name, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale]
|
||||
|
||||
def perform
|
||||
if @user.nil?
|
||||
@@ -15,12 +15,22 @@ class AccountBuilder
|
||||
end
|
||||
[@user, @account]
|
||||
rescue StandardError => e
|
||||
puts e.inspect
|
||||
Rails.logger.debug e.inspect
|
||||
raise e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_full_name
|
||||
# the empty string ensures that not-null constraint is not violated
|
||||
@user_full_name || ''
|
||||
end
|
||||
|
||||
def account_name
|
||||
# the empty string ensures that not-null constraint is not violated
|
||||
@account_name || ''
|
||||
end
|
||||
|
||||
def validate_email
|
||||
address = ValidEmail2::Address.new(@email)
|
||||
if address.valid? # && !address.disposable?
|
||||
@@ -39,7 +49,7 @@ class AccountBuilder
|
||||
end
|
||||
|
||||
def create_account
|
||||
@account = Account.create!(name: @account_name, locale: I18n.locale)
|
||||
@account = Account.create!(name: account_name, locale: I18n.locale)
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
@@ -64,7 +74,7 @@ class AccountBuilder
|
||||
@user = User.new(email: @email,
|
||||
password: user_password,
|
||||
password_confirmation: user_password,
|
||||
name: @user_full_name)
|
||||
name: user_full_name)
|
||||
@user.type = 'SuperAdmin' if @super_admin
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# The AgentBuilder class is responsible for creating a new agent.
|
||||
# It initializes with necessary attributes and provides a perform method
|
||||
# to create a user and account user in a transaction.
|
||||
class AgentBuilder
|
||||
# Initializes an AgentBuilder with necessary attributes.
|
||||
# @param email [String] the email of the user.
|
||||
# @param name [String] the name of the user.
|
||||
# @param role [String] the role of the user, defaults to 'agent' if not provided.
|
||||
# @param inviter [User] the user who is inviting the agent (Current.user in most cases).
|
||||
# @param availability [String] the availability status of the user, defaults to 'offline' if not provided.
|
||||
# @param auto_offline [Boolean] the auto offline status of the user.
|
||||
pattr_initialize [:email, { name: '' }, :inviter, :account, { role: :agent }, { availability: :offline }, { auto_offline: false }]
|
||||
|
||||
# Creates a user and account user in a transaction.
|
||||
# @return [User] the created user.
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
@user = find_or_create_user
|
||||
send_confirmation_if_required
|
||||
create_account_user
|
||||
end
|
||||
@user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Finds a user by email or creates a new one with a temporary password.
|
||||
# @return [User] the found or created user.
|
||||
def find_or_create_user
|
||||
user = User.from_email(email)
|
||||
return user if user
|
||||
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password)
|
||||
end
|
||||
|
||||
# Sends confirmation instructions if the user is persisted and not confirmed.
|
||||
def send_confirmation_if_required
|
||||
@user.send_confirmation_instructions if user_needs_confirmation?
|
||||
end
|
||||
|
||||
# Checks if the user needs confirmation.
|
||||
# @return [Boolean] true if the user is persisted and not confirmed, false otherwise.
|
||||
def user_needs_confirmation?
|
||||
@user.persisted? && !@user.confirmed?
|
||||
end
|
||||
|
||||
# Creates an account user linking the user to the current account.
|
||||
def create_account_user
|
||||
AccountUser.create!({
|
||||
account_id: account.id,
|
||||
user_id: @user.id,
|
||||
inviter_id: inviter.id
|
||||
}.merge({
|
||||
role: role,
|
||||
availability: availability,
|
||||
auto_offline: auto_offline
|
||||
}.compact))
|
||||
end
|
||||
end
|
||||
@@ -75,7 +75,7 @@ class ContactInboxWithContactBuilder
|
||||
def find_contact_by_email(email)
|
||||
return if email.blank?
|
||||
|
||||
account.contacts.find_by(email: email.downcase)
|
||||
account.contacts.from_email(email)
|
||||
end
|
||||
|
||||
def find_contact_by_phone_number(phone_number)
|
||||
|
||||
@@ -25,7 +25,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
build_contact_inbox
|
||||
build_message
|
||||
end
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
rescue Koala::Facebook::AuthenticationError => e
|
||||
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||
Rails.logger.error e
|
||||
@inbox.channel.authorization_error!
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||
@@ -93,6 +95,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message_type: @message_type,
|
||||
content: response.content,
|
||||
source_id: response.identifier,
|
||||
content_attributes: {
|
||||
in_reply_to_external_id: response.in_reply_to_external_id
|
||||
},
|
||||
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
||||
}
|
||||
end
|
||||
@@ -105,11 +110,15 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
}
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
rescue Koala::Facebook::AuthenticationError => e
|
||||
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||
Rails.logger.error e
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue Koala::Facebook::ClientError => e
|
||||
@@ -127,4 +136,6 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
end
|
||||
process_contact_params_result(result)
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
|
||||
@@ -20,7 +20,9 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
ActiveRecord::Base.transaction do
|
||||
build_message
|
||||
end
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
rescue Koala::Facebook::AuthenticationError => e
|
||||
Rails.logger.warn("Instagram authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||
Rails.logger.error e
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
@@ -46,6 +48,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
@outgoing_echo ? recipient_id : sender_id
|
||||
end
|
||||
|
||||
def message_is_unsupported?
|
||||
message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true
|
||||
end
|
||||
|
||||
def sender_id
|
||||
@messaging[:sender][:id]
|
||||
end
|
||||
@@ -76,6 +82,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
|
||||
end
|
||||
|
||||
def message_reply_attributes
|
||||
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
|
||||
end
|
||||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
return if message_content.blank? && all_unsupported_files?
|
||||
@@ -112,14 +122,20 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
params = {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: message_type,
|
||||
source_id: message_identifier,
|
||||
content: message_content,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
sender: @outgoing_echo ? nil : contact,
|
||||
content_attributes: {
|
||||
in_reply_to_external_id: message_reply_attributes
|
||||
}
|
||||
}
|
||||
|
||||
params[:content_attributes][:is_unsupported] = true if message_is_unsupported?
|
||||
params
|
||||
end
|
||||
|
||||
def already_sent_from_chatwoot?
|
||||
|
||||
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
|
||||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@in_reply_to = content_attributes&.dig(:in_reply_to)
|
||||
@items = content_attributes&.dig(:items)
|
||||
end
|
||||
|
||||
def perform
|
||||
@@ -26,6 +26,38 @@ class Messages::MessageBuilder
|
||||
|
||||
private
|
||||
|
||||
# Extracts content attributes from the given params.
|
||||
# - Converts ActionController::Parameters to a regular hash if needed.
|
||||
# - Attempts to parse a JSON string if content is a string.
|
||||
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
|
||||
def content_attributes
|
||||
params = convert_to_hash(@params)
|
||||
content_attributes = params.fetch(:content_attributes, {})
|
||||
|
||||
return parse_json(content_attributes) if content_attributes.is_a?(String)
|
||||
return content_attributes if content_attributes.is_a?(Hash)
|
||||
|
||||
{}
|
||||
end
|
||||
|
||||
# Converts the given object to a hash.
|
||||
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
|
||||
# Otherwise, returns the object as-is.
|
||||
def convert_to_hash(obj)
|
||||
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
# Attempts to parse a string as JSON.
|
||||
# If successful, returns the parsed hash with symbolized names.
|
||||
# If unsuccessful, returns nil.
|
||||
def parse_json(content)
|
||||
JSON.parse(content, symbolize_names: true)
|
||||
rescue JSON::ParserError
|
||||
{}
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
return if @attachments.blank?
|
||||
|
||||
@@ -117,7 +149,8 @@ class Messages::MessageBuilder
|
||||
content_type: @params[:content_type],
|
||||
items: @items,
|
||||
in_reply_to: @in_reply_to,
|
||||
echo_id: @params[:echo_id]
|
||||
echo_id: @params[:echo_id],
|
||||
source_id: @params[:source_id]
|
||||
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
class NotificationBuilder
|
||||
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
|
||||
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!, :secondary_actor]
|
||||
|
||||
def perform
|
||||
return unless user_subscribed_to_notification?
|
||||
|
||||
build_notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def secondary_actor
|
||||
def current_user
|
||||
Current.user
|
||||
end
|
||||
|
||||
def user_subscribed_to_notification?
|
||||
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
||||
# added for the case where an assignee might be removed from the account but remains in conversation
|
||||
return if notification_setting.blank?
|
||||
return false if notification_setting.blank?
|
||||
|
||||
return true if notification_setting.public_send("email_#{notification_type}?")
|
||||
return true if notification_setting.public_send("push_#{notification_type}?")
|
||||
@@ -25,11 +23,15 @@ class NotificationBuilder
|
||||
end
|
||||
|
||||
def build_notification
|
||||
# Create conversation_creation notification only if user is subscribed to it
|
||||
return if notification_type == 'conversation_creation' && !user_subscribed_to_notification?
|
||||
|
||||
user.notifications.create!(
|
||||
notification_type: notification_type,
|
||||
account: account,
|
||||
primary_actor: primary_actor,
|
||||
secondary_actor: secondary_actor
|
||||
# secondary_actor is secondary_actor if present, else current_user
|
||||
secondary_actor: secondary_actor || current_user
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,10 @@ class V2::ReportBuilder
|
||||
end
|
||||
|
||||
def timeseries
|
||||
send(params[:metric])
|
||||
return send(params[:metric]) if metric_valid?
|
||||
|
||||
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
|
||||
{}
|
||||
end
|
||||
|
||||
# For backward compatible with old report
|
||||
@@ -43,6 +46,21 @@ class V2::ReportBuilder
|
||||
}
|
||||
end
|
||||
|
||||
def short_summary
|
||||
{
|
||||
conversations_count: conversations.count,
|
||||
avg_first_response_time: avg_first_response_time_summary,
|
||||
avg_resolution_time: avg_resolution_time_summary
|
||||
}
|
||||
end
|
||||
|
||||
def bot_summary
|
||||
{
|
||||
bot_resolutions_count: bot_resolutions.count,
|
||||
bot_handoffs_count: bot_handoffs.count
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_metrics
|
||||
if params[:type].equal?(:account)
|
||||
live_conversations
|
||||
@@ -53,6 +71,18 @@ class V2::ReportBuilder
|
||||
|
||||
private
|
||||
|
||||
def metric_valid?
|
||||
%w[conversations_count
|
||||
incoming_messages_count
|
||||
outgoing_messages_count
|
||||
avg_first_response_time
|
||||
avg_resolution_time reply_time
|
||||
resolutions_count
|
||||
bot_resolutions_count
|
||||
bot_handoffs_count
|
||||
reply_time].include?(params[:metric])
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
@@ -102,6 +132,7 @@ class V2::ReportBuilder
|
||||
unattended: @open_conversations.unattended.count
|
||||
}
|
||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||
metric[:pending] = @open_conversations.pending.count if params[:type].equal?(:account)
|
||||
metric
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count
|
||||
end
|
||||
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_first_response_time
|
||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_reply_time
|
||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
:user_id
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.account_users.each_with_object([]) do |account_user, arr|
|
||||
arr << {
|
||||
id: account_user.user_id,
|
||||
conversations_count: @grouped_conversations_count[account_user.user_id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
||||
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class V2::Reports::BaseSummaryBuilder
|
||||
include DateRangeHelper
|
||||
|
||||
private
|
||||
|
||||
def group_by_key
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def get_grouped_average(events)
|
||||
events.group(group_by_key).average(average_value_key)
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
params[:business_hours].present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
class V2::Reports::BotMetricsBuilder
|
||||
include DateRangeHelper
|
||||
attr_reader :account, :params
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def metrics
|
||||
{
|
||||
conversation_count: bot_conversations.count,
|
||||
message_count: bot_messages.count,
|
||||
resolution_rate: bot_resolution_rate.to_i,
|
||||
handoff_rate: bot_handoff_rate.to_i
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def bot_activated_inbox_ids
|
||||
@bot_activated_inbox_ids ||= account.inboxes.filter(&:active_bot?).map(&:id)
|
||||
end
|
||||
|
||||
def bot_conversations
|
||||
@bot_conversations ||= account.conversations.where(inbox_id: bot_activated_inbox_ids).where(created_at: range)
|
||||
end
|
||||
|
||||
def bot_messages
|
||||
@bot_messages ||= account.messages.outgoing.where(conversation_id: bot_conversations.ids).where(created_at: range)
|
||||
end
|
||||
|
||||
def bot_resolutions_count
|
||||
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
|
||||
created_at: range).distinct.count
|
||||
end
|
||||
|
||||
def bot_handoffs_count
|
||||
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff,
|
||||
created_at: range).distinct.count
|
||||
end
|
||||
|
||||
def bot_resolution_rate
|
||||
return 0 if bot_conversations.count.zero?
|
||||
|
||||
bot_resolutions_count.to_f / bot_conversations.count * 100
|
||||
end
|
||||
|
||||
def bot_handoff_rate
|
||||
return 0 if bot_conversations.count.zero?
|
||||
|
||||
bot_handoffs_count.to_f / bot_conversations.count * 100
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count
|
||||
end
|
||||
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_first_response_time
|
||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_reply_time
|
||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
'conversations.team_id'
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.teams.each_with_object([]) do |team, arr|
|
||||
arr << {
|
||||
id: team.id,
|
||||
conversations_count: @grouped_conversations_count[team.id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B
|
||||
mergee_contact: @mergee_contact
|
||||
)
|
||||
contact_merge_action.perform
|
||||
render json: @base_contact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -10,11 +10,18 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@agent_bot = Current.account.agent_bots.create!(permitted_params)
|
||||
@agent_bot = Current.account.agent_bots.create!(permitted_params.except(:avatar_url))
|
||||
process_avatar_from_url
|
||||
end
|
||||
|
||||
def update
|
||||
@agent_bot.update!(permitted_params)
|
||||
@agent_bot.update!(permitted_params.except(:avatar_url))
|
||||
process_avatar_from_url
|
||||
end
|
||||
|
||||
def avatar
|
||||
@agent_bot.avatar.purge if @agent_bot.avatar.attached?
|
||||
@agent_bot
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -30,6 +37,10 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
|
||||
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content])
|
||||
end
|
||||
|
||||
def process_avatar_from_url
|
||||
::Avatar::AvatarFromUrlJob.perform_later(@agent_bot, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_agent, except: [:create, :index]
|
||||
before_action :fetch_agent, except: [:create, :index, :bulk_create]
|
||||
before_action :check_authorization
|
||||
before_action :find_user, only: [:create]
|
||||
before_action :validate_limit, only: [:create]
|
||||
before_action :create_user, only: [:create]
|
||||
before_action :save_account_user, only: [:create]
|
||||
before_action :validate_limit_for_bulk_create, only: [:bulk_create]
|
||||
|
||||
def index
|
||||
@agents = agents
|
||||
end
|
||||
|
||||
def create; end
|
||||
def create
|
||||
builder = AgentBuilder.new(
|
||||
email: new_agent_params['email'],
|
||||
name: new_agent_params['name'],
|
||||
role: new_agent_params['role'],
|
||||
availability: new_agent_params['availability'],
|
||||
auto_offline: new_agent_params['auto_offline'],
|
||||
inviter: current_user,
|
||||
account: Current.account
|
||||
)
|
||||
|
||||
builder.perform
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.slice(:name).compact)
|
||||
@@ -23,6 +33,30 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def bulk_create
|
||||
emails = params[:emails]
|
||||
|
||||
emails.each do |email|
|
||||
builder = AgentBuilder.new(
|
||||
email: email,
|
||||
name: email.split('@').first,
|
||||
inviter: current_user,
|
||||
account: Current.account
|
||||
)
|
||||
begin
|
||||
builder.perform
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
||||
end
|
||||
end
|
||||
|
||||
# This endpoint is used to bulk create agents during onboarding
|
||||
# onboarding_step key in present in Current account custom attributes, since this is a one time operation
|
||||
Current.account.custom_attributes.delete('onboarding_step')
|
||||
Current.account.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
@@ -33,47 +67,34 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
@agent = agents.find(params[:id])
|
||||
end
|
||||
|
||||
def find_user
|
||||
@user = User.find_by(email: new_agent_params[:email])
|
||||
end
|
||||
|
||||
# TODO: move this to a builder and combine the save account user method into a builder
|
||||
# ensure the account user association is also created in a single transaction
|
||||
def create_user
|
||||
return @user.send_confirmation_instructions if @user
|
||||
|
||||
@user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
|
||||
end
|
||||
|
||||
def save_account_user
|
||||
AccountUser.create!({
|
||||
account_id: Current.account.id,
|
||||
user_id: @user.id,
|
||||
inviter_id: current_user.id
|
||||
}.merge({
|
||||
role: new_agent_params[:role],
|
||||
availability: new_agent_params[:availability],
|
||||
auto_offline: new_agent_params[:auto_offline]
|
||||
}.compact))
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
# intial string ensures the password requirements are met
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
||||
end
|
||||
|
||||
def agents
|
||||
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
|
||||
end
|
||||
|
||||
def validate_limit_for_bulk_create
|
||||
limit_available = params[:emails].count <= available_agent_count
|
||||
|
||||
render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available
|
||||
end
|
||||
|
||||
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') unless can_add_agent?
|
||||
end
|
||||
|
||||
def available_agent_count
|
||||
Current.account.usage_limits[:agents] - agents.count
|
||||
end
|
||||
|
||||
def can_add_agent?
|
||||
available_agent_count.positive?
|
||||
end
|
||||
|
||||
def delete_user_record(agent)
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
@articles_count = @all_articles.count
|
||||
|
||||
@articles = if list_params[:category_slug].present?
|
||||
@all_articles.order_by_position.page(@current_page).per(50)
|
||||
@all_articles.order_by_position.page(@current_page)
|
||||
else
|
||||
@all_articles.order_by_updated_at.page(@current_page)
|
||||
end
|
||||
|
||||
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||
|
||||
def category_params
|
||||
params.require(:category).permit(
|
||||
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
|
||||
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def export
|
||||
column_names = params['column_names']
|
||||
Account::ContactsExportJob.perform_later(Current.account.id, column_names)
|
||||
Account::ContactsExportJob.perform_later(Current.account.id, column_names, Current.user.email)
|
||||
head :ok, message: I18n.t('errors.contacts.export.success')
|
||||
end
|
||||
|
||||
@@ -83,14 +83,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
|
||||
@contact.save!
|
||||
@contact_inbox = build_contact_inbox
|
||||
process_avatar
|
||||
process_avatar_from_url
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@contact.assign_attributes(contact_update_params)
|
||||
@contact.save!
|
||||
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
|
||||
process_avatar_from_url
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -148,7 +148,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :identifier, :email, :phone_number, :avatar, :avatar_url, additional_attributes: {}, custom_attributes: {})
|
||||
params.permit(:name, :identifier, :email, :phone_number, :avatar, :blocked, :avatar_url, additional_attributes: {}, custom_attributes: {})
|
||||
end
|
||||
|
||||
def contact_custom_attributes
|
||||
@@ -174,7 +174,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||
end
|
||||
|
||||
def process_avatar
|
||||
def process_avatar_from_url
|
||||
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
|
||||
|
||||
def set_agent
|
||||
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(@agent)
|
||||
@conversation.assignee = @agent
|
||||
@conversation.save!
|
||||
render_agent
|
||||
end
|
||||
|
||||
|
||||
@@ -18,6 +18,15 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
end
|
||||
end
|
||||
|
||||
def retry
|
||||
return if message.blank?
|
||||
|
||||
message.update!(status: :sent, content_attributes: {})
|
||||
::SendReplyJob.perform_later(message.id)
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
def translate
|
||||
return head :ok if already_translated_content_available?
|
||||
|
||||
|
||||
@@ -60,13 +60,26 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
if params[:status]
|
||||
# FIXME: move this logic into a service object
|
||||
if pending_to_open_by_bot?
|
||||
@conversation.bot_handoff!
|
||||
elsif params[:status].present?
|
||||
set_conversation_status
|
||||
@status = @conversation.save!
|
||||
else
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
||||
assign_conversation if should_assign_conversation?
|
||||
end
|
||||
|
||||
def pending_to_open_by_bot?
|
||||
return false unless Current.user.is_a?(AgentBot)
|
||||
|
||||
@conversation.status == 'pending' && params[:status] == 'open'
|
||||
end
|
||||
|
||||
def should_assign_conversation?
|
||||
@conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
||||
end
|
||||
|
||||
def toggle_priority
|
||||
@@ -110,8 +123,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def assign_conversation
|
||||
@agent = Current.account.users.find(current_user.id)
|
||||
@conversation.update_assignee(@agent)
|
||||
@conversation.assignee = current_user
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def conversation
|
||||
|
||||
@@ -39,6 +39,8 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
||||
:attribute_display_type,
|
||||
:attribute_key,
|
||||
:attribute_model,
|
||||
:regex_pattern,
|
||||
:regex_cue,
|
||||
attribute_values: []
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||
RESULTS_PER_PAGE = 15
|
||||
include DateRangeHelper
|
||||
|
||||
before_action :fetch_notification, only: [:update]
|
||||
before_action :fetch_notification, only: [:update, :destroy, :snooze, :unread]
|
||||
before_action :set_primary_actor, only: [:read_all]
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
||||
@count = notifications.count
|
||||
@notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
@notifications = notification_finder.notifications
|
||||
@unread_count = notification_finder.unread_count
|
||||
@count = notification_finder.count
|
||||
end
|
||||
|
||||
def read_all
|
||||
@@ -28,11 +29,36 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
def unread
|
||||
@notification.update(read_at: nil)
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
def destroy
|
||||
@notification.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy_all
|
||||
if params[:type] == 'read'
|
||||
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read)
|
||||
else
|
||||
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all)
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
||||
@unread_count = notification_finder.unread_count
|
||||
render json: @unread_count
|
||||
end
|
||||
|
||||
def snooze
|
||||
updated_meta = (@notification.meta || {}).merge('last_snoozed_at' => nil)
|
||||
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s), meta: updated_meta) if params[:snoozed_until]
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_primary_actor
|
||||
@@ -50,7 +76,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
|
||||
def notifications
|
||||
@notifications ||= current_user.notifications.where(account_id: current_account.id)
|
||||
def notification_finder
|
||||
@notification_finder ||= NotificationFinder.new(Current.user, Current.account, params)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def logo
|
||||
@portal.logo.purge if @portal.logo.attached?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def process_attached_logo
|
||||
blob_id = params[:blob_id]
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
|
||||
@@ -5,11 +5,13 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
before_action :ensure_account_name, only: [:create]
|
||||
before_action :validate_captcha, only: [:create]
|
||||
before_action :fetch_account, except: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
|
||||
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||
CustomExceptions::Account::InvalidParams,
|
||||
CustomExceptions::Account::UserExists,
|
||||
CustomExceptions::Account::UserErrors,
|
||||
with: :render_error_response
|
||||
@@ -38,11 +40,14 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
|
||||
def cache_keys
|
||||
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
|
||||
render json: { cache_keys: get_cache_keys }, status: :ok
|
||||
render json: { cache_keys: cache_keys_for_account }, status: :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
@account.custom_attributes.merge!(custom_attributes_params)
|
||||
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def update_active_at
|
||||
@@ -53,7 +58,18 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def get_cache_keys
|
||||
def ensure_account_name
|
||||
# ensure that account_name and user_full_name is present
|
||||
# this is becuase the account builder and the models validations are not triggered
|
||||
# this change is to align the behaviour with the v2 accounts controller
|
||||
# since these values are not required directly there
|
||||
return if account_params[:account_name].present?
|
||||
return if account_params[:user_full_name].present?
|
||||
|
||||
raise CustomExceptions::Account::InvalidParams.new({})
|
||||
end
|
||||
|
||||
def cache_keys_for_account
|
||||
{
|
||||
label: fetch_value_for_key(params[:id], Label.name.underscore),
|
||||
inbox: fetch_value_for_key(params[:id], Inbox.name.underscore),
|
||||
@@ -70,6 +86,10 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||
end
|
||||
|
||||
def custom_attributes_params
|
||||
params.permit(:industry, :company_size, :timezone)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
@@ -10,7 +10,9 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
@user.update!(password_params.except(:current_password))
|
||||
end
|
||||
|
||||
@user.update!(profile_params)
|
||||
@user.assign_attributes(profile_params)
|
||||
@user.custom_attributes.merge!(custom_attributes_params)
|
||||
@user.save!
|
||||
end
|
||||
|
||||
def avatar
|
||||
@@ -31,6 +33,11 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def resend_confirmation
|
||||
@user.send_confirmation_instructions unless @user.confirmed?
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@@ -57,6 +64,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
)
|
||||
end
|
||||
|
||||
def custom_attributes_params
|
||||
params.require(:profile).permit(:phone_number)
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:profile).permit(
|
||||
:current_password,
|
||||
|
||||
@@ -79,6 +79,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
sender: @contact,
|
||||
content: permitted_params[:message][:content],
|
||||
inbox_id: conversation.inbox_id,
|
||||
content_attributes: {
|
||||
in_reply_to: permitted_params[:message][:reply_to]
|
||||
},
|
||||
echo_id: permitted_params[:message][:echo_id],
|
||||
message_type: :incoming
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
|
||||
|
||||
def index
|
||||
@conversation = conversation
|
||||
@@ -27,6 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
|
||||
conversation.contact_last_seen_at = DateTime.now.utc
|
||||
conversation.save!
|
||||
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -41,8 +43,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def toggle_typing
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
case permitted_params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
@@ -54,8 +54,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
return head :not_found if conversation.nil?
|
||||
|
||||
return head :forbidden unless @web_widget.end_conversation?
|
||||
|
||||
unless conversation.resolved?
|
||||
@@ -81,6 +79,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
|
||||
end
|
||||
|
||||
def render_not_found_if_empty
|
||||
return head :not_found if conversation.nil?
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
||||
message: [:content, :referer_url, :timestamp, :echo_id],
|
||||
|
||||
@@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def permitted_params
|
||||
# timestamp parameter is used in create conversation method
|
||||
params.permit(:id, :before, :after, :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, :reply_to])
|
||||
end
|
||||
|
||||
def set_message
|
||||
|
||||
@@ -14,6 +14,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
render json: summary_metrics
|
||||
end
|
||||
|
||||
def bot_summary
|
||||
summary = V2::ReportBuilder.new(Current.account, current_summary_params).bot_summary
|
||||
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).bot_summary
|
||||
render json: summary
|
||||
end
|
||||
|
||||
def agents
|
||||
@report_data = generate_agents_report
|
||||
generate_csv('agents_report', 'api/v2/accounts/reports/agents')
|
||||
@@ -48,6 +54,11 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
render json: conversation_metrics
|
||||
end
|
||||
|
||||
def bot_metrics
|
||||
bot_metrics = V2::Reports::BotMetricsBuilder.new(Current.account, params).metrics
|
||||
render json: bot_metrics
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_csv(filename, template)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team]
|
||||
|
||||
def agent
|
||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||
end
|
||||
|
||||
def team
|
||||
render_report_with(V2::Reports::TeamSummaryBuilder)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize :report, :view?
|
||||
end
|
||||
|
||||
def prepare_builder_params
|
||||
@builder_params = {
|
||||
since: permitted_params[:since],
|
||||
until: permitted_params[:until],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(permitted_params[:business_hours])
|
||||
}
|
||||
end
|
||||
|
||||
def render_report_with(builder_class)
|
||||
builder = builder_class.new(account: Current.account, params: @builder_params)
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:since, :until, :business_hours)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
class Api::V2::AccountsController < Api::BaseController
|
||||
include AuthHelper
|
||||
|
||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||
only: [:create], raise: false
|
||||
before_action :check_signup_enabled, only: [:create]
|
||||
before_action :validate_captcha, only: [:create]
|
||||
before_action :fetch_account, except: [:create]
|
||||
before_action :check_authorization, except: [:create]
|
||||
|
||||
rescue_from CustomExceptions::Account::InvalidEmail,
|
||||
CustomExceptions::Account::UserExists,
|
||||
CustomExceptions::Account::UserErrors,
|
||||
with: :render_error_response
|
||||
|
||||
def create
|
||||
@user, @account = AccountBuilder.new(
|
||||
email: account_params[:email],
|
||||
user_password: account_params[:password],
|
||||
locale: account_params[:locale],
|
||||
user: current_user
|
||||
).perform
|
||||
|
||||
fetch_account_and_user_info
|
||||
update_account_info if @account.present?
|
||||
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
|
||||
else
|
||||
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def account_attributes
|
||||
{
|
||||
custom_attributes: @account.custom_attributes.merge({ 'onboarding_step' => 'profile_update' })
|
||||
}
|
||||
end
|
||||
|
||||
def update_account_info
|
||||
@account.update!(
|
||||
account_attributes
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_account_and_user_info; end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||
end
|
||||
|
||||
def validate_captcha
|
||||
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||
end
|
||||
end
|
||||
|
||||
Api::V2::AccountsController.prepend_mod_with('Api::V2::AccountsController')
|
||||
@@ -1,7 +1,8 @@
|
||||
module AccessTokenAuthHelper
|
||||
BOT_ACCESSIBLE_ENDPOINTS = {
|
||||
'api/v1/accounts/conversations' => %w[toggle_status create],
|
||||
'api/v1/accounts/conversations/messages' => ['create']
|
||||
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create],
|
||||
'api/v1/accounts/conversations/messages' => ['create'],
|
||||
'api/v1/accounts/conversations/assignments' => ['create']
|
||||
}.freeze
|
||||
|
||||
def ensure_access_token
|
||||
@@ -14,7 +15,14 @@ module AccessTokenAuthHelper
|
||||
render_unauthorized('Invalid Access Token') && return if @access_token.blank?
|
||||
|
||||
@resource = @access_token.owner
|
||||
Current.user = @resource if current_user.is_a?(User)
|
||||
Current.user = @resource if allowed_current_user_type?(@resource)
|
||||
end
|
||||
|
||||
def allowed_current_user_type?(resource)
|
||||
return true if resource.is_a?(User)
|
||||
return true if resource.is_a?(AgentBot)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def validate_bot_access_token!
|
||||
|
||||
@@ -25,6 +25,6 @@ module EnsureCurrentAccountHelper
|
||||
end
|
||||
|
||||
def account_accessible_for_bot?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
render_unauthorized('Bot is not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,7 +55,7 @@ class DashboardController < ActionController::Base
|
||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||
FACEBOOK_API_VERSION: 'v14.0',
|
||||
FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'),
|
||||
IS_ENTERPRISE: ChatwootApp.enterprise?,
|
||||
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
|
||||
GIT_SHA: GIT_HASH
|
||||
|
||||
@@ -5,7 +5,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
||||
skip_before_action :authenticate_user!, raise: false
|
||||
|
||||
def create
|
||||
@user = User.find_by(email: params[:email])
|
||||
@user = User.from_email(params[:email])
|
||||
if @user
|
||||
@user.send_reset_password_instructions
|
||||
build_response(I18n.t('messages.reset_password_success'), 200)
|
||||
|
||||
@@ -33,7 +33,7 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
|
||||
def process_sso_auth_token
|
||||
return if params[:email].blank?
|
||||
|
||||
user = User.find_by(email: params[:email])
|
||||
user = User.from_email(params[:email])
|
||||
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,13 +9,15 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@resource = AgentBot.new(agent_bot_params)
|
||||
@resource = AgentBot.new(agent_bot_params.except(:avatar_url))
|
||||
@resource.save!
|
||||
process_avatar_from_url
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
def update
|
||||
@resource.update!(agent_bot_params)
|
||||
@resource.update!(agent_bot_params.except(:avatar_url))
|
||||
process_avatar_from_url
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -23,6 +25,11 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def avatar
|
||||
@resource.avatar.purge if @resource.avatar.attached?
|
||||
@resource
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_resource
|
||||
@@ -30,6 +37,10 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
||||
end
|
||||
|
||||
def agent_bot_params
|
||||
params.permit(:name, :description, :account_id, :outgoing_url)
|
||||
params.permit(:name, :description, :account_id, :outgoing_url, :avatar, :avatar_url)
|
||||
end
|
||||
|
||||
def process_avatar_from_url
|
||||
::Avatar::AvatarFromUrlJob.perform_later(@resource, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||
@resource = (User.from_email(user_params[:email]) || User.new(user_params))
|
||||
@resource.skip_confirmation!
|
||||
@resource.save!
|
||||
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
|
||||
include Events::Types
|
||||
before_action :set_conversation, only: [:toggle_typing, :update_last_seen]
|
||||
before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status]
|
||||
|
||||
def index
|
||||
@conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@conversation = create_conversation
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
# Check if the conversation is already resolved to prevent redundant operations
|
||||
return if @conversation.resolved?
|
||||
|
||||
# Assign the conversation's contact as the resolver
|
||||
# This step attributes the resolution action to the contact involved in the conversation
|
||||
# If this assignment is not made, the system implicitly becomes the resolver by default
|
||||
Current.contact = @conversation.contact
|
||||
|
||||
# Update the conversation's status to 'resolved' to reflect its closure
|
||||
@conversation.status = :resolved
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def toggle_typing
|
||||
case params[:typing_status]
|
||||
when 'on'
|
||||
@@ -23,13 +39,18 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
||||
def update_last_seen
|
||||
@conversation.contact_last_seen_at = DateTime.now.utc
|
||||
@conversation.save!
|
||||
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_conversation
|
||||
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:id])
|
||||
@conversation = if @contact_inbox.hmac_verified?
|
||||
@contact_inbox.contact.conversations.find_by!(display_id: params[:id])
|
||||
else
|
||||
@contact_inbox.conversations.find_by!(display_id: params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
|
||||
@@ -39,10 +39,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
)
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:query, :locale, :sort, :status)
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Public::Api::V1::Portals::BaseController < PublicController
|
||||
before_action :show_plain_layout
|
||||
before_action :set_color_scheme
|
||||
before_action :set_global_config
|
||||
around_action :set_locale
|
||||
after_action :allow_iframe_requests
|
||||
|
||||
@@ -11,11 +12,11 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
end
|
||||
|
||||
def set_color_scheme
|
||||
@theme = if %w[dark light].include?(params[:theme])
|
||||
params[:theme]
|
||||
else
|
||||
''
|
||||
end
|
||||
@theme_from_params = params[:theme] if %w[dark light].include?(params[:theme])
|
||||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
end
|
||||
|
||||
def set_locale(&)
|
||||
@@ -40,6 +41,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
def switch_locale_with_article(&)
|
||||
article = Article.find_by(slug: params[:article_slug])
|
||||
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
|
||||
render_404 && return if article.blank?
|
||||
|
||||
@locale = if article.category.present?
|
||||
article.category.locale
|
||||
@@ -53,4 +56,13 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
def allow_iframe_requests
|
||||
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
|
||||
end
|
||||
|
||||
def render_404
|
||||
portal
|
||||
render 'public/api/v1/portals/error/404', status: :not_found
|
||||
end
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,10 +13,9 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
|
||||
private
|
||||
|
||||
def set_category
|
||||
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
||||
end
|
||||
@category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
|
||||
|
||||
def portal
|
||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||
Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
|
||||
render_404 && return if @category.blank?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
before_action :set_config
|
||||
before_action :allowed_configs
|
||||
def show
|
||||
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
|
||||
# ref: https://github.com/rubocop/rubocop/issues/7767
|
||||
# rubocop:disable Style/HashTransformValues
|
||||
@fb_config = InstallationConfig.where(name: @allowed_configs)
|
||||
.pluck(:name, :serialized_value)
|
||||
.map { |name, serialized_value| [name, serialized_value['value']] }
|
||||
.to_h
|
||||
@app_config = InstallationConfig.where(name: @allowed_configs)
|
||||
.pluck(:name, :serialized_value)
|
||||
.map { |name, serialized_value| [name, serialized_value['value']] }
|
||||
.to_h
|
||||
# rubocop:enable Style/HashTransformValues
|
||||
@installation_configs = ConfigLoader.new.general_configs.each_with_object({}) do |config_hash, result|
|
||||
result[config_hash['name']] = config_hash.except('name')
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
params['app_config'].each do |key, value|
|
||||
next unless @allowed_configs.include?(key)
|
||||
|
||||
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
||||
i.value = value
|
||||
i.save!
|
||||
end
|
||||
redirect_to super_admin_app_config_url
|
||||
redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_config
|
||||
@config = params[:config] || 'general'
|
||||
end
|
||||
|
||||
def allowed_configs
|
||||
@allowed_configs = case @config
|
||||
when 'facebook'
|
||||
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
|
||||
when 'email'
|
||||
['MAILER_INBOUND_EMAIL_DOMAIN']
|
||||
else
|
||||
%w[ENABLE_ACCOUNT_SIGNUP]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SuperAdmin::AppConfigsController.prepend_mod_with('SuperAdmin::AppConfigsController')
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
# If you want to add pagination or other controller-level concerns,
|
||||
# you're free to overwrite the RESTful controller actions.
|
||||
class SuperAdmin::ApplicationController < Administrate::ApplicationController
|
||||
include ActionView::Helpers::TagHelper
|
||||
include ActionView::Context
|
||||
|
||||
helper_method :render_vue_component
|
||||
# authenticiation done via devise : SuperAdmin Model
|
||||
before_action :authenticate_super_admin!
|
||||
|
||||
@@ -20,4 +24,24 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController
|
||||
params.fetch(resource_name, {}).fetch(:direction, 'desc')
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_vue_component(component_name, props = {})
|
||||
html_options = {
|
||||
id: 'app',
|
||||
data: {
|
||||
component_name: component_name,
|
||||
props: props.to_json
|
||||
}
|
||||
}
|
||||
content_tag(:div, '', html_options)
|
||||
end
|
||||
|
||||
def invalid_action_perfomed
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
flash[:error] = 'Invalid action performed'
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,7 +27,8 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
|
||||
|
||||
true
|
||||
rescue StandardError => e
|
||||
@error_message = e.message
|
||||
Rails.logger.error e.message
|
||||
@error_message = 'Invalid credentials. Please try again.'
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
|
||||
rescue_from ActiveRecord::RecordNotUnique, :with => :invalid_action_perfomed
|
||||
# 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.
|
||||
#
|
||||
|
||||
@@ -28,8 +28,7 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
||||
end
|
||||
|
||||
def sha
|
||||
sha = `git rev-parse HEAD`
|
||||
@metrics['Git SHA'] = sha.presence || 'n/a'
|
||||
@metrics['Git SHA'] = GIT_HASH
|
||||
end
|
||||
|
||||
def postgres_status
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
class SuperAdmin::SettingsController < SuperAdmin::ApplicationController
|
||||
def show; end
|
||||
|
||||
def refresh
|
||||
Internal::CheckNewVersionsJob.perform_now
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
redirect_to super_admin_settings_path, notice: 'Instance status refreshed'
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class Twilio::DeliveryStatusController < ApplicationController
|
||||
def create
|
||||
::Twilio::DeliveryStatusService.new(params: permitted_params).perform
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(
|
||||
:AccountSid,
|
||||
:From,
|
||||
:MessageSid,
|
||||
:MessagingServiceSid,
|
||||
:MessageStatus,
|
||||
:ErrorCode,
|
||||
:ErrorMessage
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -30,11 +30,7 @@ class AccessTokenDashboard < Administrate::BaseDashboard
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
owner
|
||||
id
|
||||
token
|
||||
created_at
|
||||
updated_at
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
|
||||
@@ -46,6 +46,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
name
|
||||
description
|
||||
outgoing_url
|
||||
access_token
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
|
||||
@@ -32,6 +32,7 @@ class PlatformAppDashboard < Administrate::BaseDashboard
|
||||
name
|
||||
created_at
|
||||
updated_at
|
||||
access_token
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
|
||||
@@ -36,7 +36,8 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
updated_at: Field::DateTime,
|
||||
pubsub_token: Field::String,
|
||||
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
|
||||
accounts: CountField
|
||||
accounts: CountField,
|
||||
access_token: Field::HasOne
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
@@ -67,6 +68,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
updated_at
|
||||
confirmed_at
|
||||
account_users
|
||||
access_token
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
|
||||
@@ -18,4 +18,9 @@ class ContactDrop < BaseDrop
|
||||
def last_name
|
||||
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
||||
end
|
||||
|
||||
def custom_attribute
|
||||
custom_attributes = @obj.try(:custom_attributes) || {}
|
||||
custom_attributes.transform_keys(&:to_s)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,6 +19,11 @@ class ConversationDrop < BaseDrop
|
||||
end
|
||||
end
|
||||
|
||||
def custom_attribute
|
||||
custom_attributes = @obj.try(:custom_attributes) || {}
|
||||
custom_attributes.transform_keys(&:to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_sender_name(sender)
|
||||
|
||||
@@ -3,13 +3,21 @@ class ConversationFinder
|
||||
|
||||
DEFAULT_STATUS = 'open'.freeze
|
||||
SORT_OPTIONS = {
|
||||
latest: 'latest',
|
||||
sort_on_created_at: 'sort_on_created_at',
|
||||
last_user_message_at: 'last_user_message_at',
|
||||
sort_on_priority: 'sort_on_priority',
|
||||
sort_on_waiting_since: 'sort_on_waiting_since'
|
||||
}.with_indifferent_access
|
||||
'last_activity_at_asc' => %w[sort_on_last_activity_at asc],
|
||||
'last_activity_at_desc' => %w[sort_on_last_activity_at desc],
|
||||
'created_at_asc' => %w[sort_on_created_at asc],
|
||||
'created_at_desc' => %w[sort_on_created_at desc],
|
||||
'priority_asc' => %w[sort_on_priority asc],
|
||||
'priority_desc' => %w[sort_on_priority desc],
|
||||
'waiting_since_asc' => %w[sort_on_waiting_since asc],
|
||||
'waiting_since_desc' => %w[sort_on_waiting_since desc],
|
||||
|
||||
# To be removed in v3.5.0
|
||||
'latest' => %w[sort_on_last_activity_at desc],
|
||||
'sort_on_created_at' => %w[sort_on_created_at asc],
|
||||
'sort_on_priority' => %w[sort_on_priority desc],
|
||||
'sort_on_waiting_since' => %w[sort_on_waiting_since asc]
|
||||
}.with_indifferent_access
|
||||
# assumptions
|
||||
# inbox_id if not given, take from all conversations, else specific to inbox
|
||||
# assignee_type if not given, take 'all'
|
||||
@@ -159,7 +167,14 @@ class ConversationFinder
|
||||
@conversations = @conversations.includes(
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||
)
|
||||
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
|
||||
@conversations.send(sort_by).page(current_page)
|
||||
|
||||
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
|
||||
@conversations = @conversations.send(sort_by, sort_order)
|
||||
|
||||
if params[:updated_within].present?
|
||||
@conversations.where('conversations.updated_at > ?', Time.zone.now - params[:updated_within].to_i.seconds)
|
||||
else
|
||||
@conversations.page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
class NotificationFinder
|
||||
attr_reader :current_user, :current_account, :params
|
||||
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
def initialize(current_user, current_account, params = {})
|
||||
@current_user = current_user
|
||||
@current_account = current_account
|
||||
@params = params
|
||||
set_up
|
||||
end
|
||||
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: sort_order)
|
||||
end
|
||||
|
||||
def unread_count
|
||||
@notifications.where(read_at: nil).count
|
||||
end
|
||||
|
||||
def count
|
||||
@notifications.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_up
|
||||
find_all_notifications
|
||||
filter_snoozed_notifications
|
||||
fitler_read_notifications
|
||||
end
|
||||
|
||||
def find_all_notifications
|
||||
@notifications = current_user.notifications.where(account_id: @current_account.id)
|
||||
end
|
||||
|
||||
def filter_snoozed_notifications
|
||||
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
|
||||
end
|
||||
|
||||
def fitler_read_notifications
|
||||
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
|
||||
end
|
||||
|
||||
def type_included?(type)
|
||||
(params[:includes] || []).include?(type)
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def sort_order
|
||||
params[:sort_order] || :desc
|
||||
end
|
||||
end
|
||||
@@ -53,7 +53,7 @@ module Api::V1::InboxesHelper
|
||||
rescue StandardError => e
|
||||
raise StandardError, e.message
|
||||
ensure
|
||||
ChatwootExceptionTracker.new(e).capture_exception if e.present?
|
||||
Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present?
|
||||
end
|
||||
|
||||
def check_smtp_connection(channel_data, smtp)
|
||||
|
||||
@@ -15,8 +15,8 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
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)
|
||||
# e.g. ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03']
|
||||
result_arr << (['Start of the hour'] + 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
|
||||
@@ -25,7 +25,7 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
# 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]
|
||||
row = [format('%02d:00', 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
|
||||
@@ -37,7 +37,7 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
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]
|
||||
# row will look like ['22:00', 0, 0, 1, 4, 6, 7, 4]
|
||||
# add the row to the result array
|
||||
|
||||
result_arr << row
|
||||
@@ -46,12 +46,12 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
# 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],
|
||||
# ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'],
|
||||
# ['00:00', 0, 0, 0],
|
||||
# ['01:00', 0, 0, 0],
|
||||
# ['02:00', 0, 0, 0],
|
||||
# ['03:00', 0, 0, 0],
|
||||
# ['04:00', 0, 0, 0],
|
||||
# ]
|
||||
result_arr
|
||||
end
|
||||
|
||||
@@ -37,7 +37,7 @@ module Api::V2::Accounts::ReportsHelper
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||
}
|
||||
)
|
||||
).summary
|
||||
).short_summary
|
||||
end
|
||||
|
||||
private
|
||||
@@ -45,12 +45,8 @@ module Api::V2::Accounts::ReportsHelper
|
||||
def generate_readable_report_metrics(report_metric)
|
||||
[
|
||||
report_metric[:conversations_count],
|
||||
time_to_minutes(report_metric[:avg_first_response_time]),
|
||||
time_to_minutes(report_metric[:avg_resolution_time])
|
||||
Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format
|
||||
]
|
||||
end
|
||||
|
||||
def time_to_minutes(time_in_seconds)
|
||||
(time_in_seconds / 60).to_i
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
module ContactHelper
|
||||
def parse_name(full_name)
|
||||
# If the input is nil or not a string, return a hash with all values set to nil
|
||||
return default_name_hash if invalid_name?(full_name)
|
||||
|
||||
# If the input is a number, return a hash with the number as the first name
|
||||
return numeric_name_hash(full_name) if valid_number?(full_name)
|
||||
|
||||
full_name = full_name.squish
|
||||
|
||||
# If full name consists of only one word, consider it as the first name
|
||||
return single_word_name_hash(full_name) if single_word?(full_name)
|
||||
|
||||
parts = split_name(full_name)
|
||||
parts = handle_conjunction(parts)
|
||||
build_name_hash(parts)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_name_hash
|
||||
{ first_name: nil, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
|
||||
end
|
||||
|
||||
def invalid_name?(full_name)
|
||||
!full_name.is_a?(String) || full_name.empty?
|
||||
end
|
||||
|
||||
def numeric_name_hash(full_name)
|
||||
{ first_name: full_name, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
|
||||
end
|
||||
|
||||
def valid_number?(full_name)
|
||||
full_name.gsub(/\s+/, '').match?(/\A\+?\d+\z/)
|
||||
end
|
||||
|
||||
def single_word_name_hash(full_name)
|
||||
{ first_name: full_name, last_name: nil, middle_name: nil, prefix: nil, suffix: nil }
|
||||
end
|
||||
|
||||
def single_word?(full_name)
|
||||
full_name.split.size == 1
|
||||
end
|
||||
|
||||
def split_name(full_name)
|
||||
full_name.split
|
||||
end
|
||||
|
||||
def handle_conjunction(parts)
|
||||
conjunctions = ['and', '&']
|
||||
parts.each_index do |i|
|
||||
next unless conjunctions.include?(parts[i]) && i.positive?
|
||||
|
||||
parts[i - 1] = [parts[i - 1], parts[i + 1]].join(' ')
|
||||
parts.delete_at(i)
|
||||
parts.delete_at(i)
|
||||
end
|
||||
parts
|
||||
end
|
||||
|
||||
def build_name_hash(parts)
|
||||
suffix = parts.pop if parts.last.match?(/(\w+\.|[IVXLM]+|[A-Z]+)$/)
|
||||
last_name = parts.pop
|
||||
prefix = parts.shift if parts.first.match?(/^\w+\./)
|
||||
first_name = parts.shift
|
||||
middle_name = parts.join(' ')
|
||||
|
||||
hash = {
|
||||
first_name: first_name,
|
||||
last_name: last_name,
|
||||
prefix: prefix,
|
||||
middle_name: middle_name,
|
||||
suffix: suffix
|
||||
}
|
||||
|
||||
# Reverse name if "," was used in Last, First notation.
|
||||
if hash[:first_name] =~ /,$/
|
||||
hash[:first_name] = hash[:last_name]
|
||||
hash[:last_name] = Regexp.last_match.pre_match
|
||||
end
|
||||
hash
|
||||
end
|
||||
end
|
||||
@@ -1,11 +1,71 @@
|
||||
module PortalHelper
|
||||
def generate_portal_bg_color(portal_color, theme)
|
||||
base_color = theme == 'dark' ? 'black' : 'white'
|
||||
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
|
||||
"color-mix(in srgb, #{portal_color} 20%, #{base_color})"
|
||||
end
|
||||
|
||||
def generate_portal_bg(portal_color, theme)
|
||||
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
|
||||
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
|
||||
"url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||
end
|
||||
|
||||
def generate_gradient_to_bottom(theme)
|
||||
base_color = theme == 'dark' ? '#151718' : 'white'
|
||||
"linear-gradient(to bottom, transparent, #{base_color})"
|
||||
end
|
||||
|
||||
def generate_portal_hover_color(portal_color, theme)
|
||||
base_color = theme == 'dark' ? '#1B1B1B' : '#F9F9F9'
|
||||
"color-mix(in srgb, #{portal_color} 5%, #{base_color})"
|
||||
end
|
||||
|
||||
def language_name(locale)
|
||||
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
|
||||
language_map[locale] || locale
|
||||
end
|
||||
|
||||
def theme_query_string(theme)
|
||||
theme.present? && theme != 'system' ? "?theme=#{theme}" : ''
|
||||
end
|
||||
|
||||
def generate_home_link(portal_slug, portal_locale, theme, is_plain_layout_enabled)
|
||||
if is_plain_layout_enabled
|
||||
"/hc/#{portal_slug}/#{portal_locale}#{theme_query_string(theme)}"
|
||||
else
|
||||
"/hc/#{portal_slug}/#{portal_locale}"
|
||||
end
|
||||
end
|
||||
|
||||
def generate_category_link(params)
|
||||
portal_slug = params[:portal_slug]
|
||||
category_locale = params[:category_locale]
|
||||
category_slug = params[:category_slug]
|
||||
theme = params[:theme]
|
||||
is_plain_layout_enabled = params[:is_plain_layout_enabled]
|
||||
|
||||
if is_plain_layout_enabled
|
||||
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{theme_query_string(theme)}"
|
||||
else
|
||||
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}"
|
||||
end
|
||||
end
|
||||
|
||||
def generate_article_link(portal_slug, article_slug, theme, is_plain_layout_enabled)
|
||||
if is_plain_layout_enabled
|
||||
"/hc/#{portal_slug}/articles/#{article_slug}#{theme_query_string(theme)}"
|
||||
else
|
||||
"/hc/#{portal_slug}/articles/#{article_slug}"
|
||||
end
|
||||
end
|
||||
|
||||
def render_category_content(content)
|
||||
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
|
||||
end
|
||||
|
||||
def thumbnail_bg_color(username)
|
||||
colors = ['#6D95BA', '#A4C3C3', '#E19191']
|
||||
return colors.sample if username.blank?
|
||||
|
||||
colors[username.length % colors.size]
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user