Compare commits
1057 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
371ec9b8d5 | ||
|
|
5e288d57ea | ||
|
|
61bd5b3484 | ||
|
|
4ed1335f89 | ||
|
|
4274c7afdb | ||
|
|
27f67142c8 | ||
|
|
ac8aba6ba4 | ||
|
|
07ff61c070 | ||
|
|
3645ac6704 | ||
|
|
9764719245 | ||
|
|
b3aeed7fc1 | ||
|
|
5ae5d8546e | ||
|
|
d190d8b1af | ||
|
|
c45538684d | ||
|
|
575e7bf4c4 | ||
|
|
a4338d8472 | ||
|
|
7d542cf115 | ||
|
|
9c9a542bbe | ||
|
|
b6c56551bc | ||
|
|
1379228196 | ||
|
|
d6e19b9273 | ||
|
|
5e0e4051dc | ||
|
|
8caf3a0a7b | ||
|
|
96ae97664c | ||
|
|
901b121695 | ||
|
|
3e88c9f0c7 | ||
|
|
94f5c130bf | ||
|
|
eb04c3f113 | ||
|
|
8ece6fb998 | ||
|
|
794213b5c6 | ||
|
|
ef4a9ab66b | ||
|
|
86e978faad | ||
|
|
d5d8f2a318 | ||
|
|
4ca1fad335 | ||
|
|
0edf6bdcb1 | ||
|
|
c2839ddd56 | ||
|
|
28581f88b2 | ||
|
|
833c625d18 | ||
|
|
6e2a3a410a | ||
|
|
3e85af9589 | ||
|
|
589dec52a3 | ||
|
|
a5477509bd | ||
|
|
5d951a96b5 | ||
|
|
76552f6ae5 | ||
|
|
e153515847 | ||
|
|
3bc692d894 | ||
|
|
73360d66cd | ||
|
|
04575d8051 | ||
|
|
9aba51cb45 | ||
|
|
1172b6c871 | ||
|
|
240f01f378 | ||
|
|
5975117636 | ||
|
|
51b285f665 | ||
|
|
b3958d4735 | ||
|
|
128119d494 | ||
|
|
324dc01699 | ||
|
|
f11d468e31 | ||
|
|
aa5ed13752 | ||
|
|
10ce7da18d | ||
|
|
73bd55dfac | ||
|
|
db10f81d53 | ||
|
|
717aac4438 | ||
|
|
077a464481 | ||
|
|
9bf18592fc | ||
|
|
677e196c96 | ||
|
|
19e6896f0d | ||
|
|
ba537baab2 | ||
|
|
2b8b6b086b | ||
|
|
cf3ec2b601 | ||
|
|
35eaa7852c | ||
|
|
b87558d301 | ||
|
|
9f1003e94e | ||
|
|
73c003907b | ||
|
|
b65d292af4 | ||
|
|
43bdbf1018 | ||
|
|
ec7986420d | ||
|
|
0b23153316 | ||
|
|
ad4f5e5e65 | ||
|
|
1d48532146 | ||
|
|
950803b2aa | ||
|
|
a41d92f4da | ||
|
|
70d4eb393f | ||
|
|
9f162127d0 | ||
|
|
4cd30dabc4 | ||
|
|
8f78728863 | ||
|
|
e7c5d33d50 | ||
|
|
5400f31acb | ||
|
|
e58f1d778e | ||
|
|
5f5db2011e | ||
|
|
aea4d89f62 | ||
|
|
ccda17d704 | ||
|
|
4ab6c438cf | ||
|
|
219e63c226 | ||
|
|
ee1e4623a3 | ||
|
|
5982212903 | ||
|
|
32e6ecb12d | ||
|
|
3b774558f4 | ||
|
|
072171da18 | ||
|
|
8292221790 | ||
|
|
901954de33 | ||
|
|
286fe03500 | ||
|
|
9c4f02634b | ||
|
|
434f39de1f | ||
|
|
e72b543f38 | ||
|
|
826e818802 | ||
|
|
cbf846d32f | ||
|
|
55811a39a5 | ||
|
|
a81f5953fc | ||
|
|
bd09328ec2 | ||
|
|
17ecc88f80 | ||
|
|
7bc4c7b36e | ||
|
|
79189ac5d7 | ||
|
|
060c4e6e72 | ||
|
|
8f7c518487 | ||
|
|
12761cbfce | ||
|
|
2b30c273a4 | ||
|
|
03684a893d | ||
|
|
fef27111b9 | ||
|
|
b04ed66686 | ||
|
|
f5df8bca20 | ||
|
|
060af66c09 | ||
|
|
a6adbd61db | ||
|
|
bab58054f7 | ||
|
|
e27e990cd6 | ||
|
|
196c2e0ed8 | ||
|
|
499bd4328a | ||
|
|
e389047aaf | ||
|
|
da04ff284b | ||
|
|
10b48aed97 | ||
|
|
ac6e9ae994 | ||
|
|
7dd589fb6c | ||
|
|
6b930058d1 | ||
|
|
aef3495a79 | ||
|
|
da341a95c6 | ||
|
|
bca5ec6482 | ||
|
|
f0a0cb7269 | ||
|
|
238b7618b4 | ||
|
|
4f206f67c9 | ||
|
|
fa513bf784 | ||
|
|
a68b0b3878 | ||
|
|
249489e697 | ||
|
|
e2c67d7dae | ||
|
|
703bc310a7 | ||
|
|
4caecfa680 | ||
|
|
fd4fde2543 | ||
|
|
763c5de03f | ||
|
|
4e160a46dd | ||
|
|
56aaf18663 | ||
|
|
df841aed27 | ||
|
|
d4372a0332 | ||
|
|
1595da789d | ||
|
|
7e65cb1d19 | ||
|
|
a931ee7afb | ||
|
|
bc6944736e | ||
|
|
16c2e28e9c | ||
|
|
ab89ef8db3 | ||
|
|
9ef14d11f8 | ||
|
|
e753990da3 | ||
|
|
f469c2e65d | ||
|
|
0525501b87 | ||
|
|
3a37fd9d32 | ||
|
|
b095c9dfb9 | ||
|
|
cd00effcfe | ||
|
|
1cd0334ccd | ||
|
|
3b1a16844e | ||
|
|
6995e8a451 | ||
|
|
1dd0f319fc | ||
|
|
5ce96369cf | ||
|
|
0791d78e28 | ||
|
|
cecbb7c34e | ||
|
|
525daff5fe | ||
|
|
b2c51b4b8c | ||
|
|
ffddb05ba3 | ||
|
|
4bb81b9a41 | ||
|
|
3df4e8d2ba | ||
|
|
3edef873bc | ||
|
|
c04b5cb851 | ||
|
|
fabd717d92 | ||
|
|
945bcf5fad | ||
|
|
d35d755379 | ||
|
|
a2622cb38e | ||
|
|
b58fd78450 | ||
|
|
e8d32066b4 | ||
|
|
6d3779eb83 | ||
|
|
c7bed04c80 | ||
|
|
b0e956cfa9 | ||
|
|
8608b7cded | ||
|
|
23f1b4ac03 | ||
|
|
27900c214f | ||
|
|
704701e251 | ||
|
|
34c53d352a | ||
|
|
32026d1fd4 | ||
|
|
35cdce0d52 | ||
|
|
e59098cf61 | ||
|
|
d8ca480b19 | ||
|
|
803b123ade | ||
|
|
cffb673fba | ||
|
|
1f6535d61b | ||
|
|
8a5ebe83a3 | ||
|
|
cc17d61016 | ||
|
|
0547ff719c | ||
|
|
c130846fe8 | ||
|
|
b3adde3a7a | ||
|
|
54603002a6 | ||
|
|
b995cdfc32 | ||
|
|
b09546577a | ||
|
|
6e84c1dc4c | ||
|
|
f41f3aaba8 | ||
|
|
8fa61e4632 | ||
|
|
7dacd752d3 | ||
|
|
7439d2401d | ||
|
|
dfb1ee0c56 | ||
|
|
3da73b821d | ||
|
|
66b82ac10a | ||
|
|
058acc5042 | ||
|
|
cdf822291f | ||
|
|
3f9e872e1c | ||
|
|
94aa3067f6 | ||
|
|
bff8064597 | ||
|
|
6b2d4e2585 | ||
|
|
56dfc2ae82 | ||
|
|
535d5ee47f | ||
|
|
838905f3dd | ||
|
|
59f5208c5c | ||
|
|
32a1a715ea | ||
|
|
3755d3870e | ||
|
|
0edc8a9284 | ||
|
|
5449d63602 | ||
|
|
dfa72fd6af | ||
|
|
0e50da324b | ||
|
|
679f8118f6 | ||
|
|
804d177782 | ||
|
|
eb96c9fece | ||
|
|
ed6c50621c | ||
|
|
7f74de07ed | ||
|
|
9e5bf93580 | ||
|
|
9d685da12d | ||
|
|
6bb40eb502 | ||
|
|
1ceee572cf | ||
|
|
391ffea4ab | ||
|
|
5292e569d9 | ||
|
|
82e111f1be | ||
|
|
1e9b3a1e42 | ||
|
|
35520d85a2 | ||
|
|
e19e37eef4 | ||
|
|
814795f566 | ||
|
|
1a633dcf10 | ||
|
|
c9b0e66641 | ||
|
|
440ff2f3ea | ||
|
|
96be63f50b | ||
|
|
69a323691f | ||
|
|
4357fcf7ef | ||
|
|
5f79513617 | ||
|
|
ba3ccbbc9a | ||
|
|
f182436673 | ||
|
|
793b907fe6 | ||
|
|
c75dfcd499 | ||
|
|
29a48f7914 | ||
|
|
095a8aa9dd | ||
|
|
2d8b5f04e9 | ||
|
|
ba9f97bc3e | ||
|
|
ed51f44ee8 | ||
|
|
933d787108 | ||
|
|
0bc12733a3 | ||
|
|
8cfd9c44ab | ||
|
|
3ddff84be0 | ||
|
|
afb5361a1b | ||
|
|
f376047632 | ||
|
|
7373eea842 | ||
|
|
a446df4620 | ||
|
|
306c6dd21d | ||
|
|
380ba8860c | ||
|
|
6c8c8ddcfc | ||
|
|
0c09c5e140 | ||
|
|
b761fba8cb | ||
|
|
dfceda3942 | ||
|
|
85d1825236 | ||
|
|
8f8c7e26c7 | ||
|
|
181768d91f | ||
|
|
2dcd4d8fd3 | ||
|
|
d909550134 | ||
|
|
c564ec41e2 | ||
|
|
af94a0e174 | ||
|
|
fcd8815fca | ||
|
|
5bd3f28117 | ||
|
|
384bde333e | ||
|
|
d06ec604b9 | ||
|
|
d8ce23f2a0 | ||
|
|
3ccb983377 | ||
|
|
19fb9fcd31 | ||
|
|
5f4a1b96ce | ||
|
|
6983f385fc | ||
|
|
2aadd1cac5 | ||
|
|
bfa7d429bd | ||
|
|
3238150b92 | ||
|
|
3603571967 | ||
|
|
7c2a8c0abb | ||
|
|
8b5f73badd | ||
|
|
ff57fd3d23 | ||
|
|
49aa1ea17c | ||
|
|
7430897085 | ||
|
|
82894a1c4f | ||
|
|
3bb4217a45 | ||
|
|
dfc8330035 | ||
|
|
45e03d87c7 | ||
|
|
3e358e5d26 | ||
|
|
dc5dae04eb | ||
|
|
9128b1f47d | ||
|
|
16a8226ba7 | ||
|
|
9ecaf3199d | ||
|
|
a44cd0373e | ||
|
|
2c0b629302 | ||
|
|
89a37a1771 | ||
|
|
cadc038966 | ||
|
|
07e8449379 | ||
|
|
71e908ad1a | ||
|
|
035c85b775 | ||
|
|
a87b753151 | ||
|
|
edaa4aff7e | ||
|
|
be02610349 | ||
|
|
1f65731165 | ||
|
|
fb61fb7849 | ||
|
|
060a945aea | ||
|
|
2797250f34 | ||
|
|
97b3f9b3c7 | ||
|
|
9363301d2a | ||
|
|
d93a826e28 | ||
|
|
244fe0835e | ||
|
|
5f1a5d6589 | ||
|
|
4e27c22292 | ||
|
|
53ee270096 | ||
|
|
a00ad20c08 | ||
|
|
e8764dd1c6 | ||
|
|
a869d38499 | ||
|
|
7bf7c96587 | ||
|
|
72b857a92f | ||
|
|
380d6a43a5 | ||
|
|
076f2b492e | ||
|
|
547f981c47 | ||
|
|
8bfc62a3b2 | ||
|
|
d39776a314 | ||
|
|
35641d0543 | ||
|
|
038cd6f149 | ||
|
|
38978dd447 | ||
|
|
fb24b7eaa7 | ||
|
|
b4ce45bc4b | ||
|
|
8d04198309 | ||
|
|
da796347c4 | ||
|
|
2d6a29664a | ||
|
|
4ba5cfceaf | ||
|
|
7cc324e1c0 | ||
|
|
cf89601269 | ||
|
|
c07e23bf8d | ||
|
|
5aa89d85f3 | ||
|
|
4ed1edf53d | ||
|
|
1be1326b52 | ||
|
|
42ae7d1568 | ||
|
|
b781c83545 | ||
|
|
e48cea18e7 | ||
|
|
ff82987144 | ||
|
|
f612a45550 | ||
|
|
182dce4840 | ||
|
|
4e41e072d6 | ||
|
|
a369c16db8 | ||
|
|
1fc820787a | ||
|
|
d3a83ba89e | ||
|
|
20fb66e2f7 | ||
|
|
a44646161b | ||
|
|
87027ea2d0 | ||
|
|
c9757cbb4b | ||
|
|
9a8f4aefe0 | ||
|
|
3545b80050 | ||
|
|
379855714e | ||
|
|
ff06cd7643 | ||
|
|
ade3952016 | ||
|
|
f246516a6e | ||
|
|
c296bf4178 | ||
|
|
1568554a1c | ||
|
|
ae66be197e | ||
|
|
3e904aa160 | ||
|
|
64c1440c46 | ||
|
|
f069a41390 | ||
|
|
d4a33e2290 | ||
|
|
7ee5bcecff | ||
|
|
48f6ee8846 | ||
|
|
7a24f52782 | ||
|
|
324d46120b | ||
|
|
87baec5ff8 | ||
|
|
b2e144f35c | ||
|
|
87a8e25662 | ||
|
|
8e88f00fb2 | ||
|
|
d14505d59a | ||
|
|
1e6d4347fa | ||
|
|
b8d9a8c072 | ||
|
|
fd15ae5e8c | ||
|
|
fb6377414b | ||
|
|
9a5dbe055e | ||
|
|
42dd280aca | ||
|
|
41b2946cdc | ||
|
|
a90f0f2c59 | ||
|
|
4222c0e53b | ||
|
|
ee0f0f0be0 | ||
|
|
f8d874453c | ||
|
|
aa891489f0 | ||
|
|
4c69b059d4 | ||
|
|
359bd9f762 | ||
|
|
2de0b61726 | ||
|
|
d75163aa57 | ||
|
|
e49f30641e | ||
|
|
1631c2c342 | ||
|
|
cf7de369b2 | ||
|
|
78c03d8f2f | ||
|
|
876320b849 | ||
|
|
a1d13f8ff3 | ||
|
|
3c19bdfaa9 | ||
|
|
4fa895086e | ||
|
|
9945d8debb | ||
|
|
4362de2198 | ||
|
|
a5c5879e4f | ||
|
|
1a57f4f33d | ||
|
|
a99e173168 | ||
|
|
e02a28f61e | ||
|
|
26d3ff97ce | ||
|
|
57fb3c9785 | ||
|
|
edeb970a82 | ||
|
|
e17baddf01 | ||
|
|
a277d36696 | ||
|
|
6c9e86e17a | ||
|
|
e75ef21eb6 | ||
|
|
04e5443b82 | ||
|
|
8b4cdf3b9b | ||
|
|
37f1620f7c | ||
|
|
b0a0e805cf | ||
|
|
c619e253a2 | ||
|
|
2bd111f1e2 | ||
|
|
40174b50eb | ||
|
|
e0fe28717f | ||
|
|
ac5fc22043 | ||
|
|
e30f196dad | ||
|
|
9e4e1ce8ec | ||
|
|
f710898844 | ||
|
|
94633484ca | ||
|
|
0817c2589f | ||
|
|
8a99386b33 | ||
|
|
52d6a563d6 | ||
|
|
d0a5ae1da4 | ||
|
|
783c00a1d9 | ||
|
|
bc70ec8b07 | ||
|
|
50e1efe5d7 | ||
|
|
d8629e53f1 | ||
|
|
cc9df1dabb | ||
|
|
cd6cb8182e | ||
|
|
5b0e90e5b9 | ||
|
|
3c3bbc84b3 | ||
|
|
23615cff4f | ||
|
|
daadc6cb68 | ||
|
|
e157a2a36b | ||
|
|
d8d7debfee | ||
|
|
a82b206fe6 | ||
|
|
a4416214c8 | ||
|
|
8fe75cd210 | ||
|
|
303effebbc | ||
|
|
f32a34190d | ||
|
|
8588ef1d8a | ||
|
|
51ec4821f3 | ||
|
|
957033a7bb | ||
|
|
62c74deac3 | ||
|
|
29fd448998 | ||
|
|
e55cb08a6a | ||
|
|
047359e8dc | ||
|
|
eb814e181a | ||
|
|
857031ff5a | ||
|
|
d5eeb68714 | ||
|
|
966b287026 | ||
|
|
a348729109 | ||
|
|
1f29b7733e | ||
|
|
e26ae30f6f | ||
|
|
547943a05c | ||
|
|
46aa229531 | ||
|
|
28bd796289 | ||
|
|
6a3f82ed7e | ||
|
|
523f3301c0 | ||
|
|
f085343a99 | ||
|
|
f76a924700 | ||
|
|
f8e3b76a4a | ||
|
|
b8f1e8a7ef | ||
|
|
d007fc49d8 | ||
|
|
f6d8ebd8d3 | ||
|
|
6ff9c4578a | ||
|
|
33acfe1464 | ||
|
|
f5eeb16bb1 | ||
|
|
c35c5faaa4 | ||
|
|
8425ebc13f | ||
|
|
6fc37a4298 | ||
|
|
99f3e77c12 | ||
|
|
a9c087c45f | ||
|
|
3f4333087f | ||
|
|
e1ac29683d | ||
|
|
5c74cbfe19 | ||
|
|
3fdb3fa673 | ||
|
|
ba584974cb | ||
|
|
bddd6408ac | ||
|
|
413ad66a07 | ||
|
|
0ef5d884cc | ||
|
|
f6b6d23e93 | ||
|
|
50be69f3d3 | ||
|
|
5a75e4d5e6 | ||
|
|
ec463df9d6 | ||
|
|
b648334323 | ||
|
|
916972aeb1 | ||
|
|
3893e01af9 | ||
|
|
7835f32c8b | ||
|
|
37302244ee | ||
|
|
f1be7ddb83 | ||
|
|
7447a65a83 | ||
|
|
129009d602 | ||
|
|
c656bd6f4b | ||
|
|
36528d7484 | ||
|
|
f9c85b6a67 | ||
|
|
4dfe6bdbe8 | ||
|
|
ee343f2fa1 | ||
|
|
1a1f5f85b2 | ||
|
|
bd64b0c884 | ||
|
|
e1c8928ed9 | ||
|
|
1f98940445 | ||
|
|
707aa22a6d | ||
|
|
32da15fa8a | ||
|
|
97cd6e289a | ||
|
|
def6576fb9 | ||
|
|
196c10b253 | ||
|
|
cfdca38d59 | ||
|
|
ccd90a69ee | ||
|
|
c16f962d2b | ||
|
|
8fccf69ceb | ||
|
|
adc8833670 | ||
|
|
ecbf90ded8 | ||
|
|
c5d2d7782a | ||
|
|
72dae22ef4 | ||
|
|
9a9cd41009 | ||
|
|
0c4c8162c2 | ||
|
|
d47cc5d5f4 | ||
|
|
cb942e512d | ||
|
|
bd0a479645 | ||
|
|
cd0da914f4 | ||
|
|
7a7e72897a | ||
|
|
c1542054c9 | ||
|
|
250e67e7ae | ||
|
|
cdbe839b35 | ||
|
|
240a77dcce | ||
|
|
1dc5bb8bd1 | ||
|
|
23534da27d | ||
|
|
8652d4031c | ||
|
|
384e311c7a | ||
|
|
2791f88b4c | ||
|
|
402d5af19a | ||
|
|
b554d8c19c | ||
|
|
7d9dd64303 | ||
|
|
41bea8931f | ||
|
|
f83d8de476 | ||
|
|
af2a652098 | ||
|
|
a3c911263d | ||
|
|
836c677837 | ||
|
|
7e4dbfdd7e | ||
|
|
6eda556242 | ||
|
|
3ea454c7ed | ||
|
|
9123d7014d | ||
|
|
3ea3abe81a | ||
|
|
7289c3d7f9 | ||
|
|
d00e1df29c | ||
|
|
16a18c4f22 | ||
|
|
30b156d92f | ||
|
|
931f9d33e4 | ||
|
|
e61fe4d092 | ||
|
|
5bc33ac654 | ||
|
|
878ba5a869 | ||
|
|
92632b2b96 | ||
|
|
d803a9e298 | ||
|
|
b9f67533dd | ||
|
|
7ade78bedf | ||
|
|
b4eca48f4d | ||
|
|
2cf8e317fa | ||
|
|
821a422ab5 | ||
|
|
91c7b4f2cd | ||
|
|
adb43ec5b3 | ||
|
|
c9721d7bc9 | ||
|
|
bbc2b8a396 | ||
|
|
54cfa67d52 | ||
|
|
9b72b3e332 | ||
|
|
c03919be2d | ||
|
|
706cc6f49c | ||
|
|
10c7e81e02 | ||
|
|
03637b2d4d | ||
|
|
b7218a05be | ||
|
|
a2cd57d9c6 | ||
|
|
001849eeaa | ||
|
|
bf09a70096 | ||
|
|
530aec92a9 | ||
|
|
deb8f2a0b7 | ||
|
|
5c247e3d2c | ||
|
|
04b9a070c4 | ||
|
|
dd2caf720c | ||
|
|
0d16a7aab0 | ||
|
|
31325d0999 | ||
|
|
6f99784224 | ||
|
|
201e6f7e7b | ||
|
|
c4d41134b8 | ||
|
|
680c92ecec | ||
|
|
deb07d2b7f | ||
|
|
3a14fc373a | ||
|
|
29e429a02e | ||
|
|
907a0ee135 | ||
|
|
22a03f77ab | ||
|
|
9761b10bf6 | ||
|
|
c364d3fdca | ||
|
|
07ad5756eb | ||
|
|
6e401eecde | ||
|
|
f32e259d2f | ||
|
|
83ed0e6454 | ||
|
|
da568e4ea5 | ||
|
|
ad819bf3ba | ||
|
|
b502ebd23a | ||
|
|
7c5d94c19e | ||
|
|
a16b5f4644 | ||
|
|
f3cb8c531b | ||
|
|
469e696ab7 | ||
|
|
d99ccd9df6 | ||
|
|
3b3118d764 | ||
|
|
84386847e2 | ||
|
|
0da3d100b3 | ||
|
|
56d621bab8 | ||
|
|
f1571b5f66 | ||
|
|
55f8e179af | ||
|
|
ab5289a136 | ||
|
|
e05c48979d | ||
|
|
24c880343b | ||
|
|
b3b4ee7a28 | ||
|
|
e26b440a66 | ||
|
|
d6194316e1 | ||
|
|
341148612f | ||
|
|
79864e97d6 | ||
|
|
ed5e66e430 | ||
|
|
074a861fb4 | ||
|
|
b88656829e | ||
|
|
aefe6a5943 | ||
|
|
3d743f8498 | ||
|
|
0186fff67d | ||
|
|
38f61cdf75 | ||
|
|
84366002db | ||
|
|
eff5bb74b5 | ||
|
|
2614088fae | ||
|
|
3699e04db9 | ||
|
|
0dca009c01 | ||
|
|
1b39eb1a23 | ||
|
|
2637aebb7f | ||
|
|
8afcfde078 | ||
|
|
9ea1eaf3ed | ||
|
|
66d06afaf7 | ||
|
|
cffcca9722 | ||
|
|
fe2b9774d8 | ||
|
|
3d02fabef4 | ||
|
|
f89c2b1f63 | ||
|
|
ee755d5a6c | ||
|
|
65e2ecf88e | ||
|
|
332ec69ee8 | ||
|
|
1ce30f8431 | ||
|
|
3ae6944307 | ||
|
|
52533d4b38 | ||
|
|
38409d9336 | ||
|
|
d344e513c4 | ||
|
|
9af7f67930 | ||
|
|
f95d938fb6 | ||
|
|
f74a7e87bd | ||
|
|
d3fce5fc89 | ||
|
|
14f3f3d2ac | ||
|
|
127d5b97c4 | ||
|
|
7ef1c097e8 | ||
|
|
80e3116cd8 | ||
|
|
46bac55bb3 | ||
|
|
2cadafd5c1 | ||
|
|
bb27dca21c | ||
|
|
86fc9fbffa | ||
|
|
9bdbfc6f4f | ||
|
|
28a7d9c62b | ||
|
|
2ca344b17c | ||
|
|
1bf2278f31 | ||
|
|
6188ab0d30 | ||
|
|
f016b53013 | ||
|
|
127cd3e843 | ||
|
|
457dbe5831 | ||
|
|
8d73fb1161 | ||
|
|
af5746bb39 | ||
|
|
3f27d018c7 | ||
|
|
68402393b5 | ||
|
|
2ad33857f4 | ||
|
|
d3c7677dc7 | ||
|
|
73e92f9ef5 | ||
|
|
fcc8748daf | ||
|
|
312ee249b6 | ||
|
|
e4548f6961 | ||
|
|
1b93aac8c5 | ||
|
|
e7ed666037 | ||
|
|
85ca0683ed | ||
|
|
78689a23e5 | ||
|
|
ba63a55883 | ||
|
|
dc3d59bae1 | ||
|
|
0a851b935e | ||
|
|
ddc75d710f | ||
|
|
5482601b41 | ||
|
|
249aecbc0d | ||
|
|
03f3020e9f | ||
|
|
e151eb85f0 | ||
|
|
348586553e | ||
|
|
fb6e58b3c4 | ||
|
|
67e98456bb | ||
|
|
3e47420534 | ||
|
|
1d3d557c43 | ||
|
|
3f41974a75 | ||
|
|
3e3c7397a5 | ||
|
|
dcb51702e7 | ||
|
|
de0c9a1eff | ||
|
|
dd0c1e20a7 | ||
|
|
4769d75dc3 | ||
|
|
6b926dc697 | ||
|
|
ecae077c6d | ||
|
|
78ab1bed35 | ||
|
|
2b6dbfde6b | ||
|
|
6bb1abd7f0 | ||
|
|
aef92240cc | ||
|
|
f0d8c2d095 | ||
|
|
14529f2c35 | ||
|
|
89f40d54d9 | ||
|
|
4c006970a2 | ||
|
|
de676041df | ||
|
|
82b1567ae5 | ||
|
|
1cd7291068 | ||
|
|
fdee1df5b3 | ||
|
|
c314d00ccd | ||
|
|
62e2a8a6e3 | ||
|
|
a12231a0aa | ||
|
|
183efd427a | ||
|
|
f95f3126c3 | ||
|
|
4d9ca4b451 | ||
|
|
c76334a68a | ||
|
|
f475391ba6 | ||
|
|
b77f22790b | ||
|
|
757a578c6e | ||
|
|
c5824767c8 | ||
|
|
84f3f07279 | ||
|
|
f8e1892eee | ||
|
|
036a8edca0 | ||
|
|
58ed6f395f | ||
|
|
ef4be6a612 | ||
|
|
3d8e6f4394 | ||
|
|
0cc1f18a7e | ||
|
|
8b6e577b8f | ||
|
|
cc91f2e5db | ||
|
|
8d1f2313ac | ||
|
|
f9abd90cc9 | ||
|
|
f7293255cf | ||
|
|
7d6a130cf9 | ||
|
|
be7bb2e39f | ||
|
|
1c30728880 | ||
|
|
8d91e7cb1d | ||
|
|
68d980795a | ||
|
|
45c11a5a8e | ||
|
|
4d00351db7 | ||
|
|
fff420b652 | ||
|
|
7103a95305 | ||
|
|
bcada5d553 | ||
|
|
c9b24ff612 | ||
|
|
854c7ed04d | ||
|
|
c0054959cd | ||
|
|
1aa837d220 | ||
|
|
95df402c4c | ||
|
|
2f3d6f7e63 | ||
|
|
ffe1523170 | ||
|
|
a73d5f4b4d | ||
|
|
f35b62ed12 | ||
|
|
20abdd2908 | ||
|
|
28c2c7285c | ||
|
|
3ca8ab12a4 | ||
|
|
fd82aa143c | ||
|
|
1fcbd4f9fd | ||
|
|
73d9cd62a5 | ||
|
|
be699d24a1 | ||
|
|
798eb90bed | ||
|
|
c252f5f8d9 | ||
|
|
76d77ad76f | ||
|
|
69f5cdd61a | ||
|
|
9f52f20660 | ||
|
|
90048afa9d | ||
|
|
1ec3ed32ee | ||
|
|
16ed5821e2 | ||
|
|
b681e33944 | ||
|
|
8f4d44a212 | ||
|
|
93a5d07f9a | ||
|
|
40c230c7db | ||
|
|
d0fa3b92f8 | ||
|
|
2a7727cf5f | ||
|
|
98722e7acf | ||
|
|
683fe4c3db | ||
|
|
e851696430 | ||
|
|
b2ccf965bb | ||
|
|
f847f38812 | ||
|
|
19039aa281 | ||
|
|
091b920a22 | ||
|
|
d7f264c1c2 | ||
|
|
897f8164b9 | ||
|
|
04a6f7c954 | ||
|
|
796287a776 | ||
|
|
763e30bd1d | ||
|
|
5121374d60 | ||
|
|
3e3a175bdc | ||
|
|
4a1aa9130b | ||
|
|
5a3f5f60b6 | ||
|
|
8c1600be55 | ||
|
|
7d3ae2347b | ||
|
|
27add47db4 | ||
|
|
836bcab036 | ||
|
|
2d20a07dfb | ||
|
|
24c5c70466 | ||
|
|
22ead22499 | ||
|
|
65e1620b43 | ||
|
|
acac09375e | ||
|
|
83cf859da3 | ||
|
|
956c391f13 | ||
|
|
7767a2607e | ||
|
|
429d1ac183 | ||
|
|
f42928f88f | ||
|
|
2d816ab92d | ||
|
|
aa75380662 | ||
|
|
3cf0ced62e | ||
|
|
41fa700fb3 | ||
|
|
a4ef9fb9b7 | ||
|
|
1db23b5277 | ||
|
|
584f0cbac0 | ||
|
|
dc432a19a3 | ||
|
|
6727b1cfca | ||
|
|
8c7b0698f3 | ||
|
|
69e1622e82 | ||
|
|
af7a5d3248 | ||
|
|
7b1a4554ad | ||
|
|
b3e213c133 | ||
|
|
a9fafec79d | ||
|
|
7cacf7bbaf | ||
|
|
44bf39f7b7 | ||
|
|
0db8f7295b | ||
|
|
c1226062b1 | ||
|
|
819ed168da | ||
|
|
19940953e2 | ||
|
|
f4af3eaf5d | ||
|
|
718563fe6a | ||
|
|
7e996ad6fa | ||
|
|
0b07fb6a49 | ||
|
|
0cb87e6ed9 | ||
|
|
71a3e75ae2 | ||
|
|
a2448ea4c1 | ||
|
|
8a14141021 | ||
|
|
69353892d9 | ||
|
|
7a2fcc3469 | ||
|
|
dfa6dd5011 | ||
|
|
654400c0cf | ||
|
|
3c15fc1b0d | ||
|
|
4b32485e3c | ||
|
|
b90736ed25 | ||
|
|
f28d5c7781 | ||
|
|
0654627478 | ||
|
|
2e91a2ecdb | ||
|
|
66a8ceb27a | ||
|
|
e6b0addb6c | ||
|
|
c00f132145 | ||
|
|
b9eb8d45b2 | ||
|
|
ec7ad70458 | ||
|
|
64e19699fb | ||
|
|
6d3e1b0cbe | ||
|
|
d3a53e1d3c | ||
|
|
23bbb3ee32 | ||
|
|
56bfcd52b7 | ||
|
|
1c19b6de1e | ||
|
|
85bed0bdf1 | ||
|
|
0102796769 | ||
|
|
50b2a72f1b | ||
|
|
04cc239756 | ||
|
|
0c20da2481 | ||
|
|
72d2a563f3 | ||
|
|
6f3f8c944d | ||
|
|
ed60fd2e82 | ||
|
|
636fef4693 | ||
|
|
77775e2f85 | ||
|
|
b260959a6e | ||
|
|
2a04f7b378 | ||
|
|
d4766f9c81 | ||
|
|
7f4c9b5b11 | ||
|
|
70f7c8ce89 | ||
|
|
f33a6aba39 | ||
|
|
236d717f10 | ||
|
|
0fc160f57c | ||
|
|
666023d89a | ||
|
|
8c4f2991c7 | ||
|
|
4453dff36d | ||
|
|
90b043561f | ||
|
|
43a8b34673 | ||
|
|
6005d33c94 | ||
|
|
cffb4736ce | ||
|
|
be54331d05 | ||
|
|
e19b8255d3 | ||
|
|
d680900f07 | ||
|
|
78bd4fd7de | ||
|
|
01d4a95d2d | ||
|
|
bb4490307d | ||
|
|
be492a3e3f | ||
|
|
a7b05f1e85 | ||
|
|
1c5ae4eb4d | ||
|
|
16d03d20ac | ||
|
|
8f062fab7d | ||
|
|
2565b934a5 | ||
|
|
7e88a9084d | ||
|
|
ef03292e35 | ||
|
|
d309ede623 | ||
|
|
834a80c35a | ||
|
|
99b0288d1b | ||
|
|
926f58f336 | ||
|
|
b7bfe99520 | ||
|
|
fa60b68425 | ||
|
|
8622e1e4ff | ||
|
|
3e13ae9740 | ||
|
|
e5cd577fbf | ||
|
|
86985231ff | ||
|
|
e64926a429 | ||
|
|
6232190cfe | ||
|
|
eb83d89307 | ||
|
|
b4a9941452 | ||
|
|
be782ba512 | ||
|
|
db54f247a2 | ||
|
|
0fc3b47c88 | ||
|
|
3eda2a1f2a | ||
|
|
c45b2adad6 | ||
|
|
702dbebfbe | ||
|
|
052303cc93 | ||
|
|
69380146e4 | ||
|
|
514fb56209 | ||
|
|
57b61070d9 | ||
|
|
86ce00365a | ||
|
|
a8bd32bef3 | ||
|
|
ae8801dd8a | ||
|
|
0a4e52e663 | ||
|
|
95045db74e | ||
|
|
19e7c0be0b | ||
|
|
3fcbf458b6 | ||
|
|
4580146cdb | ||
|
|
ea6a7c1c87 | ||
|
|
06cde721b3 | ||
|
|
31486e5963 | ||
|
|
1b52bdf425 | ||
|
|
9b59895ad2 | ||
|
|
c7600ff059 | ||
|
|
b1769edb65 | ||
|
|
14ad09e867 | ||
|
|
26a99d3696 | ||
|
|
c973730acc | ||
|
|
048bea376d | ||
|
|
eca4285ea8 | ||
|
|
437803da07 | ||
|
|
a7be7c3e19 | ||
|
|
5bd7dd3022 | ||
|
|
27aa0add4e | ||
|
|
24712c4c2d | ||
|
|
4bf4b4a045 | ||
|
|
9604f5c317 | ||
|
|
69c1059644 | ||
|
|
26b2903995 | ||
|
|
97abb256d5 | ||
|
|
a342911dae | ||
|
|
1f43563295 | ||
|
|
f23ebf1e99 | ||
|
|
d66b751c2e | ||
|
|
6a7c76a9ba | ||
|
|
2338787dbb | ||
|
|
a216c9cc37 | ||
|
|
b7da8d2193 | ||
|
|
41f191902b | ||
|
|
37397c7a69 | ||
|
|
153695288e | ||
|
|
a5e29758a4 | ||
|
|
964427e533 | ||
|
|
0a925df2a9 | ||
|
|
db95de6731 | ||
|
|
bc2191eae6 | ||
|
|
9fed844e59 | ||
|
|
a2c125ee90 | ||
|
|
a2779612be | ||
|
|
f51c3b6519 | ||
|
|
870968a7c5 | ||
|
|
c4f39ab85c | ||
|
|
8cb431ad40 | ||
|
|
77f98c2438 | ||
|
|
d7d0e5ec82 | ||
|
|
2519efb3ea | ||
|
|
b1c527cb71 | ||
|
|
b87f687e55 | ||
|
|
a62d27ffc2 | ||
|
|
a9a1c6c49b | ||
|
|
4989d6ddfa | ||
|
|
fd01b9de36 | ||
|
|
1017010e15 | ||
|
|
e0bd06489f | ||
|
|
4a6301c2af | ||
|
|
bca830dc54 | ||
|
|
77bde5325e | ||
|
|
bdca506dd4 | ||
|
|
34faaa28ca | ||
|
|
a08d433d0a | ||
|
|
6d08162012 | ||
|
|
2481650028 | ||
|
|
5e551fee41 | ||
|
|
e05690af4c | ||
|
|
8e65dd0546 | ||
|
|
509442a2f3 | ||
|
|
a5102d1b94 | ||
|
|
19ee2b3f34 | ||
|
|
5d29c2d881 | ||
|
|
a08bbab9dc | ||
|
|
84f6394f1f | ||
|
|
d359949310 | ||
|
|
30cd8a03eb | ||
|
|
1b7015c0df | ||
|
|
55b14641e0 | ||
|
|
4936d7fcc6 | ||
|
|
b8fa43296d | ||
|
|
631dd01c92 | ||
|
|
fdc72bc84e | ||
|
|
c63da9cd6e | ||
|
|
849d570bcb | ||
|
|
85e6efb8b0 | ||
|
|
485c8c3113 | ||
|
|
f2d0a8eb8c | ||
|
|
9201bc1022 | ||
|
|
2847a95c57 | ||
|
|
fc30bb9852 | ||
|
|
0f360d34e8 | ||
|
|
ec435e086f | ||
|
|
1efe7f7cde | ||
|
|
5759341c52 | ||
|
|
ea9ba27f22 | ||
|
|
a28bbce1f9 | ||
|
|
75b48aa8ac | ||
|
|
573e56cd8c | ||
|
|
ab28b4c0c5 | ||
|
|
55e36f24a5 | ||
|
|
ea8d6bf6c8 | ||
|
|
75b8bcb853 | ||
|
|
2b2cc2effc | ||
|
|
ae1c5908ae | ||
|
|
f067cb99c8 | ||
|
|
7e7d3a1659 | ||
|
|
c5f364f3e4 | ||
|
|
2a6366ef85 |
14
.eslintrc.js
@@ -3,10 +3,14 @@ module.exports = {
|
||||
parserOptions: {
|
||||
sourceType: 'CommonJS',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'simple-import-sort',
|
||||
'import'
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:prettier/recommended'
|
||||
],
|
||||
globals: {
|
||||
@@ -26,7 +30,11 @@ module.exports = {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'import/first': 'error',
|
||||
'import/no-duplicates': 'error',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'@typescript-eslint/ban-types': [
|
||||
'error',
|
||||
{
|
||||
|
||||
38
.github/ISSUE_TEMPLATE/-en--bug-report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "[EN] Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[EN][BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Title: [Brief Description of the Bug]
|
||||
|
||||
#### Description:
|
||||
Describe in detail the problem you encountered. Include any relevant context that may help understand the origin of the bug.
|
||||
|
||||
#### Steps to Reproduce:
|
||||
1. List the steps necessary to reproduce the problem.
|
||||
2. Try to be as specific as possible.
|
||||
3. If the problem occurs in a specific scenario, describe it here.
|
||||
|
||||
#### Expected Behavior:
|
||||
Describe what you expected to happen when following the steps above.
|
||||
|
||||
#### Current Behavior:
|
||||
Explain what actually happens when you follow the steps above.
|
||||
|
||||
#### Screenshots/Videos:
|
||||
If possible, add screenshots or videos illustrating the problem. This can be extremely helpful in understanding the issue.
|
||||
|
||||
#### Environment:
|
||||
- **Server:** [e.g., Ubuntu 18.04]
|
||||
- **API Version:** [e.g., 1.5.4]
|
||||
- **Other Hardware/Software Specifications:** [e.g., CPU, GPU]
|
||||
|
||||
#### Submitting Logs:
|
||||
Please attach logs that may be related to the problem. If the logs contain sensitive information, consider sending them privately to one of the project maintainers.
|
||||
|
||||
#### Additional Notes:
|
||||
Include here any other information that you think might be useful in understanding or resolving the bug.
|
||||
28
.github/ISSUE_TEMPLATE/-en--feature-request.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "[EN] Feature request"
|
||||
about: Suggest an idea for the API
|
||||
title: "[EN][FEAT]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Title: [Brief Description of Feature Request]
|
||||
|
||||
#### Detailed Description:
|
||||
Clearly and in detail, describe the functionality you wish to be implemented. Explain how this fits into the context of the project.
|
||||
|
||||
#### Rationale:
|
||||
Explain why this functionality would be useful for the project. This helps in understanding the importance and priority of the request.
|
||||
|
||||
#### Usage Examples:
|
||||
Provide specific examples of how this feature could be used. This can include scenarios or use cases where the feature would be particularly beneficial.
|
||||
|
||||
#### Possible Implementations:
|
||||
If you have ideas on how this feature might be implemented, please share them here. This is not mandatory but can be helpful for the development team.
|
||||
|
||||
#### Impact on the Project:
|
||||
Discuss how this new feature could impact other parts of the project, if applicable.
|
||||
|
||||
#### Additional Notes:
|
||||
Any other information you believe is relevant to your request.
|
||||
38
.github/ISSUE_TEMPLATE/-pt--reportar-bug.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "[PT] Reportar bug"
|
||||
about: Reportar um problema
|
||||
title: "[PT][BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Título: [Breve Descrição do Bug]
|
||||
|
||||
#### Descrição:
|
||||
Descreva detalhadamente o problema que você encontrou. Inclua qualquer contexto relevante que possa ajudar a entender a origem do bug.
|
||||
|
||||
#### Passos para Reproduzir:
|
||||
1. Liste os passos necessários para reproduzir o problema.
|
||||
2. Tente ser o mais específico possível.
|
||||
3. Se o problema ocorrer em um cenário específico, descreva-o aqui.
|
||||
|
||||
#### Comportamento Esperado:
|
||||
Descreva o que você esperava que acontecesse quando seguisse os passos acima.
|
||||
|
||||
#### Comportamento Atual:
|
||||
Explique o que realmente acontece quando você segue os passos acima.
|
||||
|
||||
#### Capturas de Tela/Vídeos:
|
||||
Se possível, adicione capturas de tela ou vídeos que ilustrem o problema. Isso pode ser extremamente útil para entender o problema.
|
||||
|
||||
#### Ambiente:
|
||||
- **Servidor:** [ex: Ubuntu 18.04]
|
||||
- **Versão da API:** [ex: 1.5.4]
|
||||
- **Outras Especificações de Hardware/Software:** [ex: CPU, GPU]
|
||||
|
||||
#### Envio de Logs:
|
||||
Por favor, anexe os logs que possam estar relacionados ao problema. Se os logs contiverem informações sensíveis, considere enviá-los de forma privada para um dos mantenedores do projeto.
|
||||
|
||||
#### Notas Adicionais:
|
||||
Inclua aqui qualquer outra informação que você ache que possa ser útil para entender ou resolver o bug.
|
||||
28
.github/ISSUE_TEMPLATE/-pt--solicitar-recurso.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "[PT] Solicitar recurso"
|
||||
about: Sugira novos recursos para a API
|
||||
title: "[PT][FEAT]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Título: [Breve Descrição da Solicitação de Recurso]
|
||||
|
||||
#### Descrição Detalhada:
|
||||
Descreva claramente e em detalhes a funcionalidade que você deseja que seja implementada. Explique como isso se encaixa no contexto do projeto.
|
||||
|
||||
#### Racional:
|
||||
Explique por que essa funcionalidade seria útil para o projeto. Isso ajuda a entender a importância e a prioridade da solicitação.
|
||||
|
||||
#### Exemplos de Uso:
|
||||
Forneça exemplos específicos de como essa funcionalidade poderia ser utilizada. Isso pode incluir cenários ou casos de uso onde a funcionalidade seria particularmente benéfica.
|
||||
|
||||
#### Possíveis Implementações:
|
||||
Se você tem ideias sobre como essa funcionalidade pode ser implementada, por favor, compartilhe-as aqui. Isso não é obrigatório, mas pode ser útil para a equipe de desenvolvimento.
|
||||
|
||||
#### Impacto no Projeto:
|
||||
Discuta como essa nova funcionalidade poderia impactar outras partes do projeto, se aplicável.
|
||||
|
||||
#### Notas Adicionais:
|
||||
Qualquer outra informação que você acredita ser relevante para a sua solicitação.
|
||||
64
.github/workflows/publish_docker_image.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Build Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
build-amd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Extract existing image metadata
|
||||
id: image-meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: atendai/evolution-api
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push AMD image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
labels: ${{ steps.image-meta.outputs.labels }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
|
||||
build-arm:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204-arm
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Extract existing image metadata
|
||||
id: image-meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: atendai/evolution-api
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push ARM image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
labels: ${{ steps.image-meta.outputs.labels }}
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
20
.gitignore
vendored
@@ -2,6 +2,10 @@
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
/Docker/.env
|
||||
|
||||
.vscode
|
||||
|
||||
# Logs
|
||||
logs/**.json
|
||||
*.log
|
||||
@@ -11,16 +15,22 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
/docker-compose-data
|
||||
/docker-data
|
||||
|
||||
docker-compose.yaml
|
||||
|
||||
# Package
|
||||
/yarn.lock
|
||||
/package-lock.json
|
||||
|
||||
# IDE - VSCode
|
||||
# IDEs
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.nova/*
|
||||
|
||||
# Prisma
|
||||
/prisma/migrations
|
||||
@@ -30,3 +40,11 @@ lerna-debug.log*
|
||||
!/instances/.gitkeep
|
||||
/test/
|
||||
/src/env.yml
|
||||
/store
|
||||
*.env
|
||||
|
||||
/temp/*
|
||||
|
||||
.DS_Store
|
||||
*.DS_Store
|
||||
.tool-versions
|
||||
|
||||
@@ -2,8 +2,11 @@ module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 90,
|
||||
printWidth: 120,
|
||||
arrowParens: 'always',
|
||||
tabWidth: 2,
|
||||
bracketSameLine: true,
|
||||
bracketSpacing: true
|
||||
useTabs: false,
|
||||
bracketSameLine: false,
|
||||
bracketSpacing: true,
|
||||
parser: 'typescript'
|
||||
}
|
||||
11
.vscode/settings.json
vendored
@@ -5,7 +5,12 @@
|
||||
"editor.smoothScrolling": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll": true
|
||||
}
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"prisma-smart-formatter.typescript.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prisma-smart-formatter.prisma.defaultFormatter": "Prisma.prisma",
|
||||
"i18n-ally.localesPaths": [
|
||||
"store/messages"
|
||||
]
|
||||
}
|
||||
506
CHANGELOG.md
@@ -1,3 +1,509 @@
|
||||
# 1.7.3 (2024-04-18 12:07)
|
||||
|
||||
### Fixed
|
||||
* Revert fix audio encoding
|
||||
* Recovering messages lost with redis cache
|
||||
* Adjusts in redis for save instances
|
||||
* Adjusts in proxy
|
||||
* Revert pull request #523
|
||||
* Added instance name on logs
|
||||
* Added support for Spanish
|
||||
* Fix error: invalid operator. The allowed operators for identifier are equal_to,not_equal_to in chatwoot
|
||||
|
||||
# 1.7.2 (2024-04-12 17:31)
|
||||
|
||||
### Feature
|
||||
|
||||
* Mobile connection via sms (test)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjusts in redis
|
||||
* Send global event in websocket
|
||||
* Adjusts in proxy
|
||||
* Fix audio encoding
|
||||
* Fix conversation read on chatwoot version 3.7
|
||||
* Fix when receiving/sending messages from whatsapp desktop with ephemeral messages enabled
|
||||
* Changed returned sessions on typebot status change
|
||||
* Reorganization of files and folders
|
||||
|
||||
# 1.7.1 (2024-04-03 10:19)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Correction when sending files with captions on Whatsapp Business
|
||||
* Correction in receiving messages with response on WhatsApp Business
|
||||
* Correction when sending a reaction to a message on WhatsApp Business
|
||||
* Correction of receiving reactions on WhatsApp business
|
||||
* Removed mandatory description of rows from sendList
|
||||
* Feature to collect message type in typebot
|
||||
|
||||
# 1.7.0 (2024-03-11 18:23)
|
||||
|
||||
### Feature
|
||||
|
||||
* Added update message endpoint
|
||||
* Add translate capabilities to QRMessages in CW
|
||||
* Join in Group by Invite Code
|
||||
* Read messages from whatsapp in chatwoot
|
||||
* Add support to use use redis in cacheservice
|
||||
* Add support for labels
|
||||
* Command to clearcache from chatwoot inbox
|
||||
* Whatsapp Cloud API Oficial
|
||||
|
||||
### Fixed
|
||||
|
||||
* Proxy configuration improvements
|
||||
* Correction in sending lists
|
||||
* Adjust in webhook_base64
|
||||
* Correction in typebot text formatting
|
||||
* Correction in chatwoot text formatting and render list message
|
||||
* Only use a axios request to get file mimetype if necessary
|
||||
* When possible use the original file extension
|
||||
* When receiving a file from whatsapp, use the original filename in chatwoot if possible
|
||||
* Remove message ids cache in chatwoot to use chatwoot's api itself
|
||||
* Adjusts the quoted message, now has contextInfo in the message Raw
|
||||
* Collecting responses with text or numbers in Typebot
|
||||
* Added sendList endpoint to swagger documentation
|
||||
* Implemented a function to synchronize message deletions on WhatsApp, automatically reflecting in Chatwoot.
|
||||
* Improvement on numbers validation
|
||||
* Fix polls in message sending
|
||||
* Sending status message
|
||||
* Message 'connection successfully' spamming
|
||||
* Invalidate the conversation cache if reopen_conversation is false and the conversation was resolved
|
||||
* Fix looping when deleting a message in chatwoot
|
||||
* When receiving a file from whatsapp, use the original filename in chatwoot if possible
|
||||
* Correction in the sendList Function
|
||||
* Implement contact upsert in messaging-history.set
|
||||
* Improve proxy error handling
|
||||
* Refactor fetching participants for group in WhatsApp service
|
||||
* Fixed problem where the typebot final keyword did not work
|
||||
* Typebot's wait now pauses the flow and composing is defined by the delay_message parameter in set typebot
|
||||
* Composing over 20s now loops until finished
|
||||
|
||||
# 1.6.1 (2023-12-22 11:43)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed Lid Messages
|
||||
* Fixed sending variables to typebot
|
||||
* Fixed sending variables from typebot
|
||||
* Correction sending s3/minio media to chatwoot and typebot
|
||||
* Fixed the problem with typebot closing at the end of the flow, now this is optional with the TYPEBOT_KEEP_OPEN variable
|
||||
* Fixed chatwoot Bold, Italic and Underline formatting using Regex
|
||||
* Added the sign_delimiter property to the Chatwoot configuration, allowing you to set a different delimiter for the signature. Default when not defined \n
|
||||
* Include instance Id field in the instance configuration
|
||||
* Fixed the pairing code
|
||||
* Adjusts in typebot
|
||||
* Fix the problem when disconnecting the instance and connecting again using mongodb
|
||||
* Options to disable docs and manager
|
||||
* When deleting a message in whatsapp, delete the message in chatwoot too
|
||||
|
||||
|
||||
# 1.6.0 (2023-12-12 17:24)
|
||||
|
||||
### Feature
|
||||
|
||||
* Added AWS SQS Integration
|
||||
* Added support for new typebot API
|
||||
* Added endpoint sendPresence
|
||||
* New Instance Manager
|
||||
* Added auto_create to the chatwoot set to create the inbox automatically or not
|
||||
* Added reply, delete and message reaction in chatwoot v3.3.1
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjusts in proxy
|
||||
* Adjusts in start session for Typebot
|
||||
* Added mimetype field when sending media
|
||||
* Ajusts in validations to messages.upsert
|
||||
* Fixed messages not received: error handling when updating contact in chatwoot
|
||||
* Fix workaround to manage param data as an array in mongodb
|
||||
* Removed await from webhook when sending a message
|
||||
* Update typebot.service.ts - element.underline change ~ for *
|
||||
* Removed api restart on receiving an error
|
||||
* Fixes in mongodb and chatwoot
|
||||
* Adjusted return from queries in mongodb
|
||||
* Added restart instance when update profile picture
|
||||
* Correction of chatwoot functioning with admin flows
|
||||
* Fixed problem that did not generate qrcode with the chatwoot_conversation_pending option enabled
|
||||
* Fixed issue where CSAT opened a new ticket when reopen_conversation was disabled
|
||||
* Fixed issue sending contact to Chatwoot via iOS
|
||||
|
||||
### Integrations
|
||||
|
||||
* Chatwoot: v3.3.1
|
||||
* Typebot: v2.20.0
|
||||
|
||||
# 1.5.4 (2023-10-09 20:43)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Baileys logger typing issue resolved
|
||||
* Solved problem with duplicate messages in chatwoot
|
||||
|
||||
# 1.5.3 (2023-10-06 18:55)
|
||||
|
||||
### Feature
|
||||
|
||||
* Swagger documentation
|
||||
* Added base 64 sending option via webhook
|
||||
|
||||
### Fixed
|
||||
|
||||
* Remove rabbitmq queues when delete instances
|
||||
* Improvement in restart instance to completely redo the connection
|
||||
* Update node version: v20
|
||||
* Correction of messages sent by the api and typebot not appearing in chatwoot
|
||||
* Adjustment to start typebot, added startSession parameter
|
||||
* Chatwoot now receives messages sent via api and typebot
|
||||
* Fixed problem with starting with an input in typebot
|
||||
* Added check to ensure variables are not empty before executing foreach in start typebot
|
||||
|
||||
# 1.5.2 (2023-09-28 17:56)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix chatwootSchema in chatwoot model to store reopen_conversation and conversation_pending options
|
||||
* Problem resolved when sending files from minio to typebot
|
||||
* Improvement in the "startTypebot" method to create persistent session when triggered
|
||||
* New manager for Evo 1.5.2 - Set Typebot update
|
||||
* Resolved problems when reading/querying instances
|
||||
|
||||
# 1.5.1 (2023-09-17 13:50)
|
||||
|
||||
### Feature
|
||||
|
||||
* Added listening_from_me option in Set Typebot
|
||||
* Added variables options in Start Typebot
|
||||
* Added webhooks for typebot events
|
||||
* Added ChamaAI integration
|
||||
* Added webhook to send errors
|
||||
* Added support for messaging with ads on chatwoot
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix looping connection messages in chatwoot
|
||||
* Improved performance of fetch instances
|
||||
|
||||
# 1.5.0 (2023-08-18 12:47)
|
||||
|
||||
### Feature
|
||||
|
||||
* New instance manager in /manager route
|
||||
* Added extra files for chatwoot and appsmith
|
||||
* Added Get Last Message and Archive for Chat
|
||||
* Added env var QRCODE_COLOR
|
||||
* Added websocket to send events
|
||||
* Added rabbitmq to send events
|
||||
* Added Typebot integration
|
||||
* Added proxy endpoint
|
||||
* Added send and date_time in webhook data
|
||||
|
||||
### Fixed
|
||||
|
||||
* Solved problem when disconnecting from the instance the instance was deleted
|
||||
* Encoded spaces in chatwoot webhook
|
||||
* Adjustment in the saving of contacts, saving the information of the number and Jid
|
||||
* Update Dockerfile
|
||||
* If you pass empty events in create instance and set webhook it is understood as all
|
||||
* Fixed issue that did not output base64 averages
|
||||
* Messages sent by the api now arrive in chatwoot
|
||||
|
||||
### Integrations
|
||||
|
||||
* Chatwoot: v2.18.0 - v3.0.0
|
||||
* Typebot: v2.16.0
|
||||
* Manager Evolution API
|
||||
|
||||
# 1.4.8 (2023-07-27 10:27)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed error return bug
|
||||
|
||||
# 1.4.7 (2023-07-27 08:47)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed error return bug
|
||||
* Fixed problem of getting message when deleting message in chatwoot
|
||||
* Change in error return pattern
|
||||
|
||||
# 1.4.6 (2023-07-26 17:54)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed bug of creating new inbox by chatwoot
|
||||
* When conversation reopens is pending when conversation pending is true
|
||||
* Added docker-compose file with dockerhub image
|
||||
|
||||
# 1.4.5 (2023-07-26 09:32)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed problems in localization template in chatwoot
|
||||
* Fix mids going duplicated in chatwoot
|
||||
|
||||
# 1.4.4 (2023-07-25 15:24)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed chatwoot line wrap issue
|
||||
* Solved receive location in chatwoot
|
||||
* When requesting the pairing code, it also brings the qr code
|
||||
* Option reopen_conversation in chatwoot endpoint
|
||||
* Option conversation_pending in chatwoot endpoint
|
||||
|
||||
# 1.4.3 (2023-07-25 10:51)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjusts in settings with options always_online, read_messages and read_status
|
||||
* Fixed send webhook for event CALL
|
||||
* Create instance with settings
|
||||
|
||||
# 1.4.2 (2023-07-24 20:52)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed validation is set settings
|
||||
* Adjusts in group validations
|
||||
* Ajusts in sticker message to chatwoot
|
||||
|
||||
# 1.4.1 (2023-07-24 18:28)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed reconnect with pairing code or qrcode
|
||||
* Fixed problem in createJid
|
||||
|
||||
# 1.4.0 (2023-07-24 17:03)
|
||||
|
||||
### Features
|
||||
|
||||
* Added connection functionality via pairing code
|
||||
* Added fetch profile endpoint in chat controller
|
||||
* Created settings controller
|
||||
* Added reject call and send text message when receiving a call
|
||||
* Added setting to ignore group messages
|
||||
* Added connection with pairing code in chatwoot with command /init:{NUMBER}
|
||||
* Added encoding option in endpoint sendWhatsAppAudio
|
||||
|
||||
### Fixed
|
||||
|
||||
* Added link preview option in send text message
|
||||
* Fixed problem with fileSha256 appearing when sending a sticker in chatwoot
|
||||
* Fixed issue where it was not possible to open a conversation when sent at first by me on my cell phone in chatwoot
|
||||
* Now it only updates the contact name if it is the same as the phone number in chatwoot
|
||||
* Now accepts all chatwoot inbox templates
|
||||
* Command to create new instances set to /new_instance:{NAME}:{NUMBER}
|
||||
* Fix in chatwoot set, sign msg can now be disabled
|
||||
|
||||
### Integrations
|
||||
|
||||
* Chatwoot: v2.18.0 - v3.0.0 (Beta)
|
||||
|
||||
# 1.3.2 (2023-07-21 17:19)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix in update settings that needed to restart after updated
|
||||
* Correction in the use of the api with mongodb
|
||||
* Adjustments to search endpoint for contacts, chats, messages and Status messages
|
||||
* Now when deleting the instance, the data referring to it in mongodb is also deleted
|
||||
* It is now validated if the instance name contains uppercase and special characters
|
||||
* For compatibility reasons, container mode has been removed
|
||||
* Added docker-compose files example
|
||||
|
||||
### Integrations
|
||||
|
||||
* Chatwoot: v2.18.0
|
||||
|
||||
# 1.3.1 (2023-07-20 07:48)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjust in create store files
|
||||
|
||||
### Integrations
|
||||
|
||||
* Chatwoot: v2.18.0
|
||||
|
||||
# 1.3.0 (2023-07-19 11:33)
|
||||
|
||||
### Features
|
||||
|
||||
* Added messages.delete event
|
||||
* Added restart instance endpoint
|
||||
* Created automation for creating instances in the chatwoot bot with the command '#inbox_whatsapp:{INSTANCE_NAME}
|
||||
* Change Baileys version to: 6.4.0
|
||||
* Send contact in chatwoot
|
||||
* Send contact array in chatwoot
|
||||
* Added apiKey in webhook and serverUrl in fetchInstance if EXPOSE_IN_FETCH_INSTANCES: true
|
||||
* Translation set to default (english) in chatwoot
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed error to send message in large groups
|
||||
* Docker files adjusted
|
||||
* Fixed in the postman collection the webhookByEvent parameter by webhook_by_events
|
||||
* Added validations in create instance
|
||||
* Removed link preview endpoint, now it's done automatically from sending conventional text
|
||||
* Added group membership validation before sending message to groups
|
||||
* Adjusts in docker files
|
||||
* Adjusts in returns in endpoints chatwoot and webhook
|
||||
* Fixed ghost mentions in send text message
|
||||
* Fixed bug that saved contacts from groups came without number in chatwoot
|
||||
* Fixed problem to receive csat in chatwoot
|
||||
* Fixed require fileName for document only in base64 for send media message
|
||||
* Bug fix when sending mobile message change contact name to number in chatwoot
|
||||
* Bug fix when connecting whatsapp does not send confirmation message
|
||||
* Fixed quoted message with id or message directly
|
||||
* Adjust in validation for mexican and argentine numbers
|
||||
* Adjust in create store files
|
||||
|
||||
### Integrations
|
||||
|
||||
* Chatwoot: v2.18.0
|
||||
|
||||
# 1.2.2 (2023-07-15 09:36)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Tweak in route "/" with version info
|
||||
* Adjusts chatwoot version
|
||||
|
||||
### Integrations
|
||||
|
||||
* Chatwoot: v2.18.0
|
||||
|
||||
# 1.2.1 (2023-07-14 19:04)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjusts in docker files
|
||||
* Save picture url groups in chatwoot
|
||||
|
||||
# 1.2.0 (2023-07-14 15:28)
|
||||
|
||||
### Features
|
||||
|
||||
* Native integration with chatwoot
|
||||
* Added returning or non-returning participants option in fetchAllGroups
|
||||
* Added group integration to chatwoot
|
||||
* Added automation on create instance to chatwoot
|
||||
* Added verbose logs and format chatwoot service
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjusts in docker-compose files
|
||||
* Adjusts in number validation for AR and MX numbers
|
||||
* Adjusts in env files, removed save old_messages
|
||||
* Fix when sending a message to a group I don't belong returns a bad request
|
||||
* Fits the format on return from the fetchAllGroups endpoint
|
||||
* Adjust in send document with caption from chatwoot
|
||||
* Fixed message with undefind in chatwoot
|
||||
* Changed message in path /
|
||||
* Test duplicate message media in groups chatwoot
|
||||
* Optimize send message from group with mentions
|
||||
* Fixed name of the profile status in fetchInstances
|
||||
* Fixed error 500 when logout in instance with status = close
|
||||
|
||||
# 1.1.5 (2023-07-12 07:17)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjusts in temp folder
|
||||
* Return with event send_message
|
||||
|
||||
# 1.1.4 (2023-07-08 11:01)
|
||||
|
||||
### Features
|
||||
|
||||
* Route to send status broadcast
|
||||
* Added verbose logs
|
||||
* Insert allContacts in payload of endpoint sendStatus
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjusted set in webhook to go empty when enabled false
|
||||
* Adjust in store files
|
||||
* Fixed the problem when do not save contacts when receive messages
|
||||
* Changed owner of the jid for instanceName
|
||||
* Create .env for installation in docker
|
||||
|
||||
# 1.1.3 (2023-07-06 11:43)
|
||||
|
||||
### Features
|
||||
|
||||
* Added configuration for Baileys log level in env
|
||||
* Added audio to mp4 converter in optionally get Base64 From MediaMessage
|
||||
* Added organization name in vcard
|
||||
* Added email in vcard
|
||||
* Added url in vcard
|
||||
* Added verbose logs
|
||||
|
||||
### Fixed
|
||||
|
||||
* Added timestamp internally in urls to avoid caching
|
||||
* Correction in decryption of poll votes
|
||||
* Change in the way the api sent and saved the sent messages, now it goes in the messages.upsert event
|
||||
* Fixed cash when sending stickers via url
|
||||
* Improved how Redis works for instances
|
||||
* Fixed problem when disconnecting the instance it removes the instance
|
||||
* Fixed problem sending ack when preview is done by me
|
||||
* Adjust in store files
|
||||
|
||||
# 1.1.2 (2023-06-28 13:43)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed baileys version in package.json
|
||||
* Fixed problem that did not validate if the token passed in create instance already existed
|
||||
* Fixed problem that does not delete instance files in server mode
|
||||
|
||||
# 1.1.1 (2023-06-28 10:27)
|
||||
|
||||
### Features
|
||||
|
||||
* Added group invitation sending
|
||||
* Added webhook configuration per event in the individual instance registration
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjust dockerfile variables
|
||||
|
||||
# 1.1.0 (2023-06-21 11:17)
|
||||
|
||||
### Features
|
||||
|
||||
* Improved fetch instances endpoint, now it also fetch other instances even if they are not connected
|
||||
* Added conversion of audios for sending recorded audio, now it is possible to send mp3 audios and not just ogg
|
||||
* Route to fetch all groups that the connection is part of
|
||||
* Route to fetch all privacy settings
|
||||
* Route to update the privacy settings
|
||||
* Route to update group subject
|
||||
* Route to update group description
|
||||
* Route to accept invite code
|
||||
* Added configuration of events by webhook of instances
|
||||
* Now the api key can be exposed in fetch instances if the EXPOSE_IN_FETCH_INSTANCES variable is set to true
|
||||
* Added option to generate qrcode as soon as the instance is created
|
||||
* The created instance token can now also be optionally defined manually in the creation endpoint
|
||||
* Route to send Sticker
|
||||
|
||||
### Fixed
|
||||
|
||||
* Adjust dockerfile variables
|
||||
* tweaks in docker-compose to pass variables
|
||||
* Adjust the route getProfileBusiness to fetchProfileBusiness
|
||||
* fix error after logout and try to get status or to connect again
|
||||
* fix sending narrated audio on whatsapp android and ios
|
||||
* fixed the problem of not disabling the global webhook by the variable
|
||||
* Adjustment in the recording of temporary files and periodic cleaning
|
||||
* Fix for container mode also work only with files
|
||||
* Remove recording of old messages on sync
|
||||
|
||||
# 1.0.9 (2023-06-10)
|
||||
|
||||
### Fixed
|
||||
|
||||
80
Docker/.env
@@ -1,80 +0,0 @@
|
||||
CORS_ORIGIN='*' # Or separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
|
||||
CORS_METHODS='POST,GET,PUT,DELETE'
|
||||
CORS_CREDENTIALS=true
|
||||
|
||||
# Determine the logs to be displayed
|
||||
LOG_LEVEL='ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK'
|
||||
LOG_COLOR=true
|
||||
|
||||
# Determine how long the instance should be deleted from memory in case of no connection.
|
||||
# Default time: 5 minutes
|
||||
# If you don't even want an expiration, enter the value false
|
||||
DEL_INSTANCE=5
|
||||
|
||||
# Temporary data storage
|
||||
STORE_CLEANING_INTERVAL=7200 # seconds ===2h
|
||||
STORE_MESSAGE=true
|
||||
STORE_CONTACTS=false
|
||||
STORE_CHATS=false
|
||||
|
||||
# Permanent data storage
|
||||
DATABASE_ENABLED=false
|
||||
DATABASE_CONNECTION_URI='<uri>'
|
||||
DATABASE_CONNECTION_DB_PREFIX_NAME='evolution'
|
||||
DATABASE_SAVE_DATA_INSTANCE=false
|
||||
DATABASE_SAVE_DATA_OLD_MESSAGE=false
|
||||
DATABASE_SAVE_DATA_NEW_MESSAGE=true
|
||||
DATABASE_SAVE_MESSAGE_UPDATE=true
|
||||
DATABASE_SAVE_DATA_CONTACTS=true
|
||||
DATABASE_SAVE_DATA_CHATS=true
|
||||
|
||||
REDIS_ENABLED=true
|
||||
REDIS_URI='<uri>/1'
|
||||
REDIS_PREFIX_KEY='evolution'
|
||||
|
||||
# Webhook Settings
|
||||
## Define a global webhook that will listen for enabled events from all instances
|
||||
WEBHOOK_GLOBAL_URL='<url>'
|
||||
WEBHOOK_GLOBAL_ENABLED=false
|
||||
WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
|
||||
## Set the events you want to hear
|
||||
WEBHOOK_EVENTS_STATUS_INSTANCE=true
|
||||
WEBHOOK_EVENTS_APPLICATION_STARTUP=true
|
||||
WEBHOOK_EVENTS_QRCODE_UPDATED=true
|
||||
WEBHOOK_EVENTS_MESSAGES_SET=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPSERT=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
WEBHOOK_EVENTS_PRESENCE_UPDATE=true
|
||||
WEBHOOK_EVENTS_CHATS_SET=true
|
||||
WEBHOOK_EVENTS_CHATS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CHATS_UPDATE=true
|
||||
WEBHOOK_EVENTS_CONNECTION_UPDATE=true
|
||||
WEBHOOK_EVENTS_GROUPS_UPSERT=false
|
||||
WEBHOOK_EVENTS_GROUPS_UPDATE=false
|
||||
WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=false
|
||||
## This event fires every time a new token is requested via the refresh route
|
||||
WEBHOOK_EVENTS_NEW_JWT_TOKEN=true
|
||||
|
||||
CONFIG_SESSION_PHONE_CLIENT='Evolution API'
|
||||
CONFIG_SESSION_PHONE_NAME='Chrome'
|
||||
|
||||
# Set qrcode display limit
|
||||
QRCODE_LIMIT=6
|
||||
|
||||
# Defines an authentication type for the api
|
||||
AUTHENTICATION_TYPE='jwt' # or 'apikey'
|
||||
## Define a global apikey to access all instances.
|
||||
### OBS: This key must be inserted in the request header to create an instance.
|
||||
AUTHENTICATION_API_KEY='t8OOEeISKzpmc3jjcMqBWYSaJsafdefer'
|
||||
## Set the secret key to encrypt and decrypt your token and its expiration time
|
||||
AUTHENTICATION_JWT_EXPIRIN_IN=3600 # seconds - 3600s ===1h | zero (0) - never expires
|
||||
AUTHENTICATION_JWT_SECRET='L0YWtjb2w554WFqPG'
|
||||
|
||||
AUTHENTICATION_INSTANCE_NAME='evolution'
|
||||
AUTHENTICATION_INSTANCE_WEBHOOK_URL='<url>'
|
||||
AUTHENTICATION_INSTANCE_MODE='container' # or 'server'
|
||||
AUTHENTICATION_INSTANCE_WEBHOOK_BY_EVENTS=false
|
||||
153
Docker/.env.example
Normal file
@@ -0,0 +1,153 @@
|
||||
# Server URL - Set your application url
|
||||
SERVER_URL=http://localhost:8080
|
||||
|
||||
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
|
||||
CORS_ORIGIN=*
|
||||
CORS_METHODS=POST,GET,PUT,DELETE
|
||||
CORS_CREDENTIALS=true
|
||||
|
||||
# Determine the logs to be displayed
|
||||
LOG_LEVEL=ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS
|
||||
LOG_COLOR=true
|
||||
# Log Baileys - "fatal" | "error" | "warn" | "info" | "debug" | "trace"
|
||||
LOG_BAILEYS=error
|
||||
|
||||
# Determine how long the instance should be deleted from memory in case of no connection.
|
||||
# Default time: 5 minutes
|
||||
# If you don't even want an expiration, enter the value false
|
||||
DEL_INSTANCE=false
|
||||
DEL_TEMP_INSTANCES=true # Delete instances with status closed on start
|
||||
|
||||
# Temporary data storage
|
||||
STORE_MESSAGES=true
|
||||
STORE_MESSAGE_UP=true
|
||||
STORE_CONTACTS=true
|
||||
STORE_CHATS=true
|
||||
|
||||
# Set Store Interval in Seconds (7200 = 2h)
|
||||
CLEAN_STORE_CLEANING_INTERVAL=7200
|
||||
CLEAN_STORE_MESSAGES=true
|
||||
CLEAN_STORE_MESSAGE_UP=true
|
||||
CLEAN_STORE_CONTACTS=true
|
||||
CLEAN_STORE_CHATS=true
|
||||
|
||||
# Permanent data storage
|
||||
DATABASE_ENABLED=false
|
||||
DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin &
|
||||
readPreference=primary &
|
||||
ssl=false &
|
||||
directConnection=true
|
||||
DATABASE_CONNECTION_DB_PREFIX_NAME=evdocker
|
||||
|
||||
# Choose the data you want to save in the application's database or store
|
||||
DATABASE_SAVE_DATA_INSTANCE=false
|
||||
DATABASE_SAVE_DATA_NEW_MESSAGE=false
|
||||
DATABASE_SAVE_MESSAGE_UPDATE=false
|
||||
DATABASE_SAVE_DATA_CONTACTS=false
|
||||
DATABASE_SAVE_DATA_CHATS=false
|
||||
|
||||
RABBITMQ_ENABLED=false
|
||||
RABBITMQ_RABBITMQ_MODE=global
|
||||
RABBITMQ_EXCHANGE_NAME=evolution_exchange
|
||||
RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672
|
||||
|
||||
WEBSOCKET_ENABLED=false
|
||||
WEBSOCKET_GLOBAL_EVENTS=false
|
||||
|
||||
WA_BUSINESS_TOKEN_WEBHOOK=evolution
|
||||
WA_BUSINESS_URL=https://graph.facebook.com
|
||||
WA_BUSINESS_VERSION=v18.0
|
||||
WA_BUSINESS_LANGUAGE=pt_BR
|
||||
|
||||
SQS_ENABLED=false
|
||||
SQS_ACCESS_KEY_ID=
|
||||
SQS_SECRET_ACCESS_KEY=
|
||||
SQS_ACCOUNT_ID=
|
||||
SQS_REGION=
|
||||
|
||||
# Global Webhook Settings
|
||||
# Each instance's Webhook URL and events will be requested at the time it is created
|
||||
## Define a global webhook that will listen for enabled events from all instances
|
||||
WEBHOOK_GLOBAL_URL=''
|
||||
WEBHOOK_GLOBAL_ENABLED=false
|
||||
# With this option activated, you work with a url per webhook event, respecting the global url and the name of each event
|
||||
WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
|
||||
## Set the events you want to hear
|
||||
WEBHOOK_EVENTS_APPLICATION_STARTUP=false
|
||||
WEBHOOK_EVENTS_QRCODE_UPDATED=true
|
||||
WEBHOOK_EVENTS_MESSAGES_SET=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPSERT=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
WEBHOOK_EVENTS_PRESENCE_UPDATE=true
|
||||
WEBHOOK_EVENTS_CHATS_SET=true
|
||||
WEBHOOK_EVENTS_CHATS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CHATS_UPDATE=true
|
||||
WEBHOOK_EVENTS_CHATS_DELETE=true
|
||||
WEBHOOK_EVENTS_GROUPS_UPSERT=true
|
||||
WEBHOOK_EVENTS_GROUPS_UPDATE=true
|
||||
WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true
|
||||
WEBHOOK_EVENTS_CONNECTION_UPDATE=true
|
||||
WEBHOOK_EVENTS_LABELS_EDIT=true
|
||||
WEBHOOK_EVENTS_LABELS_ASSOCIATION=true
|
||||
WEBHOOK_EVENTS_CALL=true
|
||||
# This event fires every time a new token is requested via the refresh route
|
||||
WEBHOOK_EVENTS_NEW_JWT_TOKEN=false
|
||||
# This events is used with Typebot
|
||||
WEBHOOK_EVENTS_TYPEBOT_START=false
|
||||
WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
||||
# This event is used with Chama AI
|
||||
WEBHOOK_EVENTS_CHAMA_AI_ACTION=false
|
||||
# This event is used to send errors
|
||||
WEBHOOK_EVENTS_ERRORS=false
|
||||
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||
|
||||
# Name that will be displayed on smartphone connection
|
||||
CONFIG_SESSION_PHONE_CLIENT=EvolutionAPI
|
||||
# Browser Name = Chrome | Firefox | Edge | Opera | Safari
|
||||
CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
|
||||
# Set qrcode display limit
|
||||
QRCODE_LIMIT=30
|
||||
QRCODE_COLOR=#198754
|
||||
|
||||
# old | latest
|
||||
TYPEBOT_API_VERSION=latest
|
||||
TYPEBOT_KEEP_OPEN=false
|
||||
|
||||
#Chatwoot
|
||||
# If you leave this option as false, when deleting the message for everyone on WhatsApp, it will not be deleted on Chatwoot.
|
||||
CHATWOOT_MESSAGE_DELETE=false # false | true
|
||||
# If you leave this option as true, when sending a message in Chatwoot, the client's last message will be marked as read on WhatsApp.
|
||||
CHATWOOT_MESSAGE_READ=false # false | true
|
||||
# This db connection is used to import messages from whatsapp to chatwoot database
|
||||
CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=postgres://user:password@hostname:port/dbname
|
||||
CHATWOOT_IMPORT_DATABASE_PLACEHOLDER_MEDIA_MESSAGE=true
|
||||
|
||||
CACHE_REDIS_ENABLED=false
|
||||
CACHE_REDIS_URI=redis://redis:6379
|
||||
CACHE_REDIS_PREFIX_KEY=evolution
|
||||
CACHE_REDIS_TTL=604800
|
||||
CACHE_REDIS_SAVE_INSTANCES=false
|
||||
CACHE_LOCAL_ENABLED=false
|
||||
CACHE_LOCAL_TTL=604800
|
||||
|
||||
# Defines an authentication type for the api
|
||||
# We recommend using the apikey because it will allow you to use a custom token,
|
||||
# if you use jwt, a random token will be generated and may be expired and you will have to generate a new token
|
||||
# jwt or 'apikey'
|
||||
AUTHENTICATION_TYPE=apikey
|
||||
## Define a global apikey to access all instances.
|
||||
### OBS: This key must be inserted in the request header to create an instance.
|
||||
AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976
|
||||
AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true
|
||||
## Set the secret key to encrypt and decrypt your token and its expiration time
|
||||
# seconds - 3600s ===1h | zero (0) - never expires
|
||||
AUTHENTICATION_JWT_EXPIRIN_IN=0
|
||||
AUTHENTICATION_JWT_SECRET='L=0YWt]b2w[WF>#>:&E`'
|
||||
|
||||
LANGUAGE=en # pt-BR, en
|
||||
22
Docker/docker-compose.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: atendai/evolution-api
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
- evolution_store:/evolution/store
|
||||
env_file:
|
||||
- .env
|
||||
command: ['node', './dist/src/main.js']
|
||||
expose:
|
||||
- 8080
|
||||
|
||||
volumes:
|
||||
evolution_instances:
|
||||
evolution_store:
|
||||
119
Docker/evolution-api-all-services/.env.example
Normal file
@@ -0,0 +1,119 @@
|
||||
# Server URL - Set your application url
|
||||
SERVER_URL='http://localhost:8080'
|
||||
|
||||
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
|
||||
CORS_ORIGIN='*'
|
||||
CORS_METHODS='POST,GET,PUT,DELETE'
|
||||
CORS_CREDENTIALS=true
|
||||
|
||||
# Determine the logs to be displayed
|
||||
LOG_LEVEL='ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS'
|
||||
LOG_COLOR=true
|
||||
# Log Baileys - "fatal" | "error" | "warn" | "info" | "debug" | "trace"
|
||||
LOG_BAILEYS=error
|
||||
|
||||
# Determine how long the instance should be deleted from memory in case of no connection.
|
||||
# Default time: 5 minutes
|
||||
# If you don't even want an expiration, enter the value false
|
||||
DEL_INSTANCE=false
|
||||
DEL_TEMP_INSTANCES=true # Delete instances with status closed on start
|
||||
|
||||
# Temporary data storage
|
||||
STORE_MESSAGES=true
|
||||
STORE_MESSAGE_UP=true
|
||||
STORE_CONTACTS=true
|
||||
STORE_CHATS=true
|
||||
|
||||
# Set Store Interval in Seconds (7200 = 2h)
|
||||
CLEAN_STORE_CLEANING_INTERVAL=7200
|
||||
CLEAN_STORE_MESSAGES=true
|
||||
CLEAN_STORE_MESSAGE_UP=true
|
||||
CLEAN_STORE_CONTACTS=true
|
||||
CLEAN_STORE_CHATS=true
|
||||
|
||||
# Permanent data storage
|
||||
DATABASE_ENABLED=true
|
||||
DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin &
|
||||
readPreference=primary &
|
||||
ssl=false &
|
||||
directConnection=true
|
||||
DATABASE_CONNECTION_DB_PREFIX_NAME=evolution
|
||||
|
||||
# Choose the data you want to save in the application's database or store
|
||||
DATABASE_SAVE_DATA_INSTANCE=false
|
||||
DATABASE_SAVE_DATA_NEW_MESSAGE=false
|
||||
DATABASE_SAVE_MESSAGE_UPDATE=false
|
||||
DATABASE_SAVE_DATA_CONTACTS=false
|
||||
DATABASE_SAVE_DATA_CHATS=false
|
||||
|
||||
# Global Webhook Settings
|
||||
# Each instance's Webhook URL and events will be requested at the time it is created
|
||||
## Define a global webhook that will listen for enabled events from all instances
|
||||
WEBHOOK_GLOBAL_URL=''
|
||||
WEBHOOK_GLOBAL_ENABLED=false
|
||||
# With this option activated, you work with a url per webhook event, respecting the global url and the name of each event
|
||||
WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
|
||||
## Set the events you want to hear
|
||||
WEBHOOK_EVENTS_APPLICATION_STARTUP=false
|
||||
WEBHOOK_EVENTS_QRCODE_UPDATED=true
|
||||
WEBHOOK_EVENTS_MESSAGES_SET=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPSERT=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
WEBHOOK_EVENTS_PRESENCE_UPDATE=true
|
||||
WEBHOOK_EVENTS_CHATS_SET=true
|
||||
WEBHOOK_EVENTS_CHATS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CHATS_UPDATE=true
|
||||
WEBHOOK_EVENTS_CHATS_DELETE=true
|
||||
WEBHOOK_EVENTS_GROUPS_UPSERT=true
|
||||
WEBHOOK_EVENTS_GROUPS_UPDATE=true
|
||||
WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true
|
||||
WEBHOOK_EVENTS_CONNECTION_UPDATE=true
|
||||
WEBHOOK_EVENTS_LABELS_EDIT=true
|
||||
WEBHOOK_EVENTS_LABELS_ASSOCIATION=true
|
||||
# This event fires every time a new token is requested via the refresh route
|
||||
WEBHOOK_EVENTS_NEW_JWT_TOKEN=false
|
||||
|
||||
# Name that will be displayed on smartphone connection
|
||||
CONFIG_SESSION_PHONE_CLIENT='Evolution API'
|
||||
# Browser Name = chrome | firefox | edge | opera | safari
|
||||
CONFIG_SESSION_PHONE_NAME=chrome
|
||||
|
||||
# Set qrcode display limit
|
||||
QRCODE_LIMIT=30
|
||||
|
||||
CACHE_REDIS_ENABLED=false
|
||||
CACHE_REDIS_URI=redis://redis:6379
|
||||
CACHE_REDIS_PREFIX_KEY=evolution
|
||||
CACHE_REDIS_TTL=604800
|
||||
CACHE_REDIS_SAVE_INSTANCES=false
|
||||
CACHE_LOCAL_ENABLED=false
|
||||
CACHE_LOCAL_TTL=604800
|
||||
|
||||
# Defines an authentication type for the api
|
||||
# We recommend using the apikey because it will allow you to use a custom token,
|
||||
# if you use jwt, a random token will be generated and may be expired and you will have to generate a new token
|
||||
# jwt or 'apikey'
|
||||
AUTHENTICATION_TYPE='apikey'
|
||||
## Define a global apikey to access all instances.
|
||||
### OBS: This key must be inserted in the request header to create an instance.
|
||||
AUTHENTICATION_API_KEY='B6D711FCDE4D4FD5936544120E713976'
|
||||
AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true
|
||||
## Set the secret key to encrypt and decrypt your token and its expiration time
|
||||
# seconds - 3600s ===1h | zero (0) - never expires
|
||||
AUTHENTICATION_JWT_EXPIRIN_IN=0
|
||||
AUTHENTICATION_JWT_SECRET='L0YWtjb2w554WFqPG'
|
||||
# Set the instance name and webhook url to create an instance in init the application
|
||||
# With this option activated, you work with a url per webhook event, respecting the local url and the name of each event
|
||||
# container or server
|
||||
AUTHENTICATION_INSTANCE_MODE=server
|
||||
# if you are using container mode, set the container name and the webhook url to default instance
|
||||
AUTHENTICATION_INSTANCE_NAME=evolution
|
||||
AUTHENTICATION_INSTANCE_WEBHOOK_URL=''
|
||||
AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1
|
||||
AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456
|
||||
AUTHENTICATION_INSTANCE_CHATWOOT_URL=''
|
||||
91
Docker/evolution-api-all-services/docker-compose.yaml
Normal file
@@ -0,0 +1,91 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
|
||||
mongodb:
|
||||
container_name: mongodb
|
||||
image: mongo
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 27017:27017
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=root
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- evolution_mongodb_data:/data/db
|
||||
- evolution_mongodb_configdb:/data/configdb
|
||||
expose:
|
||||
- 27017
|
||||
|
||||
mongo-express:
|
||||
container_name: mongodb-express
|
||||
image: mongo-express
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 8081:8081
|
||||
depends_on:
|
||||
- mongodb
|
||||
environment:
|
||||
ME_CONFIG_BASICAUTH_USERNAME: root
|
||||
ME_CONFIG_BASICAUTH_PASSWORD: root
|
||||
ME_CONFIG_MONGODB_SERVER: mongodb
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME: root
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: root
|
||||
links:
|
||||
- mongodb
|
||||
|
||||
redis:
|
||||
container_name: redis
|
||||
image: redis:latest
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 6379:6379
|
||||
command: >
|
||||
redis-server
|
||||
--port 6379
|
||||
--appendonly yes
|
||||
volumes:
|
||||
- evolution_redis:/data
|
||||
|
||||
rebrow:
|
||||
container_name: rebrow
|
||||
image: marian/rebrow
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- redis
|
||||
ports:
|
||||
- 5001:5001
|
||||
links:
|
||||
- redis
|
||||
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: atendai/evolution-api
|
||||
restart: always
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
- evolution_store:/evolution/store
|
||||
env_file:
|
||||
- .env
|
||||
command: ['node', './dist/src/main.js']
|
||||
expose:
|
||||
- 8080
|
||||
|
||||
volumes:
|
||||
evolution_mongodb_data:
|
||||
evolution_mongodb_configdb:
|
||||
evolution_redis:
|
||||
evolution_instances:
|
||||
evolution_store:
|
||||
|
||||
networks:
|
||||
evolution-net:
|
||||
external: true
|
||||
|
||||
42
Docker/mongodb/docker-compose.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
container_name: mongodb
|
||||
image: mongo
|
||||
restart: always
|
||||
ports:
|
||||
- 27017:27017
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=root
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- evolution_mongodb_data:/data/db
|
||||
- evolution_mongodb_configdb:/data/configdb
|
||||
expose:
|
||||
- 27017
|
||||
|
||||
mongo-express:
|
||||
image: mongo-express
|
||||
environment:
|
||||
ME_CONFIG_BASICAUTH_USERNAME: root
|
||||
ME_CONFIG_BASICAUTH_PASSWORD: root
|
||||
ME_CONFIG_MONGODB_SERVER: mongodb
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME: root
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: root
|
||||
ports:
|
||||
- 8081:8081
|
||||
links:
|
||||
- mongodb
|
||||
|
||||
volumes:
|
||||
evolution_mongodb_data:
|
||||
evolution_mongodb_configdb:
|
||||
|
||||
|
||||
networks:
|
||||
evolution-net:
|
||||
name: evolution-net
|
||||
driver: bridge
|
||||
21
Docker/redis/docker-compose.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
command: >
|
||||
redis-server --port 6379 --appendonly yes
|
||||
volumes:
|
||||
- evolution_redis:/data
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
volumes:
|
||||
evolution_redis:
|
||||
|
||||
|
||||
networks:
|
||||
evolution-net:
|
||||
name: evolution-net
|
||||
driver: bridge
|
||||
215
Dockerfile
@@ -1,87 +1,156 @@
|
||||
FROM node:16.18-alpine
|
||||
FROM node:20.7.0-alpine AS builder
|
||||
|
||||
LABEL version="1.7.3" description="Api to control whatsapp features through http requests."
|
||||
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
|
||||
LABEL contact="contato@agenciadgcode.com"
|
||||
|
||||
RUN apk update && apk upgrade && \
|
||||
apk add --no-cache git
|
||||
apk add --no-cache git tzdata ffmpeg wget curl
|
||||
|
||||
WORKDIR /evolution
|
||||
|
||||
COPY ./package.json .
|
||||
|
||||
ENV DOCKER_ENV=true
|
||||
|
||||
ENV CORS_ORIGIN="*"
|
||||
ENV CORS_METHODS="POST,GET,PUT,DELETE"
|
||||
ENV CORS_CREDENTIALS=true
|
||||
|
||||
ENV LOG_LEVEL="ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK"
|
||||
ENV LOG_COLOR=true
|
||||
|
||||
ENV DEL_INSTANCE=false
|
||||
|
||||
ENV STORE_CLEANING_INTERVAL=7200
|
||||
ENV STORE_MESSAGE=true
|
||||
ENV STORE_CONTACTS=true
|
||||
ENV STORE_CHATS=true
|
||||
|
||||
ENV DATABASE_ENABLED=$DATABASE_ENABLED
|
||||
ENV DATABASE_CONNECTION_URI=$DATABASE_CONNECTION_URI
|
||||
ENV DATABASE_CONNECTION_DB_PREFIX_NAME=$DATABASE_CONNECTION_DB_PREFIX_NAME
|
||||
ENV DATABASE_SAVE_DATA_INSTANCE=$DATABASE_SAVE_DATA_INSTANCE
|
||||
ENV DATABASE_SAVE_DATA_OLD_MESSAGE=$DATABASE_SAVE_DATA_OLD_MESSAGE
|
||||
ENV DATABASE_SAVE_DATA_NEW_MESSAGE=$DATABASE_SAVE_DATA_NEW_MESSAGE
|
||||
ENV DATABASE_SAVE_MESSAGE_UPDATE=$DATABASE_SAVE_MESSAGE_UPDATE
|
||||
ENV DATABASE_SAVE_DATA_CONTACTS=$DATABASE_SAVE_DATA_CONTACTS
|
||||
ENV DATABASE_SAVE_DATA_CHATS=$DATABASE_SAVE_DATA_CHATS
|
||||
|
||||
ENV REDIS_ENABLED=$REDIS_ENABLED
|
||||
ENV REDIS_URI=$REDIS_URI
|
||||
|
||||
ENV WEBHOOK_GLOBAL_URL=$WEBHOOK_GLOBAL_URL
|
||||
ENV WEBHOOK_GLOBAL_ENABLED=true
|
||||
ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=$WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS
|
||||
|
||||
ENV WEBHOOK_EVENTS_STATUS_INSTANCE=$WEBHOOK_EVENTS_STATUS_INSTANCE
|
||||
ENV WEBHOOK_EVENTS_APPLICATION_STARTUP=$WEBHOOK_EVENTS_APPLICATION_STARTUP
|
||||
ENV WEBHOOK_EVENTS_QRCODE_UPDATED=$WEBHOOK_EVENTS_QRCODE_UPDATED
|
||||
ENV WEBHOOK_EVENTS_MESSAGES_SET=$WEBHOOK_EVENTS_MESSAGES_SET
|
||||
ENV WEBHOOK_EVENTS_MESSAGES_UPDATE=$WEBHOOK_EVENTS_MESSAGES_UPDATE
|
||||
ENV WEBHOOK_EVENTS_MESSAGES_UPSERT=$WEBHOOK_EVENTS_MESSAGES_UPSERT
|
||||
ENV WEBHOOK_EVENTS_SEND_MESSAGE=$WEBHOOK_EVENTS_SEND_MESSAGE
|
||||
ENV WEBHOOK_EVENTS_CONTACTS_SET=$WEBHOOK_EVENTS_CONTACTS_SET
|
||||
ENV WEBHOOK_EVENTS_CONTACTS_UPSERT=$WEBHOOK_EVENTS_CONTACTS_UPSERT
|
||||
ENV WEBHOOK_EVENTS_CONTACTS_UPDATE=$WEBHOOK_EVENTS_CONTACTS_UPDATE
|
||||
ENV WEBHOOK_EVENTS_PRESENCE_UPDATE=$WEBHOOK_EVENTS_PRESENCE_UPDATE
|
||||
ENV WEBHOOK_EVENTS_CHATS_SET=$WEBHOOK_EVENTS_CHATS_SET
|
||||
ENV WEBHOOK_EVENTS_CHATS_UPSERT=$WEBHOOK_EVENTS_CHATS_UPSERT
|
||||
ENV WEBHOOK_EVENTS_CHATS_UPDATE=$WEBHOOK_EVENTS_CHATS_UPDATE
|
||||
ENV WEBHOOK_EVENTS_CONNECTION_UPDATE=$WEBHOOK_EVENTS_CONNECTION_UPDATE
|
||||
ENV WEBHOOK_EVENTS_GROUPS_UPSERT=$WEBHOOK_EVENTS_GROUPS_UPSERT
|
||||
ENV WEBHOOK_EVENTS_GROUPS_UPDATE=$WEBHOOK_EVENTS_GROUPS_UPDATE
|
||||
ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=$WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE
|
||||
|
||||
ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=true
|
||||
|
||||
ENV CONFIG_SESSION_PHONE_CLIENT="Evolution API"
|
||||
ENV CONFIG_SESSION_PHONE_NAME="Chrome"
|
||||
|
||||
ENV QRCODE_LIMIT=30
|
||||
|
||||
ENV AUTHENTICATION_TYPE="apikey"
|
||||
|
||||
ENV AUTHENTICATION_API_KEY=$AUTHENTICATION_API_KEY
|
||||
|
||||
ENV AUTHENTICATION_JWT_EXPIRIN_IN=0
|
||||
ENV AUTHENTICATION_JWT_SECRET="L0YWtjb2w554WFqPG"
|
||||
|
||||
ENV AUTHENTICATION_INSTANCE_NAME=$AUTHENTICATION_INSTANCE_NAME
|
||||
ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL=$AUTHENTICATION_INSTANCE_WEBHOOK_URL
|
||||
ENV AUTHENTICATION_INSTANCE_MODE=$AUTHENTICATION_INSTANCE_MODE
|
||||
ENV AUTHENTICATION_INSTANCE_WEBHOOK_BY_EVENTS=$AUTHENTICATION_INSTANCE_WEBHOOK_BY_EVENTS
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20.7.0-alpine AS final
|
||||
|
||||
ENV TZ=America/Sao_Paulo
|
||||
ENV DOCKER_ENV=true
|
||||
|
||||
ENV SERVER_TYPE=http
|
||||
ENV SERVER_PORT=8080
|
||||
ENV SERVER_URL=http://localhost:8080
|
||||
|
||||
ENV CORS_ORIGIN=*
|
||||
ENV CORS_METHODS=POST,GET,PUT,DELETE
|
||||
ENV CORS_CREDENTIALS=true
|
||||
|
||||
ENV LOG_LEVEL=ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS
|
||||
ENV LOG_COLOR=true
|
||||
ENV LOG_BAILEYS=error
|
||||
|
||||
ENV DEL_INSTANCE=false
|
||||
ENV DEL_TEMP_INSTANCES=true
|
||||
|
||||
ENV STORE_MESSAGES=true
|
||||
ENV STORE_MESSAGE_UP=true
|
||||
ENV STORE_CONTACTS=true
|
||||
ENV STORE_CHATS=true
|
||||
|
||||
ENV CLEAN_STORE_CLEANING_INTERVAL=7200
|
||||
ENV CLEAN_STORE_MESSAGES=true
|
||||
ENV CLEAN_STORE_MESSAGE_UP=true
|
||||
ENV CLEAN_STORE_CONTACTS=true
|
||||
ENV CLEAN_STORE_CHATS=true
|
||||
|
||||
ENV DATABASE_ENABLED=false
|
||||
ENV DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true
|
||||
ENV DATABASE_CONNECTION_DB_PREFIX_NAME=evolution
|
||||
|
||||
ENV DATABASE_SAVE_DATA_INSTANCE=false
|
||||
ENV DATABASE_SAVE_DATA_NEW_MESSAGE=false
|
||||
ENV DATABASE_SAVE_MESSAGE_UPDATE=false
|
||||
ENV DATABASE_SAVE_DATA_CONTACTS=false
|
||||
ENV DATABASE_SAVE_DATA_CHATS=false
|
||||
|
||||
ENV RABBITMQ_ENABLED=false
|
||||
ENV RABBITMQ_MODE=global
|
||||
ENV RABBITMQ_EXCHANGE_NAME=evolution_exchange
|
||||
ENV RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672
|
||||
|
||||
ENV WEBSOCKET_ENABLED=false
|
||||
ENV WEBSOCKET_GLOBAL_EVENTS=false
|
||||
|
||||
ENV WA_BUSINESS_TOKEN_WEBHOOK=evolution
|
||||
ENV WA_BUSINESS_URL=https://graph.facebook.com
|
||||
ENV WA_BUSINESS_VERSION=v18.0
|
||||
ENV WA_BUSINESS_LANGUAGE=pt_BR
|
||||
|
||||
ENV SQS_ENABLED=false
|
||||
ENV SQS_ACCESS_KEY_ID=
|
||||
ENV SQS_SECRET_ACCESS_KEY=
|
||||
ENV SQS_ACCOUNT_ID=
|
||||
ENV SQS_REGION=
|
||||
|
||||
ENV WEBHOOK_GLOBAL_URL=
|
||||
ENV WEBHOOK_GLOBAL_ENABLED=false
|
||||
|
||||
ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
|
||||
|
||||
ENV WEBHOOK_EVENTS_APPLICATION_STARTUP=false
|
||||
ENV WEBHOOK_EVENTS_INSTANCE_CREATE=false
|
||||
ENV WEBHOOK_EVENTS_INSTANCE_DELETE=false
|
||||
ENV WEBHOOK_EVENTS_QRCODE_UPDATED=true
|
||||
ENV WEBHOOK_EVENTS_MESSAGES_SET=true
|
||||
ENV WEBHOOK_EVENTS_MESSAGES_UPSERT=true
|
||||
ENV WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
ENV WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
ENV WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
ENV WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
ENV WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
ENV WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
ENV WEBHOOK_EVENTS_PRESENCE_UPDATE=true
|
||||
ENV WEBHOOK_EVENTS_CHATS_SET=true
|
||||
ENV WEBHOOK_EVENTS_CHATS_UPSERT=true
|
||||
ENV WEBHOOK_EVENTS_CHATS_UPDATE=true
|
||||
ENV WEBHOOK_EVENTS_CHATS_DELETE=true
|
||||
ENV WEBHOOK_EVENTS_GROUPS_UPSERT=true
|
||||
ENV WEBHOOK_EVENTS_GROUPS_UPDATE=true
|
||||
ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true
|
||||
ENV WEBHOOK_EVENTS_CONNECTION_UPDATE=true
|
||||
ENV WEBHOOK_EVENTS_LABELS_EDIT=true
|
||||
ENV WEBHOOK_EVENTS_LABELS_ASSOCIATION=true
|
||||
ENV WEBHOOK_EVENTS_CALL=true
|
||||
|
||||
ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=false
|
||||
|
||||
ENV WEBHOOK_EVENTS_TYPEBOT_START=false
|
||||
ENV WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
||||
|
||||
ENV WEBHOOK_EVENTS_CHAMA_AI_ACTION=false
|
||||
|
||||
ENV WEBHOOK_EVENTS_ERRORS=false
|
||||
ENV WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||
|
||||
ENV CONFIG_SESSION_PHONE_CLIENT=EvolutionAPI
|
||||
ENV CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
|
||||
ENV QRCODE_LIMIT=30
|
||||
ENV QRCODE_COLOR=#198754
|
||||
|
||||
ENV TYPEBOT_API_VERSION=latest
|
||||
|
||||
ENV CACHE_REDIS_ENABLED=false
|
||||
ENV CACHE_REDIS_URI=redis://redis:6379
|
||||
ENV CACHE_REDIS_PREFIX_KEY=evolution
|
||||
ENV CACHE_REDIS_TTL=604800
|
||||
ENV CACHE_REDIS_SAVE_INSTANCES=false
|
||||
ENV CACHE_LOCAL_ENABLED=false
|
||||
ENV CACHE_LOCAL_TTL=604800
|
||||
|
||||
ENV AUTHENTICATION_TYPE=apikey
|
||||
|
||||
ENV AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976
|
||||
ENV AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true
|
||||
|
||||
ENV AUTHENTICATION_JWT_EXPIRIN_IN=0
|
||||
ENV AUTHENTICATION_JWT_SECRET='L=0YWt]b2w[WF>#>:&E`'
|
||||
|
||||
ENV AUTHENTICATION_INSTANCE_MODE=server
|
||||
|
||||
ENV AUTHENTICATION_INSTANCE_NAME=evolution
|
||||
ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL=<url>
|
||||
ENV AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1
|
||||
ENV AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456
|
||||
ENV AUTHENTICATION_INSTANCE_CHATWOOT_URL=<url>
|
||||
|
||||
WORKDIR /evolution
|
||||
|
||||
COPY --from=builder /evolution .
|
||||
|
||||
CMD [ "node", "./dist/src/main.js" ]
|
||||
|
||||
1
Extras/appsmith/manager.json
Normal file
241
Extras/chatwoot/configurar_admin.json
Normal file
@@ -0,0 +1,241 @@
|
||||
{
|
||||
"name": "[Evolution] Configurar Admin",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "CHATWOOT_ADMIN_USER_TOKEN"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_url",
|
||||
"value": "https://CHATWOOT_URL"
|
||||
},
|
||||
{
|
||||
"name": "n8n_url",
|
||||
"value": "https://N8N_URL"
|
||||
},
|
||||
{
|
||||
"name": "organization",
|
||||
"value": "ORGANIZATION_NAME"
|
||||
},
|
||||
{
|
||||
"name": "logo",
|
||||
"value": "ORGANIZATION_LOGO"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "7a89a538-2cae-4032-8896-09627c07bc68",
|
||||
"name": "Info Base",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
620,
|
||||
480
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/1/contacts/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json[\"api_access_token\"] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"inbox_id\": {{ $('Cria Inbox Start').item.json[\"id\"] }},\n \"name\": \"Bot {{ $('Info Base').item.json[\"organization\"] }}\",\n \"phone_number\": \"+123456\",\n \"avatar_url\": \"{{ $('Info Base').item.json[\"logo\"] }}\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "12a39df3-6b95-4f83-a0bc-50b25adaca7f",
|
||||
"name": "Cria Contato Bot",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
1020,
|
||||
480
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/1/inboxes/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json[\"api_access_token\"] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"name\": \"Start {{ $('Info Base').item.json[\"organization\"] }}\",\n \"channel\": {\n \"type\": \"api\",\n \"website_url\": \"\"\n }\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "bed7c54d-e232-4fe4-9584-0515e9679868",
|
||||
"name": "Cria Inbox Start",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
820,
|
||||
480
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "36ada769-a757-4193-989b-0cc4ea504b80",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
420,
|
||||
480
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/1/automation_rules/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json[\"api_access_token\"] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"name\": \"Create Company Chatwoot\",\n \"description\": \"Create Company Chatwoot\",\n \"event_name\": \"message_created\",\n \"active\": true,\n \"actions\": \n [\n {\n \"action_name\": \"send_webhook_event\",\n \"action_params\": [\"{{ $('Info Base').item.json[\"n8n_url\"] }}/webhook/criadorchatwoot\"]\n }\n ],\n \"conditions\": \n [\n {\n \"attribute_key\": \"content\",\n \"filter_operator\": \"contains\",\n \"query_operator\": \"and\",\n \"values\": [\"Tema Criador de Empresa:\"]\n },\n {\n \"attribute_key\": \"phone_number\",\n \"filter_operator\": \"equal_to\",\n \"values\": [\"+123456\"]\n }\n ]\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "f5bbb285-71a8-4c58-a4d7-e56002d697f0",
|
||||
"name": "Cria Automação Empresas",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
1220,
|
||||
480
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/1/automation_rules/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json[\"api_access_token\"] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"name\": \"Create Inbox {{ $('Info Base').item.json[\"organization\"] }}\",\n \"description\": \"Create Inbox {{ $('Info Base').item.json[\"organization\"] }}\",\n \"event_name\": \"message_created\",\n \"active\": true,\n \"actions\": \n [\n {\n \"action_name\": \"send_webhook_event\",\n \"action_params\": [\"{{ $('Info Base').item.json[\"n8n_url\"] }}/webhook/inbox_whatsapp?utoken={{ $('Info Base').item.json[\"api_access_token\"] }}&organization={{ $('Info Base').item.json[\"organization\"] }}\"]\n }\n ],\n \"conditions\": \n [\n {\n \"attribute_key\": \"content\",\n \"filter_operator\": \"contains\",\n \"query_operator\": \"and\",\n \"values\": [\"start:\"]\n },\n \n {\n \"attribute_key\": \"phone_number\",\n \"filter_operator\": \"equal_to\",\n \"query_operator\": \"or\",\n \"values\": [\"+123456\"]\n },\n\n\n {\n \"attribute_key\": \"content\",\n \"filter_operator\": \"contains\",\n \"query_operator\": \"and\",\n \"values\": [\"new_instance:\"]\n },\n {\n \"attribute_key\": \"phone_number\",\n \"filter_operator\": \"equal_to\",\n \"values\": [\"+123456\"]\n }\n ]\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "a36bebdc-a318-40a2-8532-c7f476f8adb7",
|
||||
"name": "Cria Automação Inboxes",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
1420,
|
||||
480
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Workflow Para Configurar admin\n**Aqui você prepara o Chatwoot Principal com um usuário (Superadmin) que poderá criar empresas e caixas de entrada**\n**Instruções**\n**No node Info Base, configure as variáveis de seu Chatwoot e N8N**\n**Obs: A variável api_access_token é o token do usuário que irá poder criar as empresas**",
|
||||
"width": 894.6435495898575
|
||||
},
|
||||
"id": "db66e867-e9f4-452d-b521-725eeac652c8",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
420,
|
||||
280
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Info Base": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Inbox Start",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Contato Bot": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Automação Empresas",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Inbox Start": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Contato Bot",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Info Base",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Automação Empresas": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Automação Inboxes",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "78f155dc-7809-4bfc-9282-63f49b07fc4d",
|
||||
"id": "BSATyGpGWLR4ZwNm",
|
||||
"meta": {
|
||||
"instanceId": "4ff16e963c7f5197d7e99e6239192860914312fea0ce2a9a7fd14d74a0a0e906"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
456
Extras/chatwoot/criador_de_empresas.json
Normal file
@@ -0,0 +1,456 @@
|
||||
{
|
||||
"name": "[Evolution] Criador de Empresas",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "criadorchatwoot",
|
||||
"options": {}
|
||||
},
|
||||
"id": "5a47c10a-e43c-4fa5-baad-4b6cc511bfcd",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1420,
|
||||
860
|
||||
],
|
||||
"webhookId": "6fe428e3-1752-453c-9358-abf18b793387"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/platform/api/v1/accounts",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json[\"api_access_token\"] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "={{ $json.name_company }}"
|
||||
},
|
||||
{
|
||||
"name": "locale",
|
||||
"value": "pt_BR"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "8295c119-3a96-424e-9386-43d75f6816f5",
|
||||
"name": "Cria Conta",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
2020,
|
||||
860
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/platform/api/v1/users",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json[\"api_access_token\"] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "={{ $('Info Base').item.json.name_admin }}"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"value": "={{ $('Info Base').item.json[\"email\"] }}"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"value": "={{ $('Info Base').item.json[\"password\"] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "4fe5007a-3a6b-490a-a446-e45cc168189f",
|
||||
"name": "Cria Usuario",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
2220,
|
||||
860
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/platform/api/v1/accounts/{{ $node[\"Cria Conta\"].json[\"id\"] }}/account_users",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json[\"api_access_token\"] }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "user_id",
|
||||
"value": "={{ $node[\"Cria Usuario\"].json[\"id\"] }}"
|
||||
},
|
||||
{
|
||||
"name": "role",
|
||||
"value": "administrator"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "848c55e2-5678-4291-9602-c94d994da95b",
|
||||
"name": "Add Usuario a Conta",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
2420,
|
||||
860
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fromEmail": "={{ $('Info Base').item.json[\"from_email\"] }}",
|
||||
"toEmail": "={{ $('LimpaDados').item.json.email }}",
|
||||
"subject": "=Bem vindo à {{ $('Info Base').item.json[\"organization\"] }}",
|
||||
"text": "=Olá seja bem vindo:\n\nAbaixo segue seus dados de acesso:\n\nURL: {{ $('Info Base').item.json[\"chatwoot_url\"] }}\n\nuser: {{ $('LimpaDados').item.json[\"email\"] }}\n\nSenha: {{ $('LimpaDados').item.json[\"password\"] }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "27f3b24f-1cf2-4d0d-a354-ecba066059f6",
|
||||
"name": "Send Email",
|
||||
"type": "n8n-nodes-base.emailSend",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
3220,
|
||||
860
|
||||
],
|
||||
"credentials": {
|
||||
"smtp": {
|
||||
"id": "6BxluEUV8zrXKoVG",
|
||||
"name": "[Dgcode] SMTP"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "CHATWOOT_PLATFORM_TOKEN"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_url",
|
||||
"value": "https://CHATWOOT_URL"
|
||||
},
|
||||
{
|
||||
"name": "n8n_url",
|
||||
"value": "https://N8N_URL"
|
||||
},
|
||||
{
|
||||
"name": "organization",
|
||||
"value": "ORGANIZATION_NAME"
|
||||
},
|
||||
{
|
||||
"name": "logo",
|
||||
"value": "ORGANIZATION_LOGO"
|
||||
},
|
||||
{
|
||||
"name": "from_email",
|
||||
"value": "FROM_EMAIL"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"value": "={{ $json.name_company }}"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"value": "={{ $json.email }}"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"value": "={{ $json.password }}"
|
||||
},
|
||||
{
|
||||
"name": "name_company",
|
||||
"value": "={{ $json.name_company }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "38b4069d-e51e-4db7-933f-941b1be6d124",
|
||||
"name": "Info Base",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1820,
|
||||
860
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"keepOnlySet": true,
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "name_admin",
|
||||
"value": "={{$node[\"Webhook\"].json[\"body\"][\"messages\"][0][\"content\"].match(/Nome Usuario Administrador: ([^\\n]+)/)[1];}}"
|
||||
},
|
||||
{
|
||||
"name": "name_company",
|
||||
"value": "={{$node[\"Webhook\"].json[\"body\"][\"messages\"][0][\"content\"].match(/Nome da Empresa: ([^\\n]+)/)[1];}}"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"value": "={{$node[\"Webhook\"].json[\"body\"][\"messages\"][0][\"content\"].match(/Email: ([^\\s]+)/)[1];}}"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"value": "={{$node[\"Webhook\"].json[\"body\"][\"messages\"][0][\"content\"].match(/Senha: ([^\\s]+)/)[1];}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "28e29e73-aadc-49ca-bd6d-b57ee0160a21",
|
||||
"name": "LimpaDados",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1620,
|
||||
860
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/{{ $('Add Usuario a Conta').item.json.account_id }}/contacts/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Cria Usuario').item.json.access_token }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"inbox_id\": {{ $('Cria Inbox Start').item.json[\"id\"] }},\n \"name\": \"Bot {{ $('Info Base').item.json[\"organization\"] }}\",\n \"phone_number\": \"+123456\",\n \"avatar_url\": \"{{ $('Info Base').item.json[\"logo\"] }}\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "bb671443-bdb4-4f56-99af-f0baef246a3e",
|
||||
"name": "Cria Contato Bot",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
2820,
|
||||
860
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/{{ $('Add Usuario a Conta').item.json.account_id }}/automation_rules/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Cria Usuario').item.json.access_token }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"name\": \"Create Inbox {{ $('Info Base').item.json[\"organization\"] }}\",\n \"description\": \"Create Inbox {{ $('Info Base').item.json[\"organization\"] }}\",\n \"event_name\": \"message_created\",\n \"active\": true,\n \"actions\": \n [\n {\n \"action_name\": \"send_webhook_event\",\n \"action_params\": [\"{{ $('Info Base').item.json[\"n8n_url\"] }}/webhook/inbox_whatsapp?utoken={{ $('Cria Usuario').item.json.access_token }}&organization={{ $('Info Base').item.json[\"organization\"] }}\"]\n }\n ],\n \"conditions\": \n [\n {\n \"attribute_key\": \"content\",\n \"filter_operator\": \"contains\",\n \"query_operator\": \"and\",\n \"values\": [\"start:\"]\n },\n {\n \"attribute_key\": \"phone_number\",\n \"filter_operator\": \"equal_to\",\n \"query_operator\": \"or\",\n \"values\": [\"+123456\"]\n },\n {\n \"attribute_key\": \"content\",\n \"filter_operator\": \"contains\",\n \"query_operator\": \"and\",\n \"values\": [\"new_instance:\"]\n },\n {\n \"attribute_key\": \"phone_number\",\n \"filter_operator\": \"equal_to\",\n \"values\": [\"+123456\"]\n }\n ]\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "e016a2af-b212-4e00-a3ff-8cd03530aa06",
|
||||
"name": "Cria Automação",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
3020,
|
||||
860
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/{{ $json.account_id }}/inboxes/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Cria Usuario').item.json.access_token }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"name\": \"Start {{ $('Info Base').item.json[\"organization\"] }}\",\n \"channel\": {\n \"type\": \"api\",\n \"website_url\": \"\"\n }\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "d3c42148-8920-4c98-a874-eb7113f2dd22",
|
||||
"name": "Cria Inbox Start",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
2620,
|
||||
860
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Workflow Criador de Empresas\n**Cria Contas (Empresas) e Usuários através de tema**\n**Instruções**\n**No node Info Base, configure as variáveis de seu Chatwoot e N8N**\n**Obs: A variável api_access_token é o token PlatformApp encontrado no acesso ao Super Admin**\n**Tema para criar novas empresa:**\n\nTema Criador de Empresa:\n\nNome Usuario Administrador: Joao Linhares\nNome da Empresa: Oficina Linhates\nEmail: machineteste24@gmail.com\nSenha: Mfcd62!!",
|
||||
"height": 304.02684563758396,
|
||||
"width": 1129.7777777777778
|
||||
},
|
||||
"id": "d07516c0-4c8e-43ab-ba86-c8d063b09be5",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1420,
|
||||
520
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "LimpaDados",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Conta": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Usuario",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Usuario": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Add Usuario a Conta",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Add Usuario a Conta": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Inbox Start",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Info Base": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Conta",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"LimpaDados": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Info Base",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Contato Bot": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Automação",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Automação": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send Email",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Inbox Start": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Contato Bot",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "3ffd6d3f-6966-4de4-af8f-1fda464bc1b8",
|
||||
"id": "79R6qQDtfyCwgYjJ",
|
||||
"meta": {
|
||||
"instanceId": "4ff16e963c7f5197d7e99e6239192860914312fea0ce2a9a7fd14d74a0a0e906"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
510
Extras/chatwoot/criador_de_inbox.json
Normal file
@@ -0,0 +1,510 @@
|
||||
{
|
||||
"name": "[Evolution] Criador de Inbox",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "inbox_whatsapp",
|
||||
"options": {}
|
||||
},
|
||||
"id": "8205b929-73e9-456a-9b0d-e1474991663a",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
320,
|
||||
300
|
||||
],
|
||||
"webhookId": "cf37002d-3869-4bb1-af3a-739fdd3c1756"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $json.evolution_url }}/instance/create",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "apikey",
|
||||
"value": "={{ $json.global_api_key }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "instanceName",
|
||||
"value": "={{ $json.instance_name }}"
|
||||
},
|
||||
{
|
||||
"name": "qrcode",
|
||||
"value": "={{ $json.qrcode }}"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_account_id",
|
||||
"value": "={{ $json.chatwoot_account_id }}"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_token",
|
||||
"value": "={{ $json.chatwoot_token }}"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_url",
|
||||
"value": "={{ $json.chatwoot_url }}"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_sign_msg",
|
||||
"value": "={{ $json.chatwoot_sign_msg }}"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_reopen_conversation",
|
||||
"value": "={{ $json.chatwoot_reopen_conversation }}"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_conversation_pending",
|
||||
"value": "={{ $json.chatwoot_conversation_pending }}"
|
||||
},
|
||||
{
|
||||
"name": "reject_call",
|
||||
"value": "={{ $json.reject_call }}"
|
||||
},
|
||||
{
|
||||
"name": "msg_call",
|
||||
"value": "={{ $json.msg_call }}"
|
||||
},
|
||||
{
|
||||
"name": "groups_ignore",
|
||||
"value": "={{ $json.groups_ignore }}"
|
||||
},
|
||||
{
|
||||
"name": "always_online",
|
||||
"value": "={{ $json.always_online }}"
|
||||
},
|
||||
{
|
||||
"name": "read_messages",
|
||||
"value": "={{ $json.read_messages }}"
|
||||
},
|
||||
{
|
||||
"name": "read_status",
|
||||
"value": "={{ $json.read_status }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "275aa370-2fdb-42f4-844a-2fb3051301bd",
|
||||
"name": "Cria Instancia",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/{{ $('Info Base').item.json[\"chatwoot_account_id\"] }}/inboxes/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json.chatwoot_token }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "e4650812-ba0a-4f72-8bd8-a235eca4b2de",
|
||||
"name": "Lista Inboxes",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
980,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Workflow Para Criar Inbox\n**Aqui você configura a comunicação entre o chatwoot e a Evolution API para criar novas instâncias a partir do chatwoot**\n**Instruções**\n**No node Info Base, configure as variáveis de seu Chatwoot e Evolution API**",
|
||||
"width": 1129.7777777777778
|
||||
},
|
||||
"id": "aa763d9e-d973-44fc-8399-277bb24718a5",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
320,
|
||||
80
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"keepOnlySet": true,
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "chatwoot_url",
|
||||
"value": "CHATWOOT_URL"
|
||||
},
|
||||
{
|
||||
"name": "evolution_url",
|
||||
"value": "EVOLUTION_URL"
|
||||
},
|
||||
{
|
||||
"name": "global_api_key",
|
||||
"value": "EVOLUTION_GLOBAL_API_KEY"
|
||||
},
|
||||
{
|
||||
"name": "organization",
|
||||
"value": "={{ $json.query.organization }}"
|
||||
},
|
||||
{
|
||||
"name": "instance_name",
|
||||
"value": "={{ $json.body.messages[0].content.split(':')[1] }}-cwId-{{ $json.body.messages[0].account_id }}"
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_token",
|
||||
"value": "={{ $json.query.utoken }}"
|
||||
},
|
||||
{
|
||||
"name": "msg_call",
|
||||
"value": "Não aceitamos chamadas, por favor deixe uma mensagem!"
|
||||
}
|
||||
],
|
||||
"boolean": [
|
||||
{
|
||||
"name": "qrcode",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_sign_msg",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_reopen_conversation",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "chatwoot_conversation_pending"
|
||||
},
|
||||
{
|
||||
"name": "reject_call",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "groups_ignore"
|
||||
},
|
||||
{
|
||||
"name": "always_online",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "read_messages",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "read_status"
|
||||
}
|
||||
],
|
||||
"number": [
|
||||
{
|
||||
"name": "chatwoot_account_id",
|
||||
"value": "={{ $json.body.messages[0].account_id }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "297df325-ecc4-4a34-817c-092d16d5753b",
|
||||
"name": "Info Base",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
540,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"string": [
|
||||
{
|
||||
"value1": "={{ $json.name }}",
|
||||
"value2": "=Start {{ $('Info Base').item.json[\"organization\"] }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a8d955e6-ac51-4316-aeec-09d4d65e943a",
|
||||
"name": "é Start Inbox?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1660,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"batchSize": 1,
|
||||
"options": {}
|
||||
},
|
||||
"id": "0d2d2194-aa4a-4241-9022-217d88bb581f",
|
||||
"name": "Split In Batches",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1420,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"string": [
|
||||
{
|
||||
"value1": "={{ $json.name }}",
|
||||
"value2": "={{ $('Webhook').item.json[\"body\"][\"messages\"][0][\"content\"].split(':')[1] }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "0bfbc2cb-eff5-423c-bd3a-b266aaf6a943",
|
||||
"name": "é_pre-existente?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1900,
|
||||
340
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/{{ $('Info Base').item.json[\"chatwoot_account_id\"] }}/inboxes/{{ $json.id }}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json.chatwoot_token }}"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n\"channel\": {\n\"webhook_url\": \"{{ $('Info Base').item.json[\"evolution_url\"] }}/chatwoot/webhook/{{ encodeURIComponent($('Info Base').item.json[\"instance_name\"]) }}\"\n}\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "fb589456-5566-4a45-96a7-75986d0aa1d5",
|
||||
"name": "Update_webhook_url",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
2120,
|
||||
340
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "DELETE",
|
||||
"url": "={{ $('Info Base').item.json[\"chatwoot_url\"] }}/api/v1/accounts/{{ $('Info Base').item.json[\"chatwoot_account_id\"] }}/inboxes/{{ $json.id }}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "api_access_token",
|
||||
"value": "={{ $('Info Base').item.json.chatwoot_token }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "e6094941-410f-496c-9c9c-7b95fd9349af",
|
||||
"name": "Deleta Inbox Start",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
1900,
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "8cf9a78f-9e8a-4288-9d7b-801790af68d5",
|
||||
"name": "No Operation, do nothing",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1660,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldToSplitOut": "payload",
|
||||
"options": {}
|
||||
},
|
||||
"id": "9468896a-5f86-4598-9d20-e8f495cae859",
|
||||
"name": "Ajusta lista",
|
||||
"type": "n8n-nodes-base.itemLists",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
1200,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Info Base",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Lista Inboxes": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Ajusta lista",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Cria Instancia": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Lista Inboxes",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Info Base": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Cria Instancia",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"é Start Inbox?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Deleta Inbox Start",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "é_pre-existente?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Split In Batches": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "é Start Inbox?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"é_pre-existente?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update_webhook_url",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Split In Batches",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update_webhook_url": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split In Batches",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Deleta Inbox Start": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split In Batches",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Ajusta lista": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Split In Batches",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "ab910349-b559-4738-9ac6-de6b06d6bbce",
|
||||
"id": "ByW2ccjR4XPrOyio",
|
||||
"meta": {
|
||||
"instanceId": "4ff16e963c7f5197d7e99e6239192860914312fea0ce2a9a7fd14d74a0a0e906"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
1
Extras/typebot/typebot-example.json
Normal file
256
README.md
@@ -1,247 +1,24 @@
|
||||
<h1 align="center">Evolution Api</h1>
|
||||
<!--
|
||||
</br>
|
||||
<hr style="height: 5px;background: #007500;margin: 20px 0;box-shadow: 0px 3px 5px 0px rgb(204 204 204);">-->
|
||||
|
||||
<!-- <div align="center"> -->
|
||||
<div align="center">
|
||||
|
||||
<!-- [](#)
|
||||
[](#)
|
||||
[](https://evolution-api.com/whatsapp)
|
||||
[](https://evolution-api.com/discord)
|
||||
[](https://evolution-api.com/postman)
|
||||
[](https://doc.evolution-api.com)
|
||||
[](./LICENSE)
|
||||
[](https://app.picpay.com/user/davidsongomes1998) -->
|
||||
[](https://app.picpay.com/user/davidsongomes1998)
|
||||
[](https://bmc.link/evolutionapi)
|
||||
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
|
||||
<!-- <div align="center"><img src="./public/images/cover.png"></div>-> -->
|
||||
<div align="center"><img src="./public/images/cover.png"></div>
|
||||
|
||||
## WhatsApp-Api-NodeJs
|
||||
|
||||
This project is based on the [CodeChat](https://github.com/code-chat-br/whatsapp-api). The original project is an implementation of [Baileys](https://github.com/adiwajshing/Baileys), serving as a Restful API service that controls WhatsApp functions.</br>
|
||||
This project is based on the [CodeChat](https://github.com/code-chat-br/whatsapp-api). The original project is an implementation of [Baileys](https://github.com/WhiskeySockets/Baileys), serving as a Restful API service that controls WhatsApp functions.</br>
|
||||
The code allows the creation of multiservice chats, service bots, or any other system that utilizes WhatsApp. The documentation provides instructions on how to set up and use the project, as well as additional information about its features and configuration options.
|
||||
|
||||
## Infrastructure
|
||||
|
||||
### Nvm installation
|
||||
|
||||
```sh
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
||||
# or
|
||||
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
||||
```
|
||||
>
|
||||
> After finishing, restart the terminal to load the new information.
|
||||
>
|
||||
|
||||
### Docker installation \[optional\]
|
||||
|
||||
```sh
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
|
||||
sudo sh get-docker.sh
|
||||
|
||||
sudo usermod -aG docker ${USER}
|
||||
```
|
||||
### Nodejs installation
|
||||
|
||||
```sh
|
||||
nvm install 16.17.0
|
||||
```
|
||||
|
||||
### pm2 installation
|
||||
```sh
|
||||
npm i -g pm2
|
||||
```
|
||||
|
||||
```sh
|
||||
docker --version
|
||||
|
||||
node --version
|
||||
```
|
||||
## MongoDb [optional]
|
||||
|
||||
After installing docker and docker-compose, up the container.
|
||||
- [compose from mongodb](./mongodb/docker-compose.yaml)
|
||||
|
||||
In the same directory where the file is located, run the following command:
|
||||
```sh
|
||||
bash docker.sh
|
||||
```
|
||||
Using the database is optional.
|
||||
|
||||
## Application startup
|
||||
|
||||
Cloning the Repository
|
||||
```
|
||||
git clone https://github.com/code-chat-br/whatsapp-api.git
|
||||
```
|
||||
|
||||
Go to the project directory and install all dependencies.</br>
|
||||
```sh
|
||||
cd whatsapp-api
|
||||
|
||||
npm i
|
||||
```
|
||||
|
||||
Finally, run the command below to start the application:
|
||||
```sh
|
||||
# Under development
|
||||
npm run start
|
||||
|
||||
# In production
|
||||
npm run start:prod
|
||||
|
||||
# pm2
|
||||
pm2 start 'npm run start:prod' --name ApiCodechat
|
||||
```
|
||||
## Authentication
|
||||
|
||||
You can define two authentication **types** for the routes in the **[env file](./src/dev-env.yml)**.
|
||||
Authentications must be inserted in the request header.
|
||||
|
||||
1. **apikey**
|
||||
|
||||
2. **jwt:** A JWT is a standard for authentication and information exchange defined with a signature.
|
||||
|
||||
> Authentications are generated at instance creation time.
|
||||
|
||||
**Note:** There is also the possibility to define a global api key, which can access and control all instances.
|
||||
|
||||
### Connection
|
||||
|
||||
#### Create an instance
|
||||
|
||||
##### HTTP
|
||||
|
||||
> *NOTE:* This key must be inserted in the request header to create an instance.
|
||||
|
||||
```http
|
||||
POST /instance/create HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Content-Type: application/json
|
||||
apikey: t8OOEeISKzpmc3jjcMqBWYSaJH2PIxns
|
||||
|
||||
```
|
||||
##### cURL
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:8080/instance/create' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'apikey: t8OOEeISKzpmc3jjcMqBWYSaJH2PIxns' \
|
||||
--data-raw '{
|
||||
"instanceName": "codechat"
|
||||
}'
|
||||
```
|
||||
### Response
|
||||
|
||||
```ts
|
||||
{
|
||||
"instance": {
|
||||
"instanceName": "codechat",
|
||||
"status": "created"
|
||||
},
|
||||
"hash": {
|
||||
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]"
|
||||
|
||||
// or
|
||||
// "apikey": "88513847-1B0E-4188-8D76-4A2750C9B6C3"
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Connection with qrcode
|
||||
|
||||
##### HTTP
|
||||
|
||||
```http
|
||||
GET /instance/connect/codechat HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]
|
||||
```
|
||||
```http
|
||||
GET /instance/connect/codechat HTTP/1.1
|
||||
Host: localhost:8080
|
||||
apikey: 88513847-1B0E-4188-8D76-4A2750C9B6C3
|
||||
```
|
||||
##### cURL
|
||||
|
||||
```bash
|
||||
curl --location --request GET 'http://localhost:8080/instance/connect/codechat' \
|
||||
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. [...]'
|
||||
```
|
||||
```bash
|
||||
curl --location --request GET 'http://localhost:8080/instance/connect/codechat' \
|
||||
--header 'apikey: 88513847-1B0E-4188-8D76-4A2750C9B6C3'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```ts
|
||||
{
|
||||
"code": "2@nXSUgRJSBY6T0XJmiFKZ0 [...] ,XsgJhJHYa+0MPpXANdPHHt6Ke/I7O2QyXT/Lsge0uSg=",
|
||||
"base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhE [...] LkMtqAAAAABJRU5ErkJggg=="
|
||||
}
|
||||
```
|
||||
|
||||
### App in Docker
|
||||
- [docker run](./docker.sh)
|
||||
- [docker-compose](./docker-compose.yml)
|
||||
- [env for docker](./Docker/.env)
|
||||
- [DockerHub-codechat/api](https://hub.docker.com/r/codechat/api)
|
||||
|
||||
After building the application, in the same directory as the files above, run the following command:
|
||||
```sh
|
||||
docker-compose up
|
||||
```
|
||||
## Send Messages
|
||||
| | |
|
||||
|-----|---|
|
||||
| Send Text | ✔ |
|
||||
| Send Buttons | ✔ |
|
||||
| Send Template | ❌ |
|
||||
| Send Media: audio - video - image - document - gif <br></br>base64: ```true``` | ✔ |
|
||||
| Send Media File | ❌ |
|
||||
| Send Audio type WhatsApp | ✔ |
|
||||
| Send Location | ✔ |
|
||||
| Send List | ✔ |
|
||||
| Send Link Preview | ✔ |
|
||||
| Send Contact | ✔ |
|
||||
| Send Reaction - emoji | ✔ |
|
||||
| Send Poll Message | ✔ |
|
||||
|
||||
## Postman collections
|
||||
- [Postman Json](./postman.json)
|
||||
|
||||
## Webhook Events
|
||||
|
||||
| Name | Event | TypeData | Description |
|
||||
|------|-------|-----------|------------|
|
||||
| APPLICATION_STARTUP | application.startup | json | Notifies you when a application startup |
|
||||
| QRCODE_UPDATED | qrcode.updated | json | Sends the base64 of the qrcode for reading |
|
||||
| CONNECTION_UPDATE | connection.update | json | Informs the status of the connection with whatsapp |
|
||||
| MESSAGES_SET | message.set | json | Sends a list of all your messages uploaded on whatsapp</br>This event occurs only once |
|
||||
| MESSAGES_UPSERT | message.upsert | json | Notifies you when a message is received |
|
||||
| MESSAGES_UPDATE | message.update | json | Tells you when a message is updated |
|
||||
| SEND_MESSAGE | send.message | json | Notifies when a message is sent |
|
||||
| CONTACTS_SET | contacts.set | json | Performs initial loading of all contacts</br>This event occurs only once |
|
||||
| CONTACTS_UPSERT | contacts.upsert | json | Reloads all contacts with additional information</br>This event occurs only once |
|
||||
| CONTACTS_UPDATE | contacts.update | json | Informs you when the chat is updated |
|
||||
| PRESENCE_UPDATE | presence.update | json | Informs if the user is online, if he is performing some action like writing or recording and his last seen</br>'unavailable' | 'available' | 'composing' | 'recording' | 'paused' |
|
||||
| CHATS_SET | chats.set | json | Send a list of all loaded chats |
|
||||
| CHATS_UPDATE | chats.update | json | Informs you when the chat is updated |
|
||||
| CHATS_UPSERT | chats.upsert | json | Sends any new chat information |
|
||||
| GROUPS_UPSERT | groups.upsert | JSON | Notifies when a group is created |
|
||||
| GROUPS_UPDATE | groups.update | JSON | Notifies when a group has its information updated |
|
||||
| GROUP_PARTICIPANTS_UPDATE | group-participants.update | JSON | Notifies when an action occurs involving a participant</br>'add' | 'remove' | 'promote' | 'demote' |
|
||||
| NEW_TOKEN | new.jwt | JSON | Notifies when the token (jwt) is updated
|
||||
|
||||
## Env File
|
||||
|
||||
See additional settings that can be applied through the **env** file by clicking **[here](./src/dev-env.yml)**.
|
||||
|
||||
> **⚠️Attention⚠️:** rename the **dev-env.yml** file to **env.yml**.
|
||||
|
||||
## SSL
|
||||
|
||||
To install the SSL certificate, follow the **[instructions](https://certbot.eff.org/instructions?ws=other&os=ubuntufocal)** below.
|
||||
## SSL
|
||||
|
||||
To install the SSL certificate, follow the **[instructions](https://certbot.eff.org/instructions?ws=other&os=ubuntufocal)** below.
|
||||
@@ -254,14 +31,21 @@ This code was produced based on the baileys library and it is still under develo
|
||||
|
||||
# Donate to the project.
|
||||
|
||||
|
||||
<div align="center">
|
||||
#### PicPay
|
||||
|
||||
<div align="center">
|
||||
<a href="https://app.picpay.com/user/davidsongomes1998" target="_blank" rel="noopener noreferrer">
|
||||
|
||||
<img src="./public/images/picpay-qr.jpeg" style="width: 50% !important;">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
#### Buy me coffe - PIX
|
||||
|
||||
<div align="center">
|
||||
<a href="https://bmc.link/evolutionapi" target="_blank" rel="noopener noreferrer">
|
||||
<img src="./public/images/qrcode-pix.png" style="width: 50% !important;">
|
||||
</a>
|
||||
<p><b>CHAVE PIX (Telefone):</b> (74)99987-9409</p>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
@@ -1,18 +0,0 @@
|
||||
version: '3.3'
|
||||
|
||||
networks:
|
||||
evolution-net:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
base:
|
||||
container_name: evolution_api
|
||||
image: davidsongomes/evolution-api:latest
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /data/instances:/evolution/instances
|
||||
command: ['node', './dist/src/main.js']
|
||||
|
||||
networks:
|
||||
- evolution-net
|
||||
29
docker-compose.yaml.example
Normal file
@@ -0,0 +1,29 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: evolution/api:local
|
||||
build: .
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
- evolution_store:/evolution/store
|
||||
networks:
|
||||
- evolution-net
|
||||
env_file:
|
||||
- ./Docker/.env
|
||||
command: ['node', './dist/src/main.js']
|
||||
expose:
|
||||
- 8080
|
||||
|
||||
volumes:
|
||||
evolution_instances:
|
||||
evolution_store:
|
||||
|
||||
networks:
|
||||
evolution-net:
|
||||
name: evolution-net
|
||||
driver: bridge
|
||||
80
docker-compose.yaml.example.complete
Normal file
@@ -0,0 +1,80 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: evolution/api:local
|
||||
build: .
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
- evolution_store:/evolution/store
|
||||
networks:
|
||||
- evolution-net
|
||||
env_file:
|
||||
- ./Docker/.env
|
||||
command: ['node', './dist/src/main.js']
|
||||
expose:
|
||||
- 8080
|
||||
|
||||
mongodb:
|
||||
container_name: mongodb
|
||||
image: mongo
|
||||
restart: always
|
||||
ports:
|
||||
- 27017:27017
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=root
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- evolution_mongodb_data:/data/db
|
||||
- evolution_mongodb_configdb:/data/configdb
|
||||
networks:
|
||||
- evolution-net
|
||||
expose:
|
||||
- 27017
|
||||
|
||||
mongo-express:
|
||||
image: mongo-express
|
||||
networks:
|
||||
- evolution-net
|
||||
environment:
|
||||
ME_CONFIG_BASICAUTH_USERNAME: root
|
||||
ME_CONFIG_BASICAUTH_PASSWORD: root
|
||||
ME_CONFIG_MONGODB_SERVER: mongodb
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME: root
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: root
|
||||
ports:
|
||||
- 8081:8081
|
||||
links:
|
||||
- mongodb
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
command: >
|
||||
redis-server
|
||||
--port 6379
|
||||
--appendonly yes
|
||||
volumes:
|
||||
- evolution_redis:/data
|
||||
networks:
|
||||
- evolution-net
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
volumes:
|
||||
evolution_instances:
|
||||
evolution_store:
|
||||
evolution_mongodb_data:
|
||||
evolution_mongodb_configdb:
|
||||
evolution_redis:
|
||||
|
||||
networks:
|
||||
evolution-net:
|
||||
name: evolution-net
|
||||
driver: bridge
|
||||
28
docker-compose.yaml.example.dockerhub
Normal file
@@ -0,0 +1,28 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: atendai/evolution-api:latest
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
- evolution_store:/evolution/store
|
||||
networks:
|
||||
- evolution-net
|
||||
env_file:
|
||||
- ./Docker/.env
|
||||
command: ['node', './dist/src/main.js']
|
||||
expose:
|
||||
- 8080
|
||||
|
||||
volumes:
|
||||
evolution_instances:
|
||||
evolution_store:
|
||||
|
||||
networks:
|
||||
evolution-net:
|
||||
name: evolution-net
|
||||
driver: bridge
|
||||
@@ -1,28 +0,0 @@
|
||||
|
||||
version: '3.8'
|
||||
|
||||
networks:
|
||||
api-net:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
container_name: mongodb
|
||||
|
||||
# This image already has a single replica set
|
||||
image: mongo
|
||||
|
||||
restart: always
|
||||
volumes:
|
||||
# sudo mkdir -p /data/mongodb
|
||||
- /data/mongodb:/data/db
|
||||
ports:
|
||||
- 26712:27017
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: root
|
||||
# Set a password to access the bank
|
||||
MONGO_INITDB_ROOT_PASSWORD: <password>
|
||||
networks:
|
||||
- api-net
|
||||
expose:
|
||||
- 26712
|
||||
56
package.json
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "1.2.0",
|
||||
"version": "1.7.3",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "index.js",
|
||||
"main": "./dist/src/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "ts-node --files --transpile-only ./src/main.ts",
|
||||
"start:prod": "bash start.sh",
|
||||
"dev:server": "clear && tsnd --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
|
||||
"test": "clear && tsnd --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts"
|
||||
"test": "clear && tsnd --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/DavidsonGomes/evolution-api.git"
|
||||
"url": "git+https://github.com/EvolutionAPI/evolution-api.git"
|
||||
},
|
||||
"keywords": [
|
||||
"chat",
|
||||
@@ -36,37 +37,58 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/DavidsonGomes/evolution-api/issues"
|
||||
"url": "https://github.com/EvolutionAPI/evolution-api/issues"
|
||||
},
|
||||
"homepage": "https://github.com/DavidsonGomes/evolution-api#readme",
|
||||
"homepage": "https://github.com/EvolutionAPI/evolution-api#readme",
|
||||
"dependencies": {
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@evolution/base": "github:WhiskeySockets/Baileys",
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
"@figuro/chatwoot-sdk": "^1.1.16",
|
||||
"@hapi/boom": "^10.0.1",
|
||||
"axios": "^1.3.5",
|
||||
"class-validator": "^0.13.2",
|
||||
"@sentry/node": "^7.59.2",
|
||||
"@whiskeysockets/baileys": "github:AtendAI/Baileys",
|
||||
"amqplib": "^0.10.3",
|
||||
"aws-sdk": "^2.1499.0",
|
||||
"axios": "^1.6.5",
|
||||
"class-validator": "^0.14.1",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.7",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"evolution-manager": "^0.4.13",
|
||||
"exiftool-vendored": "^22.0.0",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"form-data": "^4.0.0",
|
||||
"hbs": "^4.2.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"i18next": "^23.7.19",
|
||||
"jimp": "^0.16.13",
|
||||
"join": "^3.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonschema": "^1.4.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"libphonenumber-js": "^1.10.39",
|
||||
"link-preview-js": "^3.0.4",
|
||||
"mongoose": "^6.10.5",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-mime-types": "^1.1.0",
|
||||
"node-windows": "^1.0.0-beta.8",
|
||||
"parse-bmfont-xml": "^1.1.4",
|
||||
"pg": "^8.11.3",
|
||||
"pino": "^8.11.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"redis": "^4.6.5",
|
||||
"uuid": "^9.0.0"
|
||||
"sharp": "^0.32.2",
|
||||
"socket.io": "^4.7.1",
|
||||
"socks-proxy-agent": "^8.0.1",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.2",
|
||||
@@ -74,16 +96,20 @@
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node-windows": "^0.1.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/qrcode-terminal": "^0.12.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"eslint": "^8.38.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
|
||||
2045
postman.json
BIN
public/images/bmc_qr.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 24 KiB |
BIN
public/images/evolution-logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 36 KiB |
BIN
public/images/picpay-qr.jpeg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
public/images/qrcode-pix.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
19
src/api/abstract/abstract.cache.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface ICache {
|
||||
get(key: string): Promise<any>;
|
||||
|
||||
hGet(key: string, field: string): Promise<any>;
|
||||
|
||||
set(key: string, value: any, ttl?: number): void;
|
||||
|
||||
hSet(key: string, field: string, value: any): Promise<void>;
|
||||
|
||||
has(key: string): Promise<boolean>;
|
||||
|
||||
keys(appendCriteria?: string): Promise<string[]>;
|
||||
|
||||
delete(key: string | string[]): Promise<number>;
|
||||
|
||||
hDelete(key: string, field: string): Promise<any>;
|
||||
|
||||
deleteAll(appendCriteria?: string): Promise<number>;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { ConfigService, Database } from '../../config/env.config';
|
||||
import { ROOT_DIR } from '../../config/path.config';
|
||||
|
||||
export type IInsert = { insertCount: number };
|
||||
|
||||
export interface IRepository {
|
||||
insert(data: any, saveDb?: boolean): Promise<IInsert>;
|
||||
insert(data: any, instanceName: string, saveDb?: boolean): Promise<IInsert>;
|
||||
update(data: any, instanceName: string, saveDb?: boolean): Promise<IInsert>;
|
||||
find(query: any): Promise<any>;
|
||||
delete(query: any, force?: boolean): Promise<any>;
|
||||
|
||||
@@ -33,11 +35,9 @@ export abstract class Repository implements IRepository {
|
||||
mkdirSync(create.path, { recursive: true });
|
||||
}
|
||||
try {
|
||||
writeFileSync(
|
||||
join(create.path, create.fileName + '.json'),
|
||||
JSON.stringify({ ...create.data }),
|
||||
{ encoding: 'utf-8' },
|
||||
);
|
||||
writeFileSync(join(create.path, create.fileName + '.json'), JSON.stringify({ ...create.data }), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
return { message: 'create - success' };
|
||||
} finally {
|
||||
@@ -45,14 +45,23 @@ export abstract class Repository implements IRepository {
|
||||
}
|
||||
};
|
||||
|
||||
public insert(data: any, saveDb = false): Promise<IInsert> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public find(query: any): Promise<any> {
|
||||
// eslint-disable-next-line
|
||||
public insert(data: any, instanceName: string, saveDb = false): Promise<IInsert> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
delete(query: any, force?: boolean): Promise<any> {
|
||||
// eslint-disable-next-line
|
||||
public update(data: any, instanceName: string, saveDb = false): Promise<IInsert> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
public find(query: any): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
delete(query: any, force?: boolean): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { Request } from 'express';
|
||||
import { validate } from 'jsonschema';
|
||||
import { BadRequestException } from '../../exceptions';
|
||||
import 'express-async-errors';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { validate } from 'jsonschema';
|
||||
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import { GroupInvite, GroupJid } from '../dto/group.dto';
|
||||
import { BadRequestException } from '../../exceptions';
|
||||
import { GetParticipant, GroupInvite } from '../dto/group.dto';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
|
||||
type DataValidate<T> = {
|
||||
request: Request;
|
||||
@@ -19,7 +21,6 @@ const logger = new Logger('Validate');
|
||||
export abstract class RouterBroker {
|
||||
constructor() {}
|
||||
public routerPath(path: string, param = true) {
|
||||
// const route = param ? '/:instanceName/' + path : '/' + path;
|
||||
let route = '/' + path;
|
||||
param ? (route += '/:instanceName') : null;
|
||||
|
||||
@@ -45,6 +46,34 @@ export abstract class RouterBroker {
|
||||
|
||||
const v = schema ? validate(ref, schema) : { valid: true, errors: [] };
|
||||
|
||||
if (!v.valid) {
|
||||
const message: any[] = v.errors.map(({ stack, schema }) => {
|
||||
let message: string;
|
||||
if (schema['description']) {
|
||||
message = schema['description'];
|
||||
} else {
|
||||
message = stack.replace('instance.', '');
|
||||
}
|
||||
return message;
|
||||
});
|
||||
logger.error(message);
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
|
||||
return await execute(instance, ref);
|
||||
}
|
||||
|
||||
public async groupNoValidate<T>(args: DataValidate<T>) {
|
||||
const { request, ClassRef, schema, execute } = args;
|
||||
|
||||
const instance = request.params as unknown as InstanceDto;
|
||||
|
||||
const ref = new ClassRef();
|
||||
|
||||
Object.assign(ref, request.body);
|
||||
|
||||
const v = validate(ref, schema);
|
||||
|
||||
if (!v.valid) {
|
||||
const message: any[] = v.errors.map(({ property, stack, schema }) => {
|
||||
let message: string;
|
||||
@@ -68,21 +97,29 @@ export abstract class RouterBroker {
|
||||
public async groupValidate<T>(args: DataValidate<T>) {
|
||||
const { request, ClassRef, schema, execute } = args;
|
||||
|
||||
const groupJid = request.query as unknown as GroupJid;
|
||||
|
||||
if (!groupJid?.groupJid) {
|
||||
throw new BadRequestException(
|
||||
'The group id needs to be informed in the query',
|
||||
'ex: "groupJid=120362@g.us"',
|
||||
);
|
||||
}
|
||||
|
||||
const instance = request.params as unknown as InstanceDto;
|
||||
const body = request.body;
|
||||
|
||||
let groupJid = body?.groupJid;
|
||||
|
||||
if (!groupJid) {
|
||||
if (request.query?.groupJid) {
|
||||
groupJid = request.query.groupJid;
|
||||
} else {
|
||||
throw new BadRequestException('The group id needs to be informed in the query', 'ex: "groupJid=120362@g.us"');
|
||||
}
|
||||
}
|
||||
|
||||
if (!groupJid.endsWith('@g.us')) {
|
||||
groupJid = groupJid + '@g.us';
|
||||
}
|
||||
|
||||
Object.assign(body, {
|
||||
groupJid: groupJid,
|
||||
});
|
||||
|
||||
const ref = new ClassRef();
|
||||
|
||||
Object.assign(body, groupJid);
|
||||
Object.assign(ref, body);
|
||||
|
||||
const v = validate(ref, schema);
|
||||
@@ -129,7 +166,44 @@ export abstract class RouterBroker {
|
||||
|
||||
const v = validate(ref, schema);
|
||||
|
||||
console.log(v, '@checkei aqui');
|
||||
if (!v.valid) {
|
||||
const message: any[] = v.errors.map(({ property, stack, schema }) => {
|
||||
let message: string;
|
||||
if (schema['description']) {
|
||||
message = schema['description'];
|
||||
} else {
|
||||
message = stack.replace('instance.', '');
|
||||
}
|
||||
return {
|
||||
property: property.replace('instance.', ''),
|
||||
message,
|
||||
};
|
||||
});
|
||||
logger.error([...message]);
|
||||
throw new BadRequestException(...message);
|
||||
}
|
||||
|
||||
return await execute(instance, ref);
|
||||
}
|
||||
|
||||
public async getParticipantsValidate<T>(args: DataValidate<T>) {
|
||||
const { request, ClassRef, schema, execute } = args;
|
||||
|
||||
const getParticipants = request.query as unknown as GetParticipant;
|
||||
|
||||
if (!getParticipants?.getParticipants) {
|
||||
throw new BadRequestException('The getParticipants needs to be informed in the query');
|
||||
}
|
||||
|
||||
const instance = request.params as unknown as InstanceDto;
|
||||
const body = request.body;
|
||||
|
||||
const ref = new ClassRef();
|
||||
|
||||
Object.assign(body, getParticipants);
|
||||
Object.assign(ref, body);
|
||||
|
||||
const v = validate(ref, schema);
|
||||
|
||||
if (!v.valid) {
|
||||
const message: any[] = v.errors.map(({ property, stack, schema }) => {
|
||||
132
src/api/controllers/chat.controller.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import {
|
||||
ArchiveChatDto,
|
||||
BlockUserDto,
|
||||
DeleteMessage,
|
||||
getBase64FromMediaMessageDto,
|
||||
NumberDto,
|
||||
PrivacySettingDto,
|
||||
ProfileNameDto,
|
||||
ProfilePictureDto,
|
||||
ProfileStatusDto,
|
||||
ReadMessageDto,
|
||||
SendPresenceDto,
|
||||
UpdateMessageDto,
|
||||
WhatsAppNumberDto,
|
||||
} from '../dto/chat.dto';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { ContactQuery } from '../repository/contact.repository';
|
||||
import { MessageQuery } from '../repository/message.repository';
|
||||
import { MessageUpQuery } from '../repository/messageUp.repository';
|
||||
import { WAMonitoringService } from '../services/monitor.service';
|
||||
|
||||
const logger = new Logger('ChatController');
|
||||
|
||||
export class ChatController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) {
|
||||
logger.verbose('requested whatsappNumber from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].whatsappNumber(data);
|
||||
}
|
||||
|
||||
public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) {
|
||||
logger.verbose('requested readMessage from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data);
|
||||
}
|
||||
|
||||
public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) {
|
||||
logger.verbose('requested archiveChat from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].archiveChat(data);
|
||||
}
|
||||
|
||||
public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) {
|
||||
logger.verbose('requested deleteMessage from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].deleteMessage(data);
|
||||
}
|
||||
|
||||
public async fetchProfilePicture({ instanceName }: InstanceDto, data: NumberDto) {
|
||||
logger.verbose('requested fetchProfilePicture from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].profilePicture(data.number);
|
||||
}
|
||||
|
||||
public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) {
|
||||
logger.verbose('requested fetchProfile from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].fetchProfile(instanceName, data.number);
|
||||
}
|
||||
|
||||
public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) {
|
||||
logger.verbose('requested fetchContacts from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].fetchContacts(query);
|
||||
}
|
||||
|
||||
public async getBase64FromMediaMessage({ instanceName }: InstanceDto, data: getBase64FromMediaMessageDto) {
|
||||
logger.verbose('requested getBase64FromMediaMessage from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(data);
|
||||
}
|
||||
|
||||
public async fetchMessages({ instanceName }: InstanceDto, query: MessageQuery) {
|
||||
logger.verbose('requested fetchMessages from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].fetchMessages(query);
|
||||
}
|
||||
|
||||
public async fetchStatusMessage({ instanceName }: InstanceDto, query: MessageUpQuery) {
|
||||
logger.verbose('requested fetchStatusMessage from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].fetchStatusMessage(query);
|
||||
}
|
||||
|
||||
public async fetchChats({ instanceName }: InstanceDto) {
|
||||
logger.verbose('requested fetchChats from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].fetchChats();
|
||||
}
|
||||
|
||||
public async sendPresence({ instanceName }: InstanceDto, data: SendPresenceDto) {
|
||||
logger.verbose('requested sendPresence from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].sendPresence(data);
|
||||
}
|
||||
|
||||
public async fetchPrivacySettings({ instanceName }: InstanceDto) {
|
||||
logger.verbose('requested fetchPrivacySettings from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings();
|
||||
}
|
||||
|
||||
public async updatePrivacySettings({ instanceName }: InstanceDto, data: PrivacySettingDto) {
|
||||
logger.verbose('requested updatePrivacySettings from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data);
|
||||
}
|
||||
|
||||
public async fetchBusinessProfile({ instanceName }: InstanceDto, data: ProfilePictureDto) {
|
||||
logger.verbose('requested fetchBusinessProfile from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile(data.number);
|
||||
}
|
||||
|
||||
public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) {
|
||||
logger.verbose('requested updateProfileName from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name);
|
||||
}
|
||||
|
||||
public async updateProfileStatus({ instanceName }: InstanceDto, data: ProfileStatusDto) {
|
||||
logger.verbose('requested updateProfileStatus from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].updateProfileStatus(data.status);
|
||||
}
|
||||
|
||||
public async updateProfilePicture({ instanceName }: InstanceDto, data: ProfilePictureDto) {
|
||||
logger.verbose('requested updateProfilePicture from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].updateProfilePicture(data.picture);
|
||||
}
|
||||
|
||||
public async removeProfilePicture({ instanceName }: InstanceDto) {
|
||||
logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].removeProfilePicture();
|
||||
}
|
||||
|
||||
public async updateMessage({ instanceName }: InstanceDto, data: UpdateMessageDto) {
|
||||
logger.verbose('requested updateMessage from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].updateMessage(data);
|
||||
}
|
||||
|
||||
public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) {
|
||||
logger.verbose('requested blockUser from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].blockUser(data);
|
||||
}
|
||||
}
|
||||
103
src/api/controllers/group.controller.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import {
|
||||
AcceptGroupInvite,
|
||||
CreateGroupDto,
|
||||
GetParticipant,
|
||||
GroupDescriptionDto,
|
||||
GroupInvite,
|
||||
GroupJid,
|
||||
GroupPictureDto,
|
||||
GroupSendInvite,
|
||||
GroupSubjectDto,
|
||||
GroupToggleEphemeralDto,
|
||||
GroupUpdateParticipantDto,
|
||||
GroupUpdateSettingDto,
|
||||
} from '../dto/group.dto';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { WAMonitoringService } from '../services/monitor.service';
|
||||
|
||||
const logger = new Logger('ChatController');
|
||||
|
||||
export class GroupController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async createGroup(instance: InstanceDto, create: CreateGroupDto) {
|
||||
logger.verbose('requested createGroup from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].createGroup(create);
|
||||
}
|
||||
|
||||
public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) {
|
||||
logger.verbose('requested updateGroupPicture from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture(update);
|
||||
}
|
||||
|
||||
public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) {
|
||||
logger.verbose('requested updateGroupSubject from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject(update);
|
||||
}
|
||||
|
||||
public async updateGroupDescription(instance: InstanceDto, update: GroupDescriptionDto) {
|
||||
logger.verbose('requested updateGroupDescription from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription(update);
|
||||
}
|
||||
|
||||
public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) {
|
||||
logger.verbose('requested findGroupInfo from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid);
|
||||
}
|
||||
|
||||
public async fetchAllGroups(instance: InstanceDto, getPaticipants: GetParticipant) {
|
||||
logger.verbose('requested fetchAllGroups from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups(getPaticipants);
|
||||
}
|
||||
|
||||
public async inviteCode(instance: InstanceDto, groupJid: GroupJid) {
|
||||
logger.verbose('requested inviteCode from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid);
|
||||
}
|
||||
|
||||
public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) {
|
||||
logger.verbose('requested inviteInfo from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode);
|
||||
}
|
||||
|
||||
public async sendInvite(instance: InstanceDto, data: GroupSendInvite) {
|
||||
logger.verbose('requested sendInvite from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data);
|
||||
}
|
||||
|
||||
public async acceptInviteCode(instance: InstanceDto, inviteCode: AcceptGroupInvite) {
|
||||
logger.verbose('requested acceptInviteCode from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].acceptInviteCode(inviteCode);
|
||||
}
|
||||
|
||||
public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) {
|
||||
logger.verbose('requested revokeInviteCode from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid);
|
||||
}
|
||||
|
||||
public async findParticipants(instance: InstanceDto, groupJid: GroupJid) {
|
||||
logger.verbose('requested findParticipants from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].findParticipants(groupJid);
|
||||
}
|
||||
|
||||
public async updateGParticipate(instance: InstanceDto, update: GroupUpdateParticipantDto) {
|
||||
logger.verbose('requested updateGParticipate from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant(update);
|
||||
}
|
||||
|
||||
public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) {
|
||||
logger.verbose('requested updateGSetting from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update);
|
||||
}
|
||||
|
||||
public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) {
|
||||
logger.verbose('requested toggleEphemeral from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(update);
|
||||
}
|
||||
|
||||
public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) {
|
||||
logger.verbose('requested leaveGroup from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid);
|
||||
}
|
||||
}
|
||||
753
src/api/controllers/instance.controller.ts
Normal file
@@ -0,0 +1,753 @@
|
||||
import { delay } from '@whiskeysockets/baileys';
|
||||
import { isURL } from 'class-validator';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ConfigService, HttpServer, WaBusiness } from '../../config/env.config';
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import { BadRequestException, InternalServerErrorException } from '../../exceptions';
|
||||
import { InstanceDto, SetPresenceDto } from '../dto/instance.dto';
|
||||
import { ChatwootService } from '../integrations/chatwoot/services/chatwoot.service';
|
||||
import { RabbitmqService } from '../integrations/rabbitmq/services/rabbitmq.service';
|
||||
import { SqsService } from '../integrations/sqs/services/sqs.service';
|
||||
import { TypebotService } from '../integrations/typebot/services/typebot.service';
|
||||
import { WebsocketService } from '../integrations/websocket/services/websocket.service';
|
||||
import { RepositoryBroker } from '../repository/repository.manager';
|
||||
import { AuthService, OldToken } from '../services/auth.service';
|
||||
import { CacheService } from '../services/cache.service';
|
||||
import { IntegrationService } from '../services/integration.service';
|
||||
import { WAMonitoringService } from '../services/monitor.service';
|
||||
import { SettingsService } from '../services/settings.service';
|
||||
import { WebhookService } from '../services/webhook.service';
|
||||
import { BaileysStartupService } from '../services/whatsapp/whatsapp.baileys.service';
|
||||
import { BusinessStartupService } from '../services/whatsapp/whatsapp.business.service';
|
||||
import { Events, Integration, wa } from '../types/wa.types';
|
||||
import { ProxyController } from './proxy.controller';
|
||||
|
||||
export class InstanceController {
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly repository: RepositoryBroker,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly authService: AuthService,
|
||||
private readonly webhookService: WebhookService,
|
||||
private readonly chatwootService: ChatwootService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly websocketService: WebsocketService,
|
||||
private readonly rabbitmqService: RabbitmqService,
|
||||
private readonly sqsService: SqsService,
|
||||
private readonly typebotService: TypebotService,
|
||||
private readonly integrationService: IntegrationService,
|
||||
private readonly proxyService: ProxyController,
|
||||
private readonly cache: CacheService,
|
||||
private readonly chatwootCache: CacheService,
|
||||
private readonly messagesLostCache: CacheService,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger(InstanceController.name);
|
||||
|
||||
public async createInstance({
|
||||
instanceName,
|
||||
webhook,
|
||||
webhook_by_events,
|
||||
webhook_base64,
|
||||
events,
|
||||
qrcode,
|
||||
number,
|
||||
mobile,
|
||||
integration,
|
||||
token,
|
||||
chatwoot_account_id,
|
||||
chatwoot_token,
|
||||
chatwoot_url,
|
||||
chatwoot_sign_msg,
|
||||
chatwoot_reopen_conversation,
|
||||
chatwoot_conversation_pending,
|
||||
chatwoot_import_contacts,
|
||||
chatwoot_import_messages,
|
||||
chatwoot_days_limit_import_messages,
|
||||
reject_call,
|
||||
msg_call,
|
||||
groups_ignore,
|
||||
always_online,
|
||||
read_messages,
|
||||
read_status,
|
||||
sync_full_history,
|
||||
websocket_enabled,
|
||||
websocket_events,
|
||||
rabbitmq_enabled,
|
||||
rabbitmq_events,
|
||||
sqs_enabled,
|
||||
sqs_events,
|
||||
typebot_url,
|
||||
typebot,
|
||||
typebot_expire,
|
||||
typebot_keyword_finish,
|
||||
typebot_delay_message,
|
||||
typebot_unknown_message,
|
||||
typebot_listening_from_me,
|
||||
proxy,
|
||||
}: InstanceDto) {
|
||||
try {
|
||||
this.logger.verbose('requested createInstance from ' + instanceName + ' instance');
|
||||
|
||||
this.logger.verbose('checking duplicate token');
|
||||
await this.authService.checkDuplicateToken(token);
|
||||
|
||||
if (!token && integration === Integration.WHATSAPP_BUSINESS) {
|
||||
throw new BadRequestException('token is required');
|
||||
}
|
||||
|
||||
this.logger.verbose('creating instance');
|
||||
let instance: BaileysStartupService | BusinessStartupService;
|
||||
if (integration === Integration.WHATSAPP_BUSINESS) {
|
||||
instance = new BusinessStartupService(
|
||||
this.configService,
|
||||
this.eventEmitter,
|
||||
this.repository,
|
||||
this.cache,
|
||||
this.chatwootCache,
|
||||
this.messagesLostCache,
|
||||
);
|
||||
} else {
|
||||
instance = new BaileysStartupService(
|
||||
this.configService,
|
||||
this.eventEmitter,
|
||||
this.repository,
|
||||
this.cache,
|
||||
this.chatwootCache,
|
||||
this.messagesLostCache,
|
||||
);
|
||||
}
|
||||
|
||||
await this.waMonitor.saveInstance({ integration, instanceName, token, number, mobile });
|
||||
|
||||
instance.instanceName = instanceName;
|
||||
|
||||
const instanceId = v4();
|
||||
|
||||
instance.sendDataWebhook(Events.INSTANCE_CREATE, {
|
||||
instanceName,
|
||||
instanceId: instanceId,
|
||||
});
|
||||
|
||||
this.logger.verbose('instance: ' + instance.instanceName + ' created');
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceName] = instance;
|
||||
this.waMonitor.delInstanceTime(instance.instanceName);
|
||||
|
||||
this.logger.verbose('generating hash');
|
||||
const hash = await this.authService.generateHash(
|
||||
{
|
||||
instanceName: instance.instanceName,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
token,
|
||||
);
|
||||
|
||||
this.logger.verbose('hash: ' + hash + ' generated');
|
||||
|
||||
let webhookEvents: string[];
|
||||
|
||||
if (webhook) {
|
||||
if (!isURL(webhook, { require_tld: false })) {
|
||||
throw new BadRequestException('Invalid "url" property in webhook');
|
||||
}
|
||||
|
||||
this.logger.verbose('creating webhook');
|
||||
try {
|
||||
let newEvents: string[] = [];
|
||||
if (events.length === 0) {
|
||||
newEvents = [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
];
|
||||
} else {
|
||||
newEvents = events;
|
||||
}
|
||||
this.webhookService.create(instance, {
|
||||
enabled: true,
|
||||
url: webhook,
|
||||
events: newEvents,
|
||||
webhook_by_events,
|
||||
webhook_base64,
|
||||
});
|
||||
|
||||
webhookEvents = (await this.webhookService.find(instance)).events;
|
||||
} catch (error) {
|
||||
this.logger.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
let websocketEvents: string[];
|
||||
|
||||
if (websocket_enabled) {
|
||||
this.logger.verbose('creating websocket');
|
||||
try {
|
||||
let newEvents: string[] = [];
|
||||
if (websocket_events.length === 0) {
|
||||
newEvents = [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
];
|
||||
} else {
|
||||
newEvents = websocket_events;
|
||||
}
|
||||
this.websocketService.create(instance, {
|
||||
enabled: true,
|
||||
events: newEvents,
|
||||
});
|
||||
|
||||
websocketEvents = (await this.websocketService.find(instance)).events;
|
||||
} catch (error) {
|
||||
this.logger.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
let rabbitmqEvents: string[];
|
||||
|
||||
if (rabbitmq_enabled) {
|
||||
this.logger.verbose('creating rabbitmq');
|
||||
try {
|
||||
let newEvents: string[] = [];
|
||||
if (rabbitmq_events.length === 0) {
|
||||
newEvents = [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
];
|
||||
} else {
|
||||
newEvents = rabbitmq_events;
|
||||
}
|
||||
this.rabbitmqService.create(instance, {
|
||||
enabled: true,
|
||||
events: newEvents,
|
||||
});
|
||||
|
||||
rabbitmqEvents = (await this.rabbitmqService.find(instance)).events;
|
||||
} catch (error) {
|
||||
this.logger.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
let sqsEvents: string[];
|
||||
|
||||
if (sqs_enabled) {
|
||||
this.logger.verbose('creating sqs');
|
||||
try {
|
||||
let newEvents: string[] = [];
|
||||
if (sqs_events.length === 0) {
|
||||
newEvents = [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
];
|
||||
} else {
|
||||
newEvents = sqs_events;
|
||||
}
|
||||
this.sqsService.create(instance, {
|
||||
enabled: true,
|
||||
events: newEvents,
|
||||
});
|
||||
|
||||
sqsEvents = (await this.sqsService.find(instance)).events;
|
||||
} catch (error) {
|
||||
this.logger.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (proxy) {
|
||||
const testProxy = await this.proxyService.testProxy(proxy);
|
||||
if (!testProxy) {
|
||||
throw new BadRequestException('Invalid proxy');
|
||||
}
|
||||
|
||||
await this.proxyService.createProxy(instance, {
|
||||
enabled: true,
|
||||
proxy,
|
||||
});
|
||||
}
|
||||
|
||||
if (typebot_url) {
|
||||
try {
|
||||
if (!isURL(typebot_url, { require_tld: false })) {
|
||||
throw new BadRequestException('Invalid "url" property in typebot_url');
|
||||
}
|
||||
|
||||
this.logger.verbose('creating typebot');
|
||||
|
||||
this.typebotService.create(instance, {
|
||||
enabled: true,
|
||||
url: typebot_url,
|
||||
typebot: typebot,
|
||||
expire: typebot_expire,
|
||||
keyword_finish: typebot_keyword_finish,
|
||||
delay_message: typebot_delay_message,
|
||||
unknown_message: typebot_unknown_message,
|
||||
listening_from_me: typebot_listening_from_me,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.verbose('creating settings');
|
||||
const settings: wa.LocalSettings = {
|
||||
reject_call: reject_call || false,
|
||||
msg_call: msg_call || '',
|
||||
groups_ignore: groups_ignore || true,
|
||||
always_online: always_online || false,
|
||||
read_messages: read_messages || false,
|
||||
read_status: read_status || false,
|
||||
sync_full_history: sync_full_history ?? false,
|
||||
};
|
||||
|
||||
this.logger.verbose('settings: ' + JSON.stringify(settings));
|
||||
|
||||
this.settingsService.create(instance, settings);
|
||||
|
||||
let webhook_wa_business = null,
|
||||
access_token_wa_business = '';
|
||||
|
||||
if (integration === Integration.WHATSAPP_BUSINESS) {
|
||||
if (!number) {
|
||||
throw new BadRequestException('number is required');
|
||||
}
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
webhook_wa_business = `${urlServer}/webhook/whatsapp/${encodeURIComponent(instance.instanceName)}`;
|
||||
access_token_wa_business = this.configService.get<WaBusiness>('WA_BUSINESS').TOKEN_WEBHOOK;
|
||||
}
|
||||
|
||||
this.integrationService.create(instance, {
|
||||
integration,
|
||||
number,
|
||||
token,
|
||||
});
|
||||
if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) {
|
||||
let getQrcode: wa.QrCode;
|
||||
|
||||
if (qrcode) {
|
||||
this.logger.verbose('creating qrcode');
|
||||
await instance.connectToWhatsapp(number, mobile);
|
||||
await delay(5000);
|
||||
getQrcode = instance.qrCode;
|
||||
}
|
||||
|
||||
const result = {
|
||||
instance: {
|
||||
instanceName: instance.instanceName,
|
||||
instanceId: instanceId,
|
||||
integration: integration,
|
||||
webhook_wa_business,
|
||||
access_token_wa_business,
|
||||
status: 'created',
|
||||
},
|
||||
hash,
|
||||
webhook: {
|
||||
webhook,
|
||||
webhook_by_events,
|
||||
webhook_base64,
|
||||
events: webhookEvents,
|
||||
},
|
||||
websocket: {
|
||||
enabled: websocket_enabled,
|
||||
events: websocketEvents,
|
||||
},
|
||||
rabbitmq: {
|
||||
enabled: rabbitmq_enabled,
|
||||
events: rabbitmqEvents,
|
||||
},
|
||||
sqs: {
|
||||
enabled: sqs_enabled,
|
||||
events: sqsEvents,
|
||||
},
|
||||
typebot: {
|
||||
enabled: typebot_url ? true : false,
|
||||
url: typebot_url,
|
||||
typebot,
|
||||
expire: typebot_expire,
|
||||
keyword_finish: typebot_keyword_finish,
|
||||
delay_message: typebot_delay_message,
|
||||
unknown_message: typebot_unknown_message,
|
||||
listening_from_me: typebot_listening_from_me,
|
||||
},
|
||||
settings,
|
||||
qrcode: getQrcode,
|
||||
};
|
||||
|
||||
this.logger.verbose('instance created');
|
||||
this.logger.verbose(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!chatwoot_account_id) {
|
||||
throw new BadRequestException('account_id is required');
|
||||
}
|
||||
|
||||
if (!chatwoot_token) {
|
||||
throw new BadRequestException('token is required');
|
||||
}
|
||||
|
||||
if (!chatwoot_url) {
|
||||
throw new BadRequestException('url is required');
|
||||
}
|
||||
|
||||
if (!isURL(chatwoot_url, { require_tld: false })) {
|
||||
throw new BadRequestException('Invalid "url" property in chatwoot');
|
||||
}
|
||||
|
||||
if (chatwoot_sign_msg !== true && chatwoot_sign_msg !== false) {
|
||||
throw new BadRequestException('sign_msg is required');
|
||||
}
|
||||
|
||||
if (chatwoot_reopen_conversation !== true && chatwoot_reopen_conversation !== false) {
|
||||
throw new BadRequestException('reopen_conversation is required');
|
||||
}
|
||||
|
||||
if (chatwoot_conversation_pending !== true && chatwoot_conversation_pending !== false) {
|
||||
throw new BadRequestException('conversation_pending is required');
|
||||
}
|
||||
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
try {
|
||||
this.chatwootService.create(instance, {
|
||||
enabled: true,
|
||||
account_id: chatwoot_account_id,
|
||||
token: chatwoot_token,
|
||||
url: chatwoot_url,
|
||||
sign_msg: chatwoot_sign_msg || false,
|
||||
name_inbox: instance.instanceName.split('-cwId-')[0],
|
||||
number,
|
||||
reopen_conversation: chatwoot_reopen_conversation || false,
|
||||
conversation_pending: chatwoot_conversation_pending || false,
|
||||
import_contacts: chatwoot_import_contacts ?? true,
|
||||
import_messages: chatwoot_import_messages ?? true,
|
||||
days_limit_import_messages: chatwoot_days_limit_import_messages ?? 60,
|
||||
auto_create: true,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.log(error);
|
||||
}
|
||||
|
||||
return {
|
||||
instance: {
|
||||
instanceName: instance.instanceName,
|
||||
instanceId: instanceId,
|
||||
integration: integration,
|
||||
webhook_wa_business,
|
||||
access_token_wa_business,
|
||||
status: 'created',
|
||||
},
|
||||
hash,
|
||||
webhook: {
|
||||
webhook,
|
||||
webhook_by_events,
|
||||
webhook_base64,
|
||||
events: webhookEvents,
|
||||
},
|
||||
websocket: {
|
||||
enabled: websocket_enabled,
|
||||
events: websocketEvents,
|
||||
},
|
||||
rabbitmq: {
|
||||
enabled: rabbitmq_enabled,
|
||||
events: rabbitmqEvents,
|
||||
},
|
||||
sqs: {
|
||||
enabled: sqs_enabled,
|
||||
events: sqsEvents,
|
||||
},
|
||||
typebot: {
|
||||
enabled: typebot_url ? true : false,
|
||||
url: typebot_url,
|
||||
typebot,
|
||||
expire: typebot_expire,
|
||||
keyword_finish: typebot_keyword_finish,
|
||||
delay_message: typebot_delay_message,
|
||||
unknown_message: typebot_unknown_message,
|
||||
listening_from_me: typebot_listening_from_me,
|
||||
},
|
||||
settings,
|
||||
chatwoot: {
|
||||
enabled: true,
|
||||
account_id: chatwoot_account_id,
|
||||
token: chatwoot_token,
|
||||
url: chatwoot_url,
|
||||
sign_msg: chatwoot_sign_msg || false,
|
||||
reopen_conversation: chatwoot_reopen_conversation || false,
|
||||
conversation_pending: chatwoot_conversation_pending || false,
|
||||
import_contacts: chatwoot_import_contacts ?? true,
|
||||
import_messages: chatwoot_import_messages ?? true,
|
||||
days_limit_import_messages: chatwoot_days_limit_import_messages || 60,
|
||||
number,
|
||||
name_inbox: instance.instanceName,
|
||||
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error.message[0]);
|
||||
throw new BadRequestException(error.message[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public async connectToWhatsapp({ instanceName, number = null, mobile = null }: InstanceDto) {
|
||||
try {
|
||||
this.logger.verbose('requested connectToWhatsapp from ' + instanceName + ' instance');
|
||||
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
const state = instance?.connectionStatus?.state;
|
||||
|
||||
this.logger.verbose('state: ' + state);
|
||||
|
||||
if (!state) {
|
||||
throw new BadRequestException('The "' + instanceName + '" instance does not exist');
|
||||
}
|
||||
|
||||
if (state == 'open') {
|
||||
return await this.connectionState({ instanceName });
|
||||
}
|
||||
|
||||
if (state == 'connecting') {
|
||||
return instance.qrCode;
|
||||
}
|
||||
|
||||
if (state == 'close') {
|
||||
this.logger.verbose('connecting');
|
||||
await instance.connectToWhatsapp(number, mobile);
|
||||
|
||||
await delay(5000);
|
||||
return instance.qrCode;
|
||||
}
|
||||
|
||||
return {
|
||||
instance: {
|
||||
instanceName: instanceName,
|
||||
status: state,
|
||||
},
|
||||
qrcode: instance?.qrCode,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async restartInstance({ instanceName }: InstanceDto) {
|
||||
try {
|
||||
this.logger.verbose('requested restartInstance from ' + instanceName + ' instance');
|
||||
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
const state = instance?.connectionStatus?.state;
|
||||
|
||||
switch (state) {
|
||||
case 'open':
|
||||
this.logger.verbose('logging out instance: ' + instanceName);
|
||||
instance.clearCacheChatwoot();
|
||||
await instance.reloadConnection();
|
||||
await delay(2000);
|
||||
|
||||
return await this.connectionState({ instanceName });
|
||||
default:
|
||||
return await this.connectionState({ instanceName });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async registerMobileCode({ instanceName }: InstanceDto, { mobileCode }: any) {
|
||||
try {
|
||||
this.logger.verbose('requested registerMobileCode from ' + instanceName + ' instance');
|
||||
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
console.log('mobileCode', mobileCode);
|
||||
await instance.receiveMobileCode(mobileCode);
|
||||
return { status: 'SUCCESS', error: false, response: { message: 'Mobile code registered' } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async connectionState({ instanceName }: InstanceDto) {
|
||||
this.logger.verbose('requested connectionState from ' + instanceName + ' instance');
|
||||
return {
|
||||
instance: {
|
||||
instanceName: instanceName,
|
||||
state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async fetchInstances({ instanceName, instanceId, number }: InstanceDto) {
|
||||
if (instanceName) {
|
||||
this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance');
|
||||
this.logger.verbose('instanceName: ' + instanceName);
|
||||
return this.waMonitor.instanceInfo(instanceName);
|
||||
} else if (instanceId || number) {
|
||||
return this.waMonitor.instanceInfoById(instanceId, number);
|
||||
}
|
||||
|
||||
this.logger.verbose('requested fetchInstances (all instances)');
|
||||
return this.waMonitor.instanceInfo();
|
||||
}
|
||||
|
||||
public async setPresence({ instanceName }: InstanceDto, data: SetPresenceDto) {
|
||||
this.logger.verbose('requested sendPresence from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].setPresence(data);
|
||||
}
|
||||
|
||||
public async logout({ instanceName }: InstanceDto) {
|
||||
this.logger.verbose('requested logout from ' + instanceName + ' instance');
|
||||
const { instance } = await this.connectionState({ instanceName });
|
||||
|
||||
if (instance.state === 'close') {
|
||||
throw new BadRequestException('The "' + instanceName + '" instance is not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
this.waMonitor.waInstances[instanceName]?.logoutInstance();
|
||||
|
||||
return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } };
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteInstance({ instanceName }: InstanceDto) {
|
||||
this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance');
|
||||
const { instance } = await this.connectionState({ instanceName });
|
||||
|
||||
if (instance.state === 'open') {
|
||||
throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected');
|
||||
}
|
||||
try {
|
||||
this.waMonitor.waInstances[instanceName]?.removeRabbitmqQueues();
|
||||
this.waMonitor.waInstances[instanceName]?.clearCacheChatwoot();
|
||||
|
||||
if (instance.state === 'connecting') {
|
||||
this.logger.verbose('logging out instance: ' + instanceName);
|
||||
|
||||
await this.logout({ instanceName });
|
||||
}
|
||||
|
||||
this.logger.verbose('deleting instance: ' + instanceName);
|
||||
|
||||
try {
|
||||
this.waMonitor.waInstances[instanceName].sendDataWebhook(Events.INSTANCE_DELETE, {
|
||||
instanceName,
|
||||
instanceId: (await this.repository.auth.find(instanceName))?.instanceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
delete this.waMonitor.waInstances[instanceName];
|
||||
this.eventEmitter.emit('remove.instance', instanceName, 'inner');
|
||||
return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } };
|
||||
} catch (error) {
|
||||
throw new BadRequestException(error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshToken(_: InstanceDto, oldToken: OldToken) {
|
||||
this.logger.verbose('requested refreshToken');
|
||||
return await this.authService.refreshToken(oldToken);
|
||||
}
|
||||
}
|
||||
20
src/api/controllers/label.controller.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { HandleLabelDto } from '../dto/label.dto';
|
||||
import { WAMonitoringService } from '../services/monitor.service';
|
||||
|
||||
const logger = new Logger('LabelController');
|
||||
|
||||
export class LabelController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async fetchLabels({ instanceName }: InstanceDto) {
|
||||
logger.verbose('requested fetchLabels from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].fetchLabels();
|
||||
}
|
||||
|
||||
public async handleLabel({ instanceName }: InstanceDto, data: HandleLabelDto) {
|
||||
logger.verbose('requested chat label change from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].handleLabel(data);
|
||||
}
|
||||
}
|
||||
72
src/api/controllers/proxy.controller.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import { BadRequestException, NotFoundException } from '../../exceptions';
|
||||
import { makeProxyAgent } from '../../utils/makeProxyAgent';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { ProxyDto } from '../dto/proxy.dto';
|
||||
import { WAMonitoringService } from '../services/monitor.service';
|
||||
import { ProxyService } from '../services/proxy.service';
|
||||
|
||||
const logger = new Logger('ProxyController');
|
||||
|
||||
export class ProxyController {
|
||||
constructor(private readonly proxyService: ProxyService, private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async createProxy(instance: InstanceDto, data: ProxyDto) {
|
||||
logger.verbose('requested createProxy from ' + instance.instanceName + ' instance');
|
||||
|
||||
if (!this.waMonitor.waInstances[instance.instanceName]) {
|
||||
throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`);
|
||||
}
|
||||
|
||||
if (!data.enabled) {
|
||||
logger.verbose('proxy disabled');
|
||||
data.proxy = null;
|
||||
}
|
||||
|
||||
if (data.proxy) {
|
||||
const testProxy = await this.testProxy(data.proxy);
|
||||
if (!testProxy) {
|
||||
throw new BadRequestException('Invalid proxy');
|
||||
}
|
||||
logger.verbose('proxy enabled');
|
||||
}
|
||||
|
||||
return this.proxyService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findProxy(instance: InstanceDto) {
|
||||
logger.verbose('requested findProxy from ' + instance.instanceName + ' instance');
|
||||
|
||||
if (!this.waMonitor.waInstances[instance.instanceName]) {
|
||||
throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`);
|
||||
}
|
||||
|
||||
return this.proxyService.find(instance);
|
||||
}
|
||||
|
||||
public async testProxy(proxy: ProxyDto['proxy']) {
|
||||
logger.verbose('requested testProxy');
|
||||
try {
|
||||
const serverIp = await axios.get('https://icanhazip.com/');
|
||||
const response = await axios.get('https://icanhazip.com/', {
|
||||
httpsAgent: makeProxyAgent(proxy),
|
||||
});
|
||||
|
||||
logger.verbose('[testProxy] from IP: ' + response?.data + ' To IP: ' + serverIp?.data);
|
||||
return response?.data !== serverIp?.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
logger.error('testProxy error: ' + error.response.data);
|
||||
} else if (axios.isAxiosError(error)) {
|
||||
logger.error('testProxy error: ');
|
||||
logger.verbose(error.cause ?? error.message);
|
||||
} else {
|
||||
logger.error('testProxy error: ');
|
||||
logger.verbose(error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/api/controllers/sendMessage.controller.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { isBase64, isURL } from 'class-validator';
|
||||
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import { BadRequestException } from '../../exceptions';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import {
|
||||
SendAudioDto,
|
||||
SendButtonDto,
|
||||
SendContactDto,
|
||||
SendListDto,
|
||||
SendLocationDto,
|
||||
SendMediaDto,
|
||||
SendPollDto,
|
||||
SendReactionDto,
|
||||
SendStatusDto,
|
||||
SendStickerDto,
|
||||
SendTemplateDto,
|
||||
SendTextDto,
|
||||
} from '../dto/sendMessage.dto';
|
||||
import { WAMonitoringService } from '../services/monitor.service';
|
||||
|
||||
const logger = new Logger('MessageRouter');
|
||||
|
||||
export class SendMessageController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async sendText({ instanceName }: InstanceDto, data: SendTextDto) {
|
||||
logger.verbose('requested sendText from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].textMessage(data);
|
||||
}
|
||||
|
||||
public async sendTemplate({ instanceName }: InstanceDto, data: SendTemplateDto) {
|
||||
logger.verbose('requested sendList from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].templateMessage(data);
|
||||
}
|
||||
|
||||
public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) {
|
||||
logger.verbose('requested sendMedia from ' + instanceName + ' instance');
|
||||
|
||||
if (
|
||||
isBase64(data?.mediaMessage?.media) &&
|
||||
!data?.mediaMessage?.fileName &&
|
||||
data?.mediaMessage?.mediatype === 'document'
|
||||
) {
|
||||
throw new BadRequestException('For base64 the file name must be informed.');
|
||||
}
|
||||
|
||||
logger.verbose('isURL: ' + isURL(data?.mediaMessage?.media) + ', isBase64: ' + isBase64(data?.mediaMessage?.media));
|
||||
if (isURL(data?.mediaMessage?.media) || isBase64(data?.mediaMessage?.media)) {
|
||||
return await this.waMonitor.waInstances[instanceName].mediaMessage(data);
|
||||
}
|
||||
throw new BadRequestException('Owned media must be a url or base64');
|
||||
}
|
||||
|
||||
public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto) {
|
||||
logger.verbose('requested sendSticker from ' + instanceName + ' instance');
|
||||
|
||||
logger.verbose(
|
||||
'isURL: ' + isURL(data?.stickerMessage?.image) + ', isBase64: ' + isBase64(data?.stickerMessage?.image),
|
||||
);
|
||||
if (isURL(data.stickerMessage.image) || isBase64(data.stickerMessage.image)) {
|
||||
return await this.waMonitor.waInstances[instanceName].mediaSticker(data);
|
||||
}
|
||||
throw new BadRequestException('Owned media must be a url or base64');
|
||||
}
|
||||
|
||||
public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) {
|
||||
logger.verbose('requested sendWhatsAppAudio from ' + instanceName + ' instance');
|
||||
|
||||
logger.verbose('isURL: ' + isURL(data?.audioMessage?.audio) + ', isBase64: ' + isBase64(data?.audioMessage?.audio));
|
||||
if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) {
|
||||
return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data);
|
||||
}
|
||||
throw new BadRequestException('Owned media must be a url or base64');
|
||||
}
|
||||
|
||||
public async sendButtons({ instanceName }: InstanceDto, data: SendButtonDto) {
|
||||
logger.verbose('requested sendButtons from ' + instanceName + ' instance');
|
||||
if (isBase64(data.buttonMessage.mediaMessage?.media) && !data.buttonMessage.mediaMessage?.fileName) {
|
||||
throw new BadRequestException('For bse64 the file name must be informed.');
|
||||
}
|
||||
return await this.waMonitor.waInstances[instanceName].buttonMessage(data);
|
||||
}
|
||||
|
||||
public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) {
|
||||
logger.verbose('requested sendLocation from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].locationMessage(data);
|
||||
}
|
||||
|
||||
public async sendList({ instanceName }: InstanceDto, data: SendListDto) {
|
||||
logger.verbose('requested sendList from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].listMessage(data);
|
||||
}
|
||||
|
||||
public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) {
|
||||
logger.verbose('requested sendContact from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].contactMessage(data);
|
||||
}
|
||||
|
||||
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
|
||||
logger.verbose('requested sendReaction from ' + instanceName + ' instance');
|
||||
if (!data.reactionMessage.reaction.match(/[^()\w\sà-ú"-+]+/)) {
|
||||
throw new BadRequestException('"reaction" must be an emoji');
|
||||
}
|
||||
return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
|
||||
}
|
||||
|
||||
public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) {
|
||||
logger.verbose('requested sendPoll from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].pollMessage(data);
|
||||
}
|
||||
|
||||
public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto) {
|
||||
logger.verbose('requested sendStatus from ' + instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instanceName].statusMessage(data);
|
||||
}
|
||||
}
|
||||
22
src/api/controllers/settings.controller.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { SettingsDto } from '../dto/settings.dto';
|
||||
import { SettingsService } from '../services/settings.service';
|
||||
|
||||
const logger = new Logger('SettingsController');
|
||||
|
||||
export class SettingsController {
|
||||
constructor(private readonly settingsService: SettingsService) {}
|
||||
|
||||
public async createSettings(instance: InstanceDto, data: SettingsDto) {
|
||||
logger.verbose('requested createSettings from ' + instance.instanceName + ' instance');
|
||||
|
||||
return this.settingsService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findSettings(instance: InstanceDto) {
|
||||
logger.verbose('requested findSettings from ' + instance.instanceName + ' instance');
|
||||
const settings = this.settingsService.find(instance);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
72
src/api/controllers/webhook.controller.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { isURL } from 'class-validator';
|
||||
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import { BadRequestException } from '../../exceptions';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { WebhookDto } from '../dto/webhook.dto';
|
||||
import { WAMonitoringService } from '../services/monitor.service';
|
||||
import { WebhookService } from '../services/webhook.service';
|
||||
|
||||
const logger = new Logger('WebhookController');
|
||||
|
||||
export class WebhookController {
|
||||
constructor(private readonly webhookService: WebhookService, private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async createWebhook(instance: InstanceDto, data: WebhookDto) {
|
||||
logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance');
|
||||
|
||||
if (!isURL(data.url, { require_tld: false })) {
|
||||
throw new BadRequestException('Invalid "url" property');
|
||||
}
|
||||
|
||||
data.enabled = data.enabled ?? true;
|
||||
|
||||
if (!data.enabled) {
|
||||
logger.verbose('webhook disabled');
|
||||
data.url = '';
|
||||
data.events = [];
|
||||
} else if (data.events.length === 0) {
|
||||
logger.verbose('webhook events empty');
|
||||
data.events = [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
];
|
||||
}
|
||||
|
||||
return this.webhookService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findWebhook(instance: InstanceDto) {
|
||||
logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance');
|
||||
return this.webhookService.find(instance);
|
||||
}
|
||||
|
||||
public async receiveWebhook(instance: InstanceDto, data: any) {
|
||||
logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance');
|
||||
return await this.waMonitor.waInstances[instance.instanceName].connectToWhatsapp(data);
|
||||
}
|
||||
}
|
||||
122
src/api/dto/chat.dto.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys';
|
||||
|
||||
export class OnWhatsAppDto {
|
||||
constructor(
|
||||
public readonly jid: string,
|
||||
public readonly exists: boolean,
|
||||
public readonly number: string,
|
||||
public readonly name?: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class getBase64FromMediaMessageDto {
|
||||
message: proto.WebMessageInfo;
|
||||
convertToMp4?: boolean;
|
||||
}
|
||||
|
||||
export class WhatsAppNumberDto {
|
||||
numbers: string[];
|
||||
}
|
||||
|
||||
export class NumberDto {
|
||||
number: string;
|
||||
}
|
||||
|
||||
export class NumberBusiness {
|
||||
wid?: string;
|
||||
jid?: string;
|
||||
exists?: boolean;
|
||||
isBusiness: boolean;
|
||||
name?: string;
|
||||
message?: string;
|
||||
description?: string;
|
||||
email?: string;
|
||||
websites?: string[];
|
||||
website?: string[];
|
||||
address?: string;
|
||||
about?: string;
|
||||
vertical?: string;
|
||||
profilehandle?: string;
|
||||
}
|
||||
|
||||
export class ProfileNameDto {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class ProfileStatusDto {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export class ProfilePictureDto {
|
||||
number?: string;
|
||||
// url or base64
|
||||
picture?: string;
|
||||
}
|
||||
|
||||
class Key {
|
||||
id: string;
|
||||
fromMe: boolean;
|
||||
remoteJid: string;
|
||||
}
|
||||
export class ReadMessageDto {
|
||||
read_messages: Key[];
|
||||
}
|
||||
|
||||
export class LastMessage {
|
||||
key: Key;
|
||||
messageTimestamp?: number;
|
||||
}
|
||||
|
||||
export class ArchiveChatDto {
|
||||
lastMessage?: LastMessage;
|
||||
chat?: string;
|
||||
archive: boolean;
|
||||
}
|
||||
|
||||
class PrivacySetting {
|
||||
readreceipts: WAReadReceiptsValue;
|
||||
profile: WAPrivacyValue;
|
||||
status: WAPrivacyValue;
|
||||
online: WAPrivacyOnlineValue;
|
||||
last: WAPrivacyValue;
|
||||
groupadd: WAPrivacyValue;
|
||||
}
|
||||
|
||||
export class PrivacySettingDto {
|
||||
privacySettings: PrivacySetting;
|
||||
}
|
||||
|
||||
export class DeleteMessage {
|
||||
id: string;
|
||||
fromMe: boolean;
|
||||
remoteJid: string;
|
||||
participant?: string;
|
||||
}
|
||||
export class Options {
|
||||
delay?: number;
|
||||
presence?: WAPresence;
|
||||
}
|
||||
class OptionsMessage {
|
||||
options: Options;
|
||||
}
|
||||
export class Metadata extends OptionsMessage {
|
||||
number: string;
|
||||
}
|
||||
|
||||
export class SendPresenceDto extends Metadata {
|
||||
options: {
|
||||
presence: WAPresence;
|
||||
delay: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class UpdateMessageDto extends Metadata {
|
||||
number: string;
|
||||
key: proto.IMessageKey;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class BlockUserDto {
|
||||
number: string;
|
||||
status: 'block' | 'unblock';
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
export class CreateGroupDto {
|
||||
subject: string;
|
||||
description?: string;
|
||||
participants: string[];
|
||||
description?: string;
|
||||
promoteParticipants?: boolean;
|
||||
}
|
||||
|
||||
export class GroupPictureDto {
|
||||
@@ -9,14 +10,38 @@ export class GroupPictureDto {
|
||||
image: string;
|
||||
}
|
||||
|
||||
export class GroupSubjectDto {
|
||||
groupJid: string;
|
||||
subject: string;
|
||||
}
|
||||
|
||||
export class GroupDescriptionDto {
|
||||
groupJid: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class GroupJid {
|
||||
groupJid: string;
|
||||
}
|
||||
|
||||
export class GetParticipant {
|
||||
getParticipants: string;
|
||||
}
|
||||
|
||||
export class GroupInvite {
|
||||
inviteCode: string;
|
||||
}
|
||||
|
||||
export class AcceptGroupInvite {
|
||||
inviteCode: string;
|
||||
}
|
||||
|
||||
export class GroupSendInvite {
|
||||
groupJid: string;
|
||||
description: string;
|
||||
numbers: string[];
|
||||
}
|
||||
|
||||
export class GroupUpdateParticipantDto extends GroupJid {
|
||||
action: 'add' | 'remove' | 'promote' | 'demote';
|
||||
participants: string[];
|
||||
51
src/api/dto/instance.dto.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { WAPresence } from '@whiskeysockets/baileys';
|
||||
|
||||
import { ProxyDto } from './proxy.dto';
|
||||
|
||||
export class InstanceDto {
|
||||
instanceName: string;
|
||||
instanceId?: string;
|
||||
qrcode?: boolean;
|
||||
number?: string;
|
||||
mobile?: boolean;
|
||||
integration?: string;
|
||||
token?: string;
|
||||
webhook?: string;
|
||||
webhook_by_events?: boolean;
|
||||
webhook_base64?: boolean;
|
||||
events?: string[];
|
||||
reject_call?: boolean;
|
||||
msg_call?: string;
|
||||
groups_ignore?: boolean;
|
||||
always_online?: boolean;
|
||||
read_messages?: boolean;
|
||||
read_status?: boolean;
|
||||
sync_full_history?: boolean;
|
||||
chatwoot_account_id?: string;
|
||||
chatwoot_token?: string;
|
||||
chatwoot_url?: string;
|
||||
chatwoot_sign_msg?: boolean;
|
||||
chatwoot_reopen_conversation?: boolean;
|
||||
chatwoot_conversation_pending?: boolean;
|
||||
chatwoot_import_contacts?: boolean;
|
||||
chatwoot_import_messages?: boolean;
|
||||
chatwoot_days_limit_import_messages?: number;
|
||||
websocket_enabled?: boolean;
|
||||
websocket_events?: string[];
|
||||
rabbitmq_enabled?: boolean;
|
||||
rabbitmq_events?: string[];
|
||||
sqs_enabled?: boolean;
|
||||
sqs_events?: string[];
|
||||
typebot_url?: string;
|
||||
typebot?: string;
|
||||
typebot_expire?: number;
|
||||
typebot_keyword_finish?: string;
|
||||
typebot_delay_message?: number;
|
||||
typebot_unknown_message?: string;
|
||||
typebot_listening_from_me?: boolean;
|
||||
proxy?: ProxyDto['proxy'];
|
||||
}
|
||||
|
||||
export class SetPresenceDto {
|
||||
presence: WAPresence;
|
||||
}
|
||||
5
src/api/dto/integration.dto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class IntegrationDto {
|
||||
integration: string;
|
||||
number: string;
|
||||
token: string;
|
||||
}
|
||||
12
src/api/dto/label.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export class LabelDto {
|
||||
id?: string;
|
||||
name: string;
|
||||
color: number;
|
||||
predefinedId?: string;
|
||||
}
|
||||
|
||||
export class HandleLabelDto {
|
||||
number: string;
|
||||
labelId: string;
|
||||
action: 'add' | 'remove';
|
||||
}
|
||||
12
src/api/dto/proxy.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
class Proxy {
|
||||
host: string;
|
||||
port: string;
|
||||
protocol: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class ProxyDto {
|
||||
enabled: boolean;
|
||||
proxy: Proxy;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { proto, WAPresence } from '@evolution/base';
|
||||
import { proto, WAPresence } from '@whiskeysockets/baileys';
|
||||
|
||||
export class Quoted {
|
||||
key: proto.IMessageKey;
|
||||
@@ -15,6 +15,8 @@ export class Options {
|
||||
presence?: WAPresence;
|
||||
quoted?: Quoted;
|
||||
mentions?: Mentions;
|
||||
linkPreview?: boolean;
|
||||
encoding?: boolean;
|
||||
}
|
||||
class OptionsMessage {
|
||||
options: Options;
|
||||
@@ -28,8 +30,14 @@ class TextMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
class linkPreviewMessage {
|
||||
text: string;
|
||||
export class StatusMessage {
|
||||
type: string;
|
||||
content: string;
|
||||
statusJidList?: string[];
|
||||
allContacts?: boolean;
|
||||
caption?: string;
|
||||
backgroundColor?: string;
|
||||
font?: number;
|
||||
}
|
||||
|
||||
class PollMessage {
|
||||
@@ -38,12 +46,16 @@ class PollMessage {
|
||||
values: string[];
|
||||
messageSecret?: Uint8Array;
|
||||
}
|
||||
|
||||
export class SendTextDto extends Metadata {
|
||||
textMessage: TextMessage;
|
||||
}
|
||||
export class SendPresence extends Metadata {
|
||||
textMessage: TextMessage;
|
||||
}
|
||||
|
||||
export class SendLinkPreviewDto extends Metadata {
|
||||
linkPreview: linkPreviewMessage;
|
||||
export class SendStatusDto extends Metadata {
|
||||
statusMessage: StatusMessage;
|
||||
}
|
||||
|
||||
export class SendPollDto extends Metadata {
|
||||
@@ -53,6 +65,7 @@ export class SendPollDto extends Metadata {
|
||||
export type MediaType = 'image' | 'document' | 'video' | 'audio';
|
||||
export class MediaMessage {
|
||||
mediatype: MediaType;
|
||||
mimetype?: string;
|
||||
caption?: string;
|
||||
// for document
|
||||
fileName?: string;
|
||||
@@ -62,6 +75,12 @@ export class MediaMessage {
|
||||
export class SendMediaDto extends Metadata {
|
||||
mediaMessage: MediaMessage;
|
||||
}
|
||||
class Sticker {
|
||||
image: string;
|
||||
}
|
||||
export class SendStickerDto extends Metadata {
|
||||
stickerMessage: Sticker;
|
||||
}
|
||||
|
||||
class Audio {
|
||||
audio: string;
|
||||
@@ -119,6 +138,19 @@ export class ContactMessage {
|
||||
fullName: string;
|
||||
wuid: string;
|
||||
phoneNumber: string;
|
||||
organization?: string;
|
||||
email?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export class TemplateMessage {
|
||||
name: string;
|
||||
language: string;
|
||||
components: any;
|
||||
}
|
||||
|
||||
export class SendTemplateDto extends Metadata {
|
||||
templateMessage: TemplateMessage;
|
||||
}
|
||||
export class SendContactDto extends Metadata {
|
||||
contactMessage: ContactMessage[];
|
||||
9
src/api/dto/settings.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class SettingsDto {
|
||||
reject_call?: boolean;
|
||||
msg_call?: string;
|
||||
groups_ignore?: boolean;
|
||||
always_online?: boolean;
|
||||
read_messages?: boolean;
|
||||
read_status?: boolean;
|
||||
sync_full_history?: boolean;
|
||||
}
|
||||
7
src/api/dto/webhook.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class WebhookDto {
|
||||
enabled?: boolean;
|
||||
url?: string;
|
||||
events?: string[];
|
||||
webhook_by_events?: boolean;
|
||||
webhook_base64?: boolean;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { isJWT } from 'class-validator';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { name } from '../../../package.json';
|
||||
import { Auth, configService } from '../../config/env.config';
|
||||
import { Logger } from '../../config/logger.config';
|
||||
import { name } from '../../../package.json';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { JwtPayload } from '../services/auth.service';
|
||||
import { ForbiddenException, UnauthorizedException } from '../../exceptions';
|
||||
import { repository } from '../whatsapp.module';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { repository } from '../server.module';
|
||||
import { JwtPayload } from '../services/auth.service';
|
||||
|
||||
const logger = new Logger('GUARD');
|
||||
|
||||
@@ -22,15 +23,8 @@ async function jwtGuard(req: Request, res: Response, next: NextFunction) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (
|
||||
(req.originalUrl.includes('/instance/create') ||
|
||||
req.originalUrl.includes('/instance/fetchInstances')) &&
|
||||
!key
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
'Missing global api key',
|
||||
'The global api key must be set',
|
||||
);
|
||||
if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) {
|
||||
throw new ForbiddenException('Missing global api key', 'The global api key must be set');
|
||||
}
|
||||
|
||||
const jwtOpts = configService.get<Auth>('AUTHENTICATION').JWT;
|
||||
@@ -61,7 +55,7 @@ async function jwtGuard(req: Request, res: Response, next: NextFunction) {
|
||||
}
|
||||
}
|
||||
|
||||
async function apikey(req: Request, res: Response, next: NextFunction) {
|
||||
async function apikey(req: Request, _: Response, next: NextFunction) {
|
||||
const env = configService.get<Auth>('AUTHENTICATION').API_KEY;
|
||||
const key = req.get('apikey');
|
||||
|
||||
@@ -69,15 +63,8 @@ async function apikey(req: Request, res: Response, next: NextFunction) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (
|
||||
(req.originalUrl.includes('/instance/create') ||
|
||||
req.originalUrl.includes('/instance/fetchInstances')) &&
|
||||
!key
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
'Missing global api key',
|
||||
'The global api key must be set',
|
||||
);
|
||||
if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) {
|
||||
throw new ForbiddenException('Missing global api key', 'The global api key must be set');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -86,7 +73,9 @@ async function apikey(req: Request, res: Response, next: NextFunction) {
|
||||
if (instanceKey.apikey === key) {
|
||||
return next();
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
75
src/api/guards/instance.guard.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { CacheConf, configService, Database } from '../../config/env.config';
|
||||
import { INSTANCE_DIR } from '../../config/path.config';
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
} from '../../exceptions';
|
||||
import { dbserver } from '../../libs/db.connect';
|
||||
import { InstanceDto } from '../dto/instance.dto';
|
||||
import { cache, waMonitor } from '../server.module';
|
||||
|
||||
async function getInstance(instanceName: string) {
|
||||
try {
|
||||
const db = configService.get<Database>('DATABASE');
|
||||
const cacheConf = configService.get<CacheConf>('CACHE');
|
||||
|
||||
const exists = !!waMonitor.waInstances[instanceName];
|
||||
|
||||
if (cacheConf.REDIS.ENABLED && cacheConf.REDIS.SAVE_INSTANCES) {
|
||||
const keyExists = await cache.has(instanceName);
|
||||
|
||||
return exists || keyExists;
|
||||
}
|
||||
|
||||
if (db.ENABLED) {
|
||||
const collection = dbserver
|
||||
.getClient()
|
||||
.db(db.CONNECTION.DB_PREFIX_NAME + '-instances')
|
||||
.collection(instanceName);
|
||||
return exists || (await collection.find({}).toArray()).length > 0;
|
||||
}
|
||||
|
||||
return exists || existsSync(join(INSTANCE_DIR, instanceName));
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(error?.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) {
|
||||
if (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const param = req.params as unknown as InstanceDto;
|
||||
if (!param?.instanceName) {
|
||||
throw new BadRequestException('"instanceName" not provided.');
|
||||
}
|
||||
|
||||
if (!(await getInstance(param.instanceName))) {
|
||||
throw new NotFoundException(`The "${param.instanceName}" instance does not exist`);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
export async function instanceLoggedGuard(req: Request, _: Response, next: NextFunction) {
|
||||
if (req.originalUrl.includes('/instance/create')) {
|
||||
const instance = req.body as InstanceDto;
|
||||
if (await getInstance(instance.instanceName)) {
|
||||
throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`);
|
||||
}
|
||||
|
||||
if (waMonitor.waInstances[instance.instanceName]) {
|
||||
waMonitor.waInstances[instance.instanceName]?.removeRabbitmqQueues();
|
||||
delete waMonitor.waInstances[instance.instanceName];
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { ChamaaiDto } from '../dto/chamaai.dto';
|
||||
import { ChamaaiService } from '../services/chamaai.service';
|
||||
|
||||
const logger = new Logger('ChamaaiController');
|
||||
|
||||
export class ChamaaiController {
|
||||
constructor(private readonly chamaaiService: ChamaaiService) {}
|
||||
|
||||
public async createChamaai(instance: InstanceDto, data: ChamaaiDto) {
|
||||
logger.verbose('requested createChamaai from ' + instance.instanceName + ' instance');
|
||||
|
||||
if (!data.enabled) {
|
||||
logger.verbose('chamaai disabled');
|
||||
data.url = '';
|
||||
data.token = '';
|
||||
data.waNumber = '';
|
||||
data.answerByAudio = false;
|
||||
}
|
||||
|
||||
return this.chamaaiService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findChamaai(instance: InstanceDto) {
|
||||
logger.verbose('requested findChamaai from ' + instance.instanceName + ' instance');
|
||||
return this.chamaaiService.find(instance);
|
||||
}
|
||||
}
|
||||
7
src/api/integrations/chamaai/dto/chamaai.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class ChamaaiDto {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
token: string;
|
||||
waNumber: string;
|
||||
answerByAudio: boolean;
|
||||
}
|
||||
24
src/api/integrations/chamaai/models/chamaai.model.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Schema } from 'mongoose';
|
||||
|
||||
import { dbserver } from '../../../../libs/db.connect';
|
||||
|
||||
export class ChamaaiRaw {
|
||||
_id?: string;
|
||||
enabled?: boolean;
|
||||
url?: string;
|
||||
token?: string;
|
||||
waNumber?: string;
|
||||
answerByAudio?: boolean;
|
||||
}
|
||||
|
||||
const chamaaiSchema = new Schema<ChamaaiRaw>({
|
||||
_id: { type: String, _id: true },
|
||||
enabled: { type: Boolean, required: true },
|
||||
url: { type: String, required: true },
|
||||
token: { type: String, required: true },
|
||||
waNumber: { type: String, required: true },
|
||||
answerByAudio: { type: Boolean, required: true },
|
||||
});
|
||||
|
||||
export const ChamaaiModel = dbserver?.model(ChamaaiRaw.name, chamaaiSchema, 'chamaai');
|
||||
export type IChamaaiModel = typeof ChamaaiModel;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { ConfigService } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { IInsert, Repository } from '../../../abstract/abstract.repository';
|
||||
import { ChamaaiRaw, IChamaaiModel } from '../../../models';
|
||||
|
||||
export class ChamaaiRepository extends Repository {
|
||||
constructor(private readonly chamaaiModel: IChamaaiModel, private readonly configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
private readonly logger = new Logger('ChamaaiRepository');
|
||||
|
||||
public async create(data: ChamaaiRaw, instance: string): Promise<IInsert> {
|
||||
try {
|
||||
this.logger.verbose('creating chamaai');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('saving chamaai to db');
|
||||
const insert = await this.chamaaiModel.replaceOne({ _id: instance }, { ...data }, { upsert: true });
|
||||
|
||||
this.logger.verbose('chamaai saved to db: ' + insert.modifiedCount + ' chamaai');
|
||||
return { insertCount: insert.modifiedCount };
|
||||
}
|
||||
|
||||
this.logger.verbose('saving chamaai to store');
|
||||
|
||||
this.writeStore<ChamaaiRaw>({
|
||||
path: join(this.storePath, 'chamaai'),
|
||||
fileName: instance,
|
||||
data,
|
||||
});
|
||||
|
||||
this.logger.verbose('chamaai saved to store in path: ' + join(this.storePath, 'chamaai') + '/' + instance);
|
||||
|
||||
this.logger.verbose('chamaai created');
|
||||
return { insertCount: 1 };
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
public async find(instance: string): Promise<ChamaaiRaw> {
|
||||
try {
|
||||
this.logger.verbose('finding chamaai');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('finding chamaai in db');
|
||||
return await this.chamaaiModel.findOne({ _id: instance });
|
||||
}
|
||||
|
||||
this.logger.verbose('finding chamaai in store');
|
||||
return JSON.parse(
|
||||
readFileSync(join(this.storePath, 'chamaai', instance + '.json'), {
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
) as ChamaaiRaw;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/api/integrations/chamaai/routes/chamaai.router.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { chamaaiSchema, instanceNameSchema } from '../../../../validate/validate.schema';
|
||||
import { RouterBroker } from '../../../abstract/abstract.router';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { HttpStatus } from '../../../routes/index.router';
|
||||
import { chamaaiController } from '../../../server.module';
|
||||
import { ChamaaiDto } from '../dto/chamaai.dto';
|
||||
|
||||
const logger = new Logger('ChamaaiRouter');
|
||||
|
||||
export class ChamaaiRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in setChamaai');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<ChamaaiDto>({
|
||||
request: req,
|
||||
schema: chamaaiSchema,
|
||||
ClassRef: ChamaaiDto,
|
||||
execute: (instance, data) => chamaaiController.createChamaai(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in findChamaai');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceNameSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => chamaaiController.findChamaai(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router = Router();
|
||||
}
|
||||
230
src/api/integrations/chamaai/services/chamaai.service.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import axios from 'axios';
|
||||
import { writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { ConfigService, HttpServer } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { ChamaaiRaw } from '../../../models';
|
||||
import { WAMonitoringService } from '../../../services/monitor.service';
|
||||
import { Events } from '../../../types/wa.types';
|
||||
import { ChamaaiDto } from '../dto/chamaai.dto';
|
||||
|
||||
export class ChamaaiService {
|
||||
constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) {}
|
||||
|
||||
private readonly logger = new Logger(ChamaaiService.name);
|
||||
|
||||
public create(instance: InstanceDto, data: ChamaaiDto) {
|
||||
this.logger.verbose('create chamaai: ' + instance.instanceName);
|
||||
this.waMonitor.waInstances[instance.instanceName].setChamaai(data);
|
||||
|
||||
return { chamaai: { ...instance, chamaai: data } };
|
||||
}
|
||||
|
||||
public async find(instance: InstanceDto): Promise<ChamaaiRaw> {
|
||||
try {
|
||||
this.logger.verbose('find chamaai: ' + instance.instanceName);
|
||||
const result = await this.waMonitor.waInstances[instance.instanceName].findChamaai();
|
||||
|
||||
if (Object.keys(result).length === 0) {
|
||||
throw new Error('Chamaai not found');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { enabled: false, url: '', token: '', waNumber: '', answerByAudio: false };
|
||||
}
|
||||
}
|
||||
|
||||
private getTypeMessage(msg: any) {
|
||||
this.logger.verbose('get type message');
|
||||
|
||||
const types = {
|
||||
conversation: msg.conversation,
|
||||
extendedTextMessage: msg.extendedTextMessage?.text,
|
||||
};
|
||||
|
||||
this.logger.verbose('type message: ' + types);
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
private getMessageContent(types: any) {
|
||||
this.logger.verbose('get message content');
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
|
||||
|
||||
const result = typeKey ? types[typeKey] : undefined;
|
||||
|
||||
this.logger.verbose('message content: ' + result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getConversationMessage(msg: any) {
|
||||
this.logger.verbose('get conversation message');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const messageContent = this.getMessageContent(types);
|
||||
|
||||
this.logger.verbose('conversation message: ' + messageContent);
|
||||
|
||||
return messageContent;
|
||||
}
|
||||
|
||||
private calculateTypingTime(text: string) {
|
||||
const wordsPerMinute = 100;
|
||||
|
||||
const wordCount = text.split(' ').length;
|
||||
const typingTimeInMinutes = wordCount / wordsPerMinute;
|
||||
const typingTimeInMilliseconds = typingTimeInMinutes * 60;
|
||||
return typingTimeInMilliseconds;
|
||||
}
|
||||
|
||||
private convertToMilliseconds(count: number) {
|
||||
const averageCharactersPerSecond = 15;
|
||||
const characterCount = count;
|
||||
const speakingTimeInSeconds = characterCount / averageCharactersPerSecond;
|
||||
return speakingTimeInSeconds;
|
||||
}
|
||||
|
||||
private getRegexPatterns() {
|
||||
const patternsToCheck = [
|
||||
'.*atend.*humano.*',
|
||||
'.*falar.*com.*um.*humano.*',
|
||||
'.*fala.*humano.*',
|
||||
'.*atend.*humano.*',
|
||||
'.*fala.*atend.*',
|
||||
'.*preciso.*ajuda.*',
|
||||
'.*quero.*suporte.*',
|
||||
'.*preciso.*assiste.*',
|
||||
'.*ajuda.*atend.*',
|
||||
'.*chama.*atendente.*',
|
||||
'.*suporte.*urgente.*',
|
||||
'.*atend.*por.*favor.*',
|
||||
'.*quero.*falar.*com.*alguém.*',
|
||||
'.*falar.*com.*um.*humano.*',
|
||||
'.*transfer.*humano.*',
|
||||
'.*transfer.*atend.*',
|
||||
'.*equipe.*humano.*',
|
||||
'.*suporte.*humano.*',
|
||||
];
|
||||
|
||||
const regexPatterns = patternsToCheck.map((pattern) => new RegExp(pattern, 'iu'));
|
||||
return regexPatterns;
|
||||
}
|
||||
|
||||
public async sendChamaai(instance: InstanceDto, remoteJid: string, msg: any) {
|
||||
const content = this.getConversationMessage(msg.message);
|
||||
const msgType = msg.messageType;
|
||||
const find = await this.find(instance);
|
||||
const url = find.url;
|
||||
const token = find.token;
|
||||
const waNumber = find.waNumber;
|
||||
const answerByAudio = find.answerByAudio;
|
||||
|
||||
if (!content && msgType !== 'audioMessage') {
|
||||
return;
|
||||
}
|
||||
|
||||
let data;
|
||||
let endpoint;
|
||||
|
||||
if (msgType === 'audioMessage') {
|
||||
const downloadBase64 = await this.waMonitor.waInstances[instance.instanceName].getBase64FromMediaMessage({
|
||||
message: {
|
||||
...msg,
|
||||
},
|
||||
});
|
||||
|
||||
const random = Math.random().toString(36).substring(7);
|
||||
const nameFile = `${random}.ogg`;
|
||||
|
||||
const fileData = Buffer.from(downloadBase64.base64, 'base64');
|
||||
|
||||
const fileName = `${path.join(
|
||||
this.waMonitor.waInstances[instance.instanceName].storePath,
|
||||
'temp',
|
||||
`${nameFile}`,
|
||||
)}`;
|
||||
|
||||
writeFileSync(fileName, fileData, 'utf8');
|
||||
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
const url = `${urlServer}/store/temp/${nameFile}`;
|
||||
|
||||
data = {
|
||||
waNumber: waNumber,
|
||||
audioUrl: url,
|
||||
queryNumber: remoteJid.split('@')[0],
|
||||
answerByAudio: answerByAudio,
|
||||
};
|
||||
endpoint = 'processMessageAudio';
|
||||
} else {
|
||||
data = {
|
||||
waNumber: waNumber,
|
||||
question: content,
|
||||
queryNumber: remoteJid.split('@')[0],
|
||||
answerByAudio: answerByAudio,
|
||||
};
|
||||
endpoint = 'processMessageText';
|
||||
}
|
||||
|
||||
const request = await axios.post(`${url}/${endpoint}`, data, {
|
||||
headers: {
|
||||
Authorization: `${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const answer = request.data?.answer;
|
||||
|
||||
const type = request.data?.type;
|
||||
|
||||
const characterCount = request.data?.characterCount;
|
||||
|
||||
if (answer) {
|
||||
if (type === 'text') {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: this.calculateTypingTime(answer) * 1000 || 1000,
|
||||
presence: 'composing',
|
||||
linkPreview: false,
|
||||
quoted: {
|
||||
key: msg.key,
|
||||
message: msg.message,
|
||||
},
|
||||
},
|
||||
textMessage: {
|
||||
text: answer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'audio') {
|
||||
this.waMonitor.waInstances[instance.instanceName].audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: characterCount ? this.convertToMilliseconds(characterCount) * 1000 || 1000 : 1000,
|
||||
presence: 'recording',
|
||||
encoding: true,
|
||||
},
|
||||
audioMessage: {
|
||||
audio: answer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.getRegexPatterns().some((pattern) => pattern.test(answer))) {
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.CHAMA_AI_ACTION, {
|
||||
remoteJid: remoteJid,
|
||||
message: msg,
|
||||
answer: answer,
|
||||
action: 'transfer',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/api/integrations/chamaai/validate/chamaai.schema.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
};
|
||||
};
|
||||
|
||||
export const chamaaiSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', enum: [true, false] },
|
||||
url: { type: 'string' },
|
||||
token: { type: 'string' },
|
||||
waNumber: { type: 'string' },
|
||||
answerByAudio: { type: 'boolean', enum: [true, false] },
|
||||
},
|
||||
required: ['enabled', 'url', 'token', 'waNumber', 'answerByAudio'],
|
||||
...isNotEmpty('enabled', 'url', 'token', 'waNumber', 'answerByAudio'),
|
||||
};
|
||||
108
src/api/integrations/chatwoot/controllers/chatwoot.controller.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { isURL } from 'class-validator';
|
||||
|
||||
import { CacheEngine } from '../../../../cache/cacheengine';
|
||||
import { ConfigService, HttpServer } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { BadRequestException } from '../../../../exceptions';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { RepositoryBroker } from '../../../repository/repository.manager';
|
||||
import { waMonitor } from '../../../server.module';
|
||||
import { CacheService } from '../../../services/cache.service';
|
||||
import { ChatwootDto } from '../dto/chatwoot.dto';
|
||||
import { ChatwootService } from '../services/chatwoot.service';
|
||||
|
||||
const logger = new Logger('ChatwootController');
|
||||
|
||||
export class ChatwootController {
|
||||
constructor(
|
||||
private readonly chatwootService: ChatwootService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly repository: RepositoryBroker,
|
||||
) {}
|
||||
|
||||
public async createChatwoot(instance: InstanceDto, data: ChatwootDto) {
|
||||
logger.verbose('requested createChatwoot from ' + instance.instanceName + ' instance');
|
||||
|
||||
if (data.enabled) {
|
||||
if (!isURL(data.url, { require_tld: false })) {
|
||||
throw new BadRequestException('url is not valid');
|
||||
}
|
||||
|
||||
if (!data.account_id) {
|
||||
throw new BadRequestException('account_id is required');
|
||||
}
|
||||
|
||||
if (!data.token) {
|
||||
throw new BadRequestException('token is required');
|
||||
}
|
||||
|
||||
if (data.sign_msg !== true && data.sign_msg !== false) {
|
||||
throw new BadRequestException('sign_msg is required');
|
||||
}
|
||||
if (data.sign_msg === false) data.sign_delimiter = null;
|
||||
}
|
||||
|
||||
if (!data.enabled) {
|
||||
logger.verbose('chatwoot disabled');
|
||||
data.account_id = '';
|
||||
data.token = '';
|
||||
data.url = '';
|
||||
data.sign_msg = false;
|
||||
data.sign_delimiter = null;
|
||||
data.reopen_conversation = false;
|
||||
data.conversation_pending = false;
|
||||
data.import_contacts = false;
|
||||
data.import_messages = false;
|
||||
data.days_limit_import_messages = 0;
|
||||
data.auto_create = false;
|
||||
}
|
||||
|
||||
data.name_inbox = instance.instanceName;
|
||||
|
||||
const result = await this.chatwootService.create(instance, data);
|
||||
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
const response = {
|
||||
...result,
|
||||
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async findChatwoot(instance: InstanceDto) {
|
||||
logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance');
|
||||
const result = await this.chatwootService.find(instance);
|
||||
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
if (Object.keys(result || {}).length === 0) {
|
||||
return {
|
||||
enabled: false,
|
||||
url: '',
|
||||
account_id: '',
|
||||
token: '',
|
||||
sign_msg: false,
|
||||
name_inbox: '',
|
||||
webhook_url: '',
|
||||
};
|
||||
}
|
||||
|
||||
const response = {
|
||||
...result,
|
||||
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async receiveWebhook(instance: InstanceDto, data: any) {
|
||||
logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance');
|
||||
|
||||
const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine());
|
||||
const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository, chatwootCache);
|
||||
|
||||
return chatwootService.receiveWebhook(instance, data);
|
||||
}
|
||||
}
|
||||
16
src/api/integrations/chatwoot/dto/chatwoot.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export class ChatwootDto {
|
||||
enabled?: boolean;
|
||||
account_id?: string;
|
||||
token?: string;
|
||||
url?: string;
|
||||
name_inbox?: string;
|
||||
sign_msg?: boolean;
|
||||
sign_delimiter?: string;
|
||||
number?: string;
|
||||
reopen_conversation?: boolean;
|
||||
conversation_pending?: boolean;
|
||||
import_contacts?: boolean;
|
||||
import_messages?: boolean;
|
||||
days_limit_import_messages?: number;
|
||||
auto_create?: boolean;
|
||||
}
|
||||
49
src/api/integrations/chatwoot/libs/postgres.client.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import postgresql from 'pg';
|
||||
|
||||
import { Chatwoot, configService } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
|
||||
const { Pool } = postgresql;
|
||||
|
||||
class Postgres {
|
||||
private logger = new Logger(Postgres.name);
|
||||
private pool;
|
||||
private connected = false;
|
||||
|
||||
getConnection(connectionString: string) {
|
||||
if (this.connected) {
|
||||
return this.pool;
|
||||
} else {
|
||||
this.pool = new Pool({
|
||||
connectionString,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
this.pool.on('error', () => {
|
||||
this.logger.error('postgres disconnected');
|
||||
this.connected = false;
|
||||
});
|
||||
|
||||
try {
|
||||
this.logger.verbose('connecting new postgres');
|
||||
this.connected = true;
|
||||
} catch (e) {
|
||||
this.connected = false;
|
||||
this.logger.error('postgres connect exception caught: ' + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pool;
|
||||
}
|
||||
}
|
||||
|
||||
getChatwootConnection() {
|
||||
const uri = configService.get<Chatwoot>('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI;
|
||||
|
||||
return this.getConnection(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export const postgresClient = new Postgres();
|
||||
40
src/api/integrations/chatwoot/models/chatwoot.model.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Schema } from 'mongoose';
|
||||
|
||||
import { dbserver } from '../../../../libs/db.connect';
|
||||
|
||||
export class ChatwootRaw {
|
||||
_id?: string;
|
||||
enabled?: boolean;
|
||||
account_id?: string;
|
||||
token?: string;
|
||||
url?: string;
|
||||
name_inbox?: string;
|
||||
sign_msg?: boolean;
|
||||
sign_delimiter?: string;
|
||||
number?: string;
|
||||
reopen_conversation?: boolean;
|
||||
conversation_pending?: boolean;
|
||||
import_contacts?: boolean;
|
||||
import_messages?: boolean;
|
||||
days_limit_import_messages?: number;
|
||||
}
|
||||
|
||||
const chatwootSchema = new Schema<ChatwootRaw>({
|
||||
_id: { type: String, _id: true },
|
||||
enabled: { type: Boolean, required: true },
|
||||
account_id: { type: String, required: true },
|
||||
token: { type: String, required: true },
|
||||
url: { type: String, required: true },
|
||||
name_inbox: { type: String, required: true },
|
||||
sign_msg: { type: Boolean, required: true },
|
||||
sign_delimiter: { type: String, required: false },
|
||||
number: { type: String, required: true },
|
||||
reopen_conversation: { type: Boolean, required: true },
|
||||
conversation_pending: { type: Boolean, required: true },
|
||||
import_contacts: { type: Boolean, required: true },
|
||||
import_messages: { type: Boolean, required: true },
|
||||
days_limit_import_messages: { type: Number, required: true },
|
||||
});
|
||||
|
||||
export const ChatwootModel = dbserver?.model(ChatwootRaw.name, chatwootSchema, 'chatwoot');
|
||||
export type IChatwootModel = typeof ChatwootModel;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { ConfigService } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { IInsert, Repository } from '../../../abstract/abstract.repository';
|
||||
import { ChatwootRaw, IChatwootModel } from '../../../models';
|
||||
|
||||
export class ChatwootRepository extends Repository {
|
||||
constructor(private readonly chatwootModel: IChatwootModel, private readonly configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
private readonly logger = new Logger('ChatwootRepository');
|
||||
|
||||
public async create(data: ChatwootRaw, instance: string): Promise<IInsert> {
|
||||
try {
|
||||
this.logger.verbose('creating chatwoot');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('saving chatwoot to db');
|
||||
const insert = await this.chatwootModel.replaceOne({ _id: instance }, { ...data }, { upsert: true });
|
||||
|
||||
this.logger.verbose('chatwoot saved to db: ' + insert.modifiedCount + ' chatwoot');
|
||||
return { insertCount: insert.modifiedCount };
|
||||
}
|
||||
|
||||
this.logger.verbose('saving chatwoot to store');
|
||||
|
||||
this.writeStore<ChatwootRaw>({
|
||||
path: join(this.storePath, 'chatwoot'),
|
||||
fileName: instance,
|
||||
data,
|
||||
});
|
||||
|
||||
this.logger.verbose('chatwoot saved to store in path: ' + join(this.storePath, 'chatwoot') + '/' + instance);
|
||||
|
||||
this.logger.verbose('chatwoot created');
|
||||
return { insertCount: 1 };
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
public async find(instance: string): Promise<ChatwootRaw> {
|
||||
try {
|
||||
this.logger.verbose('finding chatwoot');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('finding chatwoot in db');
|
||||
return await this.chatwootModel.findOne({ _id: instance });
|
||||
}
|
||||
|
||||
this.logger.verbose('finding chatwoot in store');
|
||||
return JSON.parse(
|
||||
readFileSync(join(this.storePath, 'chatwoot', instance + '.json'), {
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
) as ChatwootRaw;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/api/integrations/chatwoot/routes/chatwoot.router.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { chatwootSchema, instanceNameSchema } from '../../../../validate/validate.schema';
|
||||
import { RouterBroker } from '../../../abstract/abstract.router';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { HttpStatus } from '../../../routes/index.router';
|
||||
import { chatwootController } from '../../../server.module';
|
||||
import { ChatwootDto } from '../dto/chatwoot.dto';
|
||||
|
||||
const logger = new Logger('ChatwootRouter');
|
||||
|
||||
export class ChatwootRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in setChatwoot');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<ChatwootDto>({
|
||||
request: req,
|
||||
schema: chatwootSchema,
|
||||
ClassRef: ChatwootDto,
|
||||
execute: (instance, data) => chatwootController.createChatwoot(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in findChatwoot');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceNameSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => chatwootController.findChatwoot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('webhook'), async (req, res) => {
|
||||
logger.verbose('request received in findChatwoot');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceNameSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => chatwootController.receiveWebhook(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router = Router();
|
||||
}
|
||||
2352
src/api/integrations/chatwoot/services/chatwoot.service.ts
Normal file
472
src/api/integrations/chatwoot/utils/chatwoot-import-helper.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
import { inbox } from '@figuro/chatwoot-sdk';
|
||||
import { proto } from '@whiskeysockets/baileys';
|
||||
|
||||
import { InstanceDto } from '../../../../api/dto/instance.dto';
|
||||
import { ChatwootRaw, ContactRaw, MessageRaw } from '../../../../api/models';
|
||||
import { Chatwoot, configService } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { postgresClient } from '../libs/postgres.client';
|
||||
import { ChatwootService } from '../services/chatwoot.service';
|
||||
|
||||
type ChatwootUser = {
|
||||
user_type: string;
|
||||
user_id: number;
|
||||
};
|
||||
|
||||
type FksChatwoot = {
|
||||
phone_number: string;
|
||||
contact_id: string;
|
||||
conversation_id: string;
|
||||
};
|
||||
|
||||
type firstLastTimestamp = {
|
||||
first: number;
|
||||
last: number;
|
||||
};
|
||||
|
||||
type IWebMessageInfo = Omit<proto.IWebMessageInfo, 'key'> & Partial<Pick<proto.IWebMessageInfo, 'key'>>;
|
||||
|
||||
class ChatwootImport {
|
||||
private logger = new Logger(ChatwootImport.name);
|
||||
private repositoryMessagesCache = new Map<string, Set<string>>();
|
||||
private historyMessages = new Map<string, MessageRaw[]>();
|
||||
private historyContacts = new Map<string, ContactRaw[]>();
|
||||
|
||||
public getRepositoryMessagesCache(instance: InstanceDto) {
|
||||
return this.repositoryMessagesCache.has(instance.instanceName)
|
||||
? this.repositoryMessagesCache.get(instance.instanceName)
|
||||
: null;
|
||||
}
|
||||
|
||||
public setRepositoryMessagesCache(instance: InstanceDto, repositoryMessagesCache: Set<string>) {
|
||||
this.repositoryMessagesCache.set(instance.instanceName, repositoryMessagesCache);
|
||||
}
|
||||
|
||||
public deleteRepositoryMessagesCache(instance: InstanceDto) {
|
||||
this.repositoryMessagesCache.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public addHistoryMessages(instance: InstanceDto, messagesRaw: MessageRaw[]) {
|
||||
const actualValue = this.historyMessages.has(instance.instanceName)
|
||||
? this.historyMessages.get(instance.instanceName)
|
||||
: [];
|
||||
this.historyMessages.set(instance.instanceName, actualValue.concat(messagesRaw));
|
||||
}
|
||||
|
||||
public addHistoryContacts(instance: InstanceDto, contactsRaw: ContactRaw[]) {
|
||||
const actualValue = this.historyContacts.has(instance.instanceName)
|
||||
? this.historyContacts.get(instance.instanceName)
|
||||
: [];
|
||||
this.historyContacts.set(instance.instanceName, actualValue.concat(contactsRaw));
|
||||
}
|
||||
|
||||
public deleteHistoryMessages(instance: InstanceDto) {
|
||||
this.historyMessages.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public deleteHistoryContacts(instance: InstanceDto) {
|
||||
this.historyContacts.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public clearAll(instance: InstanceDto) {
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteHistoryContacts(instance);
|
||||
}
|
||||
|
||||
public getHistoryMessagesLenght(instance: InstanceDto) {
|
||||
return this.historyMessages.get(instance.instanceName)?.length ?? 0;
|
||||
}
|
||||
|
||||
public async importHistoryContacts(instance: InstanceDto, provider: ChatwootRaw) {
|
||||
try {
|
||||
if (this.getHistoryMessagesLenght(instance) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
let totalContactsImported = 0;
|
||||
|
||||
const contacts = this.historyContacts.get(instance.instanceName) || [];
|
||||
if (contacts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let contactsChunk: ContactRaw[] = this.sliceIntoChunks(contacts, 3000);
|
||||
while (contactsChunk.length > 0) {
|
||||
// inserting contacts in chatwoot db
|
||||
let sqlInsert = `INSERT INTO contacts
|
||||
(name, phone_number, account_id, identifier, created_at, updated_at) VALUES `;
|
||||
const bindInsert = [provider.account_id];
|
||||
|
||||
for (const contact of contactsChunk) {
|
||||
bindInsert.push(contact.pushName);
|
||||
const bindName = `$${bindInsert.length}`;
|
||||
|
||||
bindInsert.push(`+${contact.id.split('@')[0]}`);
|
||||
const bindPhoneNumber = `$${bindInsert.length}`;
|
||||
|
||||
bindInsert.push(contact.id);
|
||||
const bindIdentifier = `$${bindInsert.length}`;
|
||||
|
||||
sqlInsert += `(${bindName}, ${bindPhoneNumber}, $1, ${bindIdentifier}, NOW(), NOW()),`;
|
||||
}
|
||||
if (sqlInsert.slice(-1) === ',') {
|
||||
sqlInsert = sqlInsert.slice(0, -1);
|
||||
}
|
||||
sqlInsert += ` ON CONFLICT (identifier, account_id)
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
phone_number = EXCLUDED.phone_number,
|
||||
identifier = EXCLUDED.identifier`;
|
||||
|
||||
totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0;
|
||||
contactsChunk = this.sliceIntoChunks(contacts, 3000);
|
||||
}
|
||||
|
||||
this.deleteHistoryContacts(instance);
|
||||
|
||||
return totalContactsImported;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on import history contacts: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async importHistoryMessages(
|
||||
instance: InstanceDto,
|
||||
chatwootService: ChatwootService,
|
||||
inbox: inbox,
|
||||
provider: ChatwootRaw,
|
||||
) {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const chatwootUser = await this.getChatwootUser(provider);
|
||||
if (!chatwootUser) {
|
||||
throw new Error('User not found to import messages.');
|
||||
}
|
||||
|
||||
let totalMessagesImported = 0;
|
||||
|
||||
const messagesOrdered = this.historyMessages.get(instance.instanceName) || [];
|
||||
if (messagesOrdered.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ordering messages by number and timestamp asc
|
||||
messagesOrdered.sort((a, b) => {
|
||||
return (
|
||||
parseInt(a.key.remoteJid) - parseInt(b.key.remoteJid) ||
|
||||
(a.messageTimestamp as number) - (b.messageTimestamp as number)
|
||||
);
|
||||
});
|
||||
|
||||
const allMessagesMappedByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesOrdered);
|
||||
// Map structure: +552199999999 => { first message timestamp from number, last message timestamp from number}
|
||||
const phoneNumbersWithTimestamp = new Map<string, firstLastTimestamp>();
|
||||
allMessagesMappedByPhoneNumber.forEach((messages: MessageRaw[], phoneNumber: string) => {
|
||||
phoneNumbersWithTimestamp.set(phoneNumber, {
|
||||
first: messages[0]?.messageTimestamp as number,
|
||||
last: messages[messages.length - 1]?.messageTimestamp as number,
|
||||
});
|
||||
});
|
||||
|
||||
// processing messages in batch
|
||||
const batchSize = 4000;
|
||||
let messagesChunk: MessageRaw[] = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||
while (messagesChunk.length > 0) {
|
||||
// Map structure: +552199999999 => MessageRaw[]
|
||||
const messagesByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesChunk);
|
||||
|
||||
if (messagesByPhoneNumber.size > 0) {
|
||||
const fksByNumber = await this.selectOrCreateFksFromChatwoot(
|
||||
provider,
|
||||
inbox,
|
||||
phoneNumbersWithTimestamp,
|
||||
messagesByPhoneNumber,
|
||||
);
|
||||
|
||||
// inserting messages in chatwoot db
|
||||
let sqlInsertMsg = `INSERT INTO messages
|
||||
(content, account_id, inbox_id, conversation_id, message_type, private, content_type,
|
||||
sender_type, sender_id, created_at, updated_at) VALUES `;
|
||||
const bindInsertMsg = [provider.account_id, inbox.id];
|
||||
|
||||
messagesByPhoneNumber.forEach((messages: MessageRaw[], phoneNumber: string) => {
|
||||
const fksChatwoot = fksByNumber.get(phoneNumber);
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (!message.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fksChatwoot?.conversation_id || !fksChatwoot?.contact_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentMessage = this.getContentMessage(chatwootService, message);
|
||||
if (!contentMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
bindInsertMsg.push(contentMessage);
|
||||
const bindContent = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(fksChatwoot.conversation_id);
|
||||
const bindConversationId = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? '1' : '0');
|
||||
const bindMessageType = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_type : 'Contact');
|
||||
const bindSenderType = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_id : fksChatwoot.contact_id);
|
||||
const bindSenderId = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.messageTimestamp as number);
|
||||
const bindmessageTimestamp = `$${bindInsertMsg.length}`;
|
||||
|
||||
sqlInsertMsg += `(${bindContent}, $1, $2, ${bindConversationId}, ${bindMessageType}, FALSE, 0,
|
||||
${bindSenderType},${bindSenderId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`;
|
||||
});
|
||||
});
|
||||
if (bindInsertMsg.length > 2) {
|
||||
if (sqlInsertMsg.slice(-1) === ',') {
|
||||
sqlInsertMsg = sqlInsertMsg.slice(0, -1);
|
||||
}
|
||||
totalMessagesImported += (await pgClient.query(sqlInsertMsg, bindInsertMsg))?.rowCount ?? 0;
|
||||
}
|
||||
}
|
||||
messagesChunk = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||
}
|
||||
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
|
||||
this.importHistoryContacts(instance, provider);
|
||||
|
||||
return totalMessagesImported;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on import history messages: ${error.toString()}`);
|
||||
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public async selectOrCreateFksFromChatwoot(
|
||||
provider: ChatwootRaw,
|
||||
inbox: inbox,
|
||||
phoneNumbersWithTimestamp: Map<string, firstLastTimestamp>,
|
||||
messagesByPhoneNumber: Map<string, MessageRaw[]>,
|
||||
): Promise<Map<string, FksChatwoot>> {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const bindValues = [provider.account_id, inbox.id];
|
||||
const phoneNumberBind = Array.from(messagesByPhoneNumber.keys())
|
||||
.map((phoneNumber) => {
|
||||
const phoneNumberTimestamp = phoneNumbersWithTimestamp.get(phoneNumber);
|
||||
|
||||
if (phoneNumberTimestamp) {
|
||||
bindValues.push(phoneNumber);
|
||||
let bindStr = `($${bindValues.length},`;
|
||||
|
||||
bindValues.push(phoneNumberTimestamp.first);
|
||||
bindStr += `$${bindValues.length},`;
|
||||
|
||||
bindValues.push(phoneNumberTimestamp.last);
|
||||
return `${bindStr}$${bindValues.length})`;
|
||||
}
|
||||
})
|
||||
.join(',');
|
||||
|
||||
// select (or insert when necessary) data from tables contacts, contact_inboxes, conversations from chatwoot db
|
||||
const sqlFromChatwoot = `WITH
|
||||
phone_number AS (
|
||||
SELECT phone_number, created_at::INTEGER, last_activity_at::INTEGER FROM (
|
||||
VALUES
|
||||
${phoneNumberBind}
|
||||
) as t (phone_number, created_at, last_activity_at)
|
||||
),
|
||||
|
||||
only_new_phone_number AS (
|
||||
SELECT * FROM phone_number
|
||||
WHERE phone_number NOT IN (
|
||||
SELECT phone_number
|
||||
FROM contacts
|
||||
JOIN contact_inboxes ci ON ci.contact_id = contacts.id AND ci.inbox_id = $2
|
||||
JOIN conversations con ON con.contact_inbox_id = ci.id
|
||||
AND con.account_id = $1
|
||||
AND con.inbox_id = $2
|
||||
AND con.contact_id = contacts.id
|
||||
WHERE contacts.account_id = $1
|
||||
)
|
||||
),
|
||||
|
||||
new_contact AS (
|
||||
INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at)
|
||||
SELECT REPLACE(p.phone_number, '+', ''), p.phone_number, $1, CONCAT(REPLACE(p.phone_number, '+', ''),
|
||||
'@s.whatsapp.net'), to_timestamp(p.created_at), to_timestamp(p.last_activity_at)
|
||||
FROM only_new_phone_number AS p
|
||||
ON CONFLICT(identifier, account_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
|
||||
RETURNING id, phone_number, created_at, updated_at
|
||||
),
|
||||
|
||||
new_contact_inbox AS (
|
||||
INSERT INTO contact_inboxes (contact_id, inbox_id, source_id, created_at, updated_at)
|
||||
SELECT new_contact.id, $2, gen_random_uuid(), new_contact.created_at, new_contact.updated_at
|
||||
FROM new_contact
|
||||
RETURNING id, contact_id, created_at, updated_at
|
||||
),
|
||||
|
||||
new_conversation AS (
|
||||
INSERT INTO conversations (account_id, inbox_id, status, contact_id,
|
||||
contact_inbox_id, uuid, last_activity_at, created_at, updated_at)
|
||||
SELECT $1, $2, 0, new_contact_inbox.contact_id, new_contact_inbox.id, gen_random_uuid(),
|
||||
new_contact_inbox.updated_at, new_contact_inbox.created_at, new_contact_inbox.updated_at
|
||||
FROM new_contact_inbox
|
||||
RETURNING id, contact_id
|
||||
)
|
||||
|
||||
SELECT new_contact.phone_number, new_conversation.contact_id, new_conversation.id AS conversation_id
|
||||
FROM new_conversation
|
||||
JOIN new_contact ON new_conversation.contact_id = new_contact.id
|
||||
|
||||
UNION
|
||||
|
||||
SELECT p.phone_number, c.id contact_id, con.id conversation_id
|
||||
FROM phone_number p
|
||||
JOIN contacts c ON c.phone_number = p.phone_number
|
||||
JOIN contact_inboxes ci ON ci.contact_id = c.id AND ci.inbox_id = $2
|
||||
JOIN conversations con ON con.contact_inbox_id = ci.id AND con.account_id = $1
|
||||
AND con.inbox_id = $2 AND con.contact_id = c.id`;
|
||||
|
||||
const fksFromChatwoot = await pgClient.query(sqlFromChatwoot, bindValues);
|
||||
|
||||
return new Map(fksFromChatwoot.rows.map((item: FksChatwoot) => [item.phone_number, item]));
|
||||
}
|
||||
|
||||
public async getChatwootUser(provider: ChatwootRaw): Promise<ChatwootUser> {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const sqlUser = `SELECT owner_type AS user_type, owner_id AS user_id
|
||||
FROM access_tokens
|
||||
WHERE token = $1`;
|
||||
|
||||
return (await pgClient.query(sqlUser, [provider.token]))?.rows[0] || false;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on getChatwootUser: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public createMessagesMapByPhoneNumber(messages: MessageRaw[]): Map<string, MessageRaw[]> {
|
||||
return messages.reduce((acc: Map<string, MessageRaw[]>, message: MessageRaw) => {
|
||||
if (!this.isIgnorePhoneNumber(message?.key?.remoteJid)) {
|
||||
const phoneNumber = message?.key?.remoteJid?.split('@')[0];
|
||||
if (phoneNumber) {
|
||||
const phoneNumberPlus = `+${phoneNumber}`;
|
||||
const messages = acc.has(phoneNumberPlus) ? acc.get(phoneNumberPlus) : [];
|
||||
messages.push(message);
|
||||
acc.set(phoneNumberPlus, messages);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
public async getContactsOrderByRecentConversations(
|
||||
inbox: inbox,
|
||||
provider: ChatwootRaw,
|
||||
limit = 50,
|
||||
): Promise<{ id: number; phone_number: string; identifier: string }[]> {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const sql = `SELECT contacts.id, contacts.identifier, contacts.phone_number
|
||||
FROM conversations
|
||||
JOIN contacts ON contacts.id = conversations.contact_id
|
||||
WHERE conversations.account_id = $1
|
||||
AND inbox_id = $2
|
||||
ORDER BY conversations.last_activity_at DESC
|
||||
LIMIT $3`;
|
||||
|
||||
return (await pgClient.query(sql, [provider.account_id, inbox.id, limit]))?.rows;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on get recent conversations: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getContentMessage(chatwootService: ChatwootService, msg: IWebMessageInfo) {
|
||||
const contentMessage = chatwootService.getConversationMessage(msg.message);
|
||||
if (contentMessage) {
|
||||
return contentMessage;
|
||||
}
|
||||
|
||||
if (!configService.get<Chatwoot>('CHATWOOT').IMPORT.PLACEHOLDER_MEDIA_MESSAGE) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const types = {
|
||||
documentMessage: msg.message.documentMessage,
|
||||
documentWithCaptionMessage: msg.message.documentWithCaptionMessage?.message?.documentMessage,
|
||||
imageMessage: msg.message.imageMessage,
|
||||
videoMessage: msg.message.videoMessage,
|
||||
audioMessage: msg.message.audioMessage,
|
||||
stickerMessage: msg.message.stickerMessage,
|
||||
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
|
||||
};
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
|
||||
|
||||
switch (typeKey) {
|
||||
case 'documentMessage':
|
||||
return `_<File: ${msg.message.documentMessage.fileName}${
|
||||
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
|
||||
}>_`;
|
||||
|
||||
case 'documentWithCaptionMessage':
|
||||
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${
|
||||
msg.message.documentWithCaptionMessage.message.documentMessage.caption
|
||||
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}`
|
||||
: ''
|
||||
}>_`;
|
||||
|
||||
case 'templateMessage':
|
||||
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText
|
||||
? `*${msg.message.templateMessage.hydratedTemplate.hydratedTitleText}*\\n`
|
||||
: '' + msg.message.templateMessage.hydratedTemplate.hydratedContentText;
|
||||
|
||||
case 'imageMessage':
|
||||
return '_<Image Message>_';
|
||||
|
||||
case 'videoMessage':
|
||||
return '_<Video Message>_';
|
||||
|
||||
case 'audioMessage':
|
||||
return '_<Audio Message>_';
|
||||
|
||||
case 'stickerMessage':
|
||||
return '_<Sticker Message>_';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public sliceIntoChunks(arr: any[], chunkSize: number) {
|
||||
return arr.splice(0, chunkSize);
|
||||
}
|
||||
|
||||
public isGroup(remoteJid: string) {
|
||||
return remoteJid.includes('@g.us');
|
||||
}
|
||||
|
||||
public isIgnorePhoneNumber(remoteJid: string) {
|
||||
return this.isGroup(remoteJid) || remoteJid === 'status@broadcast' || remoteJid === '0@s.whatsapp.net';
|
||||
}
|
||||
}
|
||||
|
||||
export const chatwootImport = new ChatwootImport();
|
||||
42
src/api/integrations/chatwoot/validate/chatwoot.schema.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
};
|
||||
};
|
||||
|
||||
export const chatwootSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', enum: [true, false] },
|
||||
account_id: { type: 'string' },
|
||||
token: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
sign_msg: { type: 'boolean', enum: [true, false] },
|
||||
sign_delimiter: { type: ['string', 'null'] },
|
||||
reopen_conversation: { type: 'boolean', enum: [true, false] },
|
||||
conversation_pending: { type: 'boolean', enum: [true, false] },
|
||||
auto_create: { type: 'boolean', enum: [true, false] },
|
||||
import_contacts: { type: 'boolean', enum: [true, false] },
|
||||
import_messages: { type: 'boolean', enum: [true, false] },
|
||||
days_limit_import_messages: { type: 'number' },
|
||||
},
|
||||
required: ['enabled', 'account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'],
|
||||
...isNotEmpty('account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'),
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { RabbitmqDto } from '../dto/rabbitmq.dto';
|
||||
import { RabbitmqService } from '../services/rabbitmq.service';
|
||||
|
||||
const logger = new Logger('RabbitmqController');
|
||||
|
||||
export class RabbitmqController {
|
||||
constructor(private readonly rabbitmqService: RabbitmqService) {}
|
||||
|
||||
public async createRabbitmq(instance: InstanceDto, data: RabbitmqDto) {
|
||||
logger.verbose('requested createRabbitmq from ' + instance.instanceName + ' instance');
|
||||
|
||||
if (!data.enabled) {
|
||||
logger.verbose('rabbitmq disabled');
|
||||
data.events = [];
|
||||
}
|
||||
|
||||
if (data.events.length === 0) {
|
||||
logger.verbose('rabbitmq events empty');
|
||||
data.events = [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
];
|
||||
}
|
||||
|
||||
return this.rabbitmqService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findRabbitmq(instance: InstanceDto) {
|
||||
logger.verbose('requested findRabbitmq from ' + instance.instanceName + ' instance');
|
||||
return this.rabbitmqService.find(instance);
|
||||
}
|
||||
}
|
||||
4
src/api/integrations/rabbitmq/dto/rabbitmq.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export class RabbitmqDto {
|
||||
enabled: boolean;
|
||||
events?: string[];
|
||||
}
|
||||
100
src/api/integrations/rabbitmq/libs/amqp.server.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import * as amqp from 'amqplib/callback_api';
|
||||
|
||||
import { configService, Rabbitmq } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
|
||||
const logger = new Logger('AMQP');
|
||||
|
||||
let amqpChannel: amqp.Channel | null = null;
|
||||
|
||||
export const initAMQP = () => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
|
||||
amqp.connect(uri, (error, connection) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.createChannel((channelError, channel) => {
|
||||
if (channelError) {
|
||||
reject(channelError);
|
||||
return;
|
||||
}
|
||||
|
||||
const exchangeName = 'evolution_exchange';
|
||||
|
||||
channel.assertExchange(exchangeName, 'topic', {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
});
|
||||
|
||||
amqpChannel = channel;
|
||||
|
||||
logger.info('AMQP initialized');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getAMQP = (): amqp.Channel | null => {
|
||||
return amqpChannel;
|
||||
};
|
||||
|
||||
export const initQueues = (instanceName: string, events: string[]) => {
|
||||
if (!events || !events.length) return;
|
||||
|
||||
const queues = events.map((event) => {
|
||||
return `${event.replace(/_/g, '.').toLowerCase()}`;
|
||||
});
|
||||
|
||||
queues.forEach((event) => {
|
||||
const amqp = getAMQP();
|
||||
const exchangeName = instanceName ?? 'evolution_exchange';
|
||||
|
||||
amqp.assertExchange(exchangeName, 'topic', {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
});
|
||||
|
||||
const queueName = `${instanceName}.${event}`;
|
||||
|
||||
amqp.assertQueue(queueName, {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
arguments: {
|
||||
'x-queue-type': 'quorum',
|
||||
},
|
||||
});
|
||||
|
||||
amqp.bindQueue(queueName, exchangeName, event);
|
||||
});
|
||||
};
|
||||
|
||||
export const removeQueues = (instanceName: string, events: string[]) => {
|
||||
if (!events || !events.length) return;
|
||||
|
||||
const channel = getAMQP();
|
||||
|
||||
const queues = events.map((event) => {
|
||||
return `${event.replace(/_/g, '.').toLowerCase()}`;
|
||||
});
|
||||
|
||||
const exchangeName = instanceName ?? 'evolution_exchange';
|
||||
|
||||
queues.forEach((event) => {
|
||||
const amqp = getAMQP();
|
||||
|
||||
amqp.assertExchange(exchangeName, 'topic', {
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
});
|
||||
|
||||
const queueName = `${instanceName}.${event}`;
|
||||
|
||||
amqp.deleteQueue(queueName);
|
||||
});
|
||||
|
||||
channel.deleteExchange(exchangeName);
|
||||
};
|
||||
18
src/api/integrations/rabbitmq/models/rabbitmq.model.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Schema } from 'mongoose';
|
||||
|
||||
import { dbserver } from '../../../../libs/db.connect';
|
||||
|
||||
export class RabbitmqRaw {
|
||||
_id?: string;
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
}
|
||||
|
||||
const rabbitmqSchema = new Schema<RabbitmqRaw>({
|
||||
_id: { type: String, _id: true },
|
||||
enabled: { type: Boolean, required: true },
|
||||
events: { type: [String], required: true },
|
||||
});
|
||||
|
||||
export const RabbitmqModel = dbserver?.model(RabbitmqRaw.name, rabbitmqSchema, 'rabbitmq');
|
||||
export type IRabbitmqModel = typeof RabbitmqModel;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { ConfigService } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { IInsert, Repository } from '../../../abstract/abstract.repository';
|
||||
import { IRabbitmqModel, RabbitmqRaw } from '../../../models';
|
||||
|
||||
export class RabbitmqRepository extends Repository {
|
||||
constructor(private readonly rabbitmqModel: IRabbitmqModel, private readonly configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
private readonly logger = new Logger('RabbitmqRepository');
|
||||
|
||||
public async create(data: RabbitmqRaw, instance: string): Promise<IInsert> {
|
||||
try {
|
||||
this.logger.verbose('creating rabbitmq');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('saving rabbitmq to db');
|
||||
const insert = await this.rabbitmqModel.replaceOne({ _id: instance }, { ...data }, { upsert: true });
|
||||
|
||||
this.logger.verbose('rabbitmq saved to db: ' + insert.modifiedCount + ' rabbitmq');
|
||||
return { insertCount: insert.modifiedCount };
|
||||
}
|
||||
|
||||
this.logger.verbose('saving rabbitmq to store');
|
||||
|
||||
this.writeStore<RabbitmqRaw>({
|
||||
path: join(this.storePath, 'rabbitmq'),
|
||||
fileName: instance,
|
||||
data,
|
||||
});
|
||||
|
||||
this.logger.verbose('rabbitmq saved to store in path: ' + join(this.storePath, 'rabbitmq') + '/' + instance);
|
||||
|
||||
this.logger.verbose('rabbitmq created');
|
||||
return { insertCount: 1 };
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
public async find(instance: string): Promise<RabbitmqRaw> {
|
||||
try {
|
||||
this.logger.verbose('finding rabbitmq');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('finding rabbitmq in db');
|
||||
return await this.rabbitmqModel.findOne({ _id: instance });
|
||||
}
|
||||
|
||||
this.logger.verbose('finding rabbitmq in store');
|
||||
return JSON.parse(
|
||||
readFileSync(join(this.storePath, 'rabbitmq', instance + '.json'), {
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
) as RabbitmqRaw;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/api/integrations/rabbitmq/routes/rabbitmq.router.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { instanceNameSchema, rabbitmqSchema } from '../../../../validate/validate.schema';
|
||||
import { RouterBroker } from '../../../abstract/abstract.router';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { HttpStatus } from '../../../routes/index.router';
|
||||
import { rabbitmqController } from '../../../server.module';
|
||||
import { RabbitmqDto } from '../dto/rabbitmq.dto';
|
||||
|
||||
const logger = new Logger('RabbitmqRouter');
|
||||
|
||||
export class RabbitmqRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in setRabbitmq');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<RabbitmqDto>({
|
||||
request: req,
|
||||
schema: rabbitmqSchema,
|
||||
ClassRef: RabbitmqDto,
|
||||
execute: (instance, data) => rabbitmqController.createRabbitmq(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in findRabbitmq');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceNameSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => rabbitmqController.findRabbitmq(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router = Router();
|
||||
}
|
||||
35
src/api/integrations/rabbitmq/services/rabbitmq.service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { RabbitmqRaw } from '../../../models';
|
||||
import { WAMonitoringService } from '../../../services/monitor.service';
|
||||
import { RabbitmqDto } from '../dto/rabbitmq.dto';
|
||||
import { initQueues } from '../libs/amqp.server';
|
||||
|
||||
export class RabbitmqService {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
private readonly logger = new Logger(RabbitmqService.name);
|
||||
|
||||
public create(instance: InstanceDto, data: RabbitmqDto) {
|
||||
this.logger.verbose('create rabbitmq: ' + instance.instanceName);
|
||||
this.waMonitor.waInstances[instance.instanceName].setRabbitmq(data);
|
||||
|
||||
initQueues(instance.instanceName, data.events);
|
||||
return { rabbitmq: { ...instance, rabbitmq: data } };
|
||||
}
|
||||
|
||||
public async find(instance: InstanceDto): Promise<RabbitmqRaw> {
|
||||
try {
|
||||
this.logger.verbose('find rabbitmq: ' + instance.instanceName);
|
||||
const result = await this.waMonitor.waInstances[instance.instanceName].findRabbitmq();
|
||||
|
||||
if (Object.keys(result).length === 0) {
|
||||
throw new Error('Rabbitmq not found');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { enabled: false, events: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/api/integrations/rabbitmq/validate/rabbitmq.schema.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
};
|
||||
};
|
||||
|
||||
export const rabbitmqSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', enum: [true, false] },
|
||||
events: {
|
||||
type: 'array',
|
||||
minItems: 0,
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['enabled'],
|
||||
...isNotEmpty('enabled'),
|
||||
};
|
||||
58
src/api/integrations/sqs/controllers/sqs.controller.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { SqsDto } from '../dto/sqs.dto';
|
||||
import { SqsService } from '../services/sqs.service';
|
||||
|
||||
const logger = new Logger('SqsController');
|
||||
|
||||
export class SqsController {
|
||||
constructor(private readonly sqsService: SqsService) {}
|
||||
|
||||
public async createSqs(instance: InstanceDto, data: SqsDto) {
|
||||
logger.verbose('requested createSqs from ' + instance.instanceName + ' instance');
|
||||
|
||||
if (!data.enabled) {
|
||||
logger.verbose('sqs disabled');
|
||||
data.events = [];
|
||||
}
|
||||
|
||||
if (data.events.length === 0) {
|
||||
logger.verbose('sqs events empty');
|
||||
data.events = [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
];
|
||||
}
|
||||
|
||||
return this.sqsService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findSqs(instance: InstanceDto) {
|
||||
logger.verbose('requested findSqs from ' + instance.instanceName + ' instance');
|
||||
return this.sqsService.find(instance);
|
||||
}
|
||||
}
|
||||
4
src/api/integrations/sqs/dto/sqs.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export class SqsDto {
|
||||
enabled: boolean;
|
||||
events?: string[];
|
||||
}
|
||||
97
src/api/integrations/sqs/libs/sqs.server.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { SQS } from 'aws-sdk';
|
||||
|
||||
import { configService, Sqs } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
|
||||
const logger = new Logger('SQS');
|
||||
|
||||
let sqs: SQS;
|
||||
|
||||
export const initSQS = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const awsConfig = configService.get<Sqs>('SQS');
|
||||
sqs = new SQS({
|
||||
accessKeyId: awsConfig.ACCESS_KEY_ID,
|
||||
secretAccessKey: awsConfig.SECRET_ACCESS_KEY,
|
||||
region: awsConfig.REGION,
|
||||
});
|
||||
|
||||
logger.info('SQS initialized');
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
export const getSQS = (): SQS => {
|
||||
return sqs;
|
||||
};
|
||||
|
||||
export const initQueues = (instanceName: string, events: string[]) => {
|
||||
if (!events || !events.length) return;
|
||||
|
||||
const queues = events.map((event) => {
|
||||
return `${event.replace(/_/g, '_').toLowerCase()}`;
|
||||
});
|
||||
|
||||
const sqs = getSQS();
|
||||
|
||||
queues.forEach((event) => {
|
||||
const queueName = `${instanceName}_${event}.fifo`;
|
||||
|
||||
sqs.createQueue(
|
||||
{
|
||||
QueueName: queueName,
|
||||
Attributes: {
|
||||
FifoQueue: 'true',
|
||||
},
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
logger.error(`Error creating queue ${queueName}: ${err.message}`);
|
||||
} else {
|
||||
logger.info(`Queue ${queueName} created: ${data.QueueUrl}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const removeQueues = (instanceName: string, events: string[]) => {
|
||||
if (!events || !events.length) return;
|
||||
|
||||
const sqs = getSQS();
|
||||
|
||||
const queues = events.map((event) => {
|
||||
return `${event.replace(/_/g, '_').toLowerCase()}`;
|
||||
});
|
||||
|
||||
queues.forEach((event) => {
|
||||
const queueName = `${instanceName}_${event}.fifo`;
|
||||
|
||||
sqs.getQueueUrl(
|
||||
{
|
||||
QueueName: queueName,
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
logger.error(`Error getting queue URL for ${queueName}: ${err.message}`);
|
||||
} else {
|
||||
const queueUrl = data.QueueUrl;
|
||||
|
||||
sqs.deleteQueue(
|
||||
{
|
||||
QueueUrl: queueUrl,
|
||||
},
|
||||
(deleteErr) => {
|
||||
if (deleteErr) {
|
||||
logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`);
|
||||
} else {
|
||||
logger.info(`Queue ${queueName} deleted`);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
18
src/api/integrations/sqs/models/sqs.model.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Schema } from 'mongoose';
|
||||
|
||||
import { dbserver } from '../../../../libs/db.connect';
|
||||
|
||||
export class SqsRaw {
|
||||
_id?: string;
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
}
|
||||
|
||||
const sqsSchema = new Schema<SqsRaw>({
|
||||
_id: { type: String, _id: true },
|
||||
enabled: { type: Boolean, required: true },
|
||||
events: { type: [String], required: true },
|
||||
});
|
||||
|
||||
export const SqsModel = dbserver?.model(SqsRaw.name, sqsSchema, 'sqs');
|
||||
export type ISqsModel = typeof SqsModel;
|
||||
62
src/api/integrations/sqs/repository/sqs.repository.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { ConfigService } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { IInsert, Repository } from '../../../abstract/abstract.repository';
|
||||
import { ISqsModel, SqsRaw } from '../../../models';
|
||||
|
||||
export class SqsRepository extends Repository {
|
||||
constructor(private readonly sqsModel: ISqsModel, private readonly configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
private readonly logger = new Logger('SqsRepository');
|
||||
|
||||
public async create(data: SqsRaw, instance: string): Promise<IInsert> {
|
||||
try {
|
||||
this.logger.verbose('creating sqs');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('saving sqs to db');
|
||||
const insert = await this.sqsModel.replaceOne({ _id: instance }, { ...data }, { upsert: true });
|
||||
|
||||
this.logger.verbose('sqs saved to db: ' + insert.modifiedCount + ' sqs');
|
||||
return { insertCount: insert.modifiedCount };
|
||||
}
|
||||
|
||||
this.logger.verbose('saving sqs to store');
|
||||
|
||||
this.writeStore<SqsRaw>({
|
||||
path: join(this.storePath, 'sqs'),
|
||||
fileName: instance,
|
||||
data,
|
||||
});
|
||||
|
||||
this.logger.verbose('sqs saved to store in path: ' + join(this.storePath, 'sqs') + '/' + instance);
|
||||
|
||||
this.logger.verbose('sqs created');
|
||||
return { insertCount: 1 };
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
public async find(instance: string): Promise<SqsRaw> {
|
||||
try {
|
||||
this.logger.verbose('finding sqs');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('finding sqs in db');
|
||||
return await this.sqsModel.findOne({ _id: instance });
|
||||
}
|
||||
|
||||
this.logger.verbose('finding sqs in store');
|
||||
return JSON.parse(
|
||||
readFileSync(join(this.storePath, 'sqs', instance + '.json'), {
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
) as SqsRaw;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/api/integrations/sqs/routes/sqs.router.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { instanceNameSchema, sqsSchema } from '../../../../validate/validate.schema';
|
||||
import { RouterBroker } from '../../../abstract/abstract.router';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { HttpStatus } from '../../../routes/index.router';
|
||||
import { sqsController } from '../../../server.module';
|
||||
import { SqsDto } from '../dto/sqs.dto';
|
||||
|
||||
const logger = new Logger('SqsRouter');
|
||||
|
||||
export class SqsRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in setSqs');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<SqsDto>({
|
||||
request: req,
|
||||
schema: sqsSchema,
|
||||
ClassRef: SqsDto,
|
||||
execute: (instance, data) => sqsController.createSqs(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in findSqs');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceNameSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => sqsController.findSqs(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router = Router();
|
||||
}
|
||||
35
src/api/integrations/sqs/services/sqs.service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { SqsRaw } from '../../../models';
|
||||
import { WAMonitoringService } from '../../../services/monitor.service';
|
||||
import { SqsDto } from '../dto/sqs.dto';
|
||||
import { initQueues } from '../libs/sqs.server';
|
||||
|
||||
export class SqsService {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
private readonly logger = new Logger(SqsService.name);
|
||||
|
||||
public create(instance: InstanceDto, data: SqsDto) {
|
||||
this.logger.verbose('create sqs: ' + instance.instanceName);
|
||||
this.waMonitor.waInstances[instance.instanceName].setSqs(data);
|
||||
|
||||
initQueues(instance.instanceName, data.events);
|
||||
return { sqs: { ...instance, sqs: data } };
|
||||
}
|
||||
|
||||
public async find(instance: InstanceDto): Promise<SqsRaw> {
|
||||
try {
|
||||
this.logger.verbose('find sqs: ' + instance.instanceName);
|
||||
const result = await this.waMonitor.waInstances[instance.instanceName].findSqs();
|
||||
|
||||
if (Object.keys(result).length === 0) {
|
||||
throw new Error('Sqs not found');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { enabled: false, events: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/api/integrations/sqs/validate/sqs.schema.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
|
||||
const properties = {};
|
||||
propertyNames.forEach(
|
||||
(property) =>
|
||||
(properties[property] = {
|
||||
minLength: 1,
|
||||
description: `The "${property}" cannot be empty`,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
if: {
|
||||
propertyNames: {
|
||||
enum: [...propertyNames],
|
||||
},
|
||||
},
|
||||
then: { properties },
|
||||
};
|
||||
};
|
||||
|
||||
export const sqsSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', enum: [true, false] },
|
||||
events: {
|
||||
type: 'array',
|
||||
minItems: 0,
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'APPLICATION_STARTUP',
|
||||
'QRCODE_UPDATED',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUP_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CONNECTION_UPDATE',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CALL',
|
||||
'NEW_JWT_TOKEN',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'CHAMA_AI_ACTION',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['enabled'],
|
||||
...isNotEmpty('enabled'),
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { TypebotDto } from '../dto/typebot.dto';
|
||||
import { TypebotService } from '../services/typebot.service';
|
||||
|
||||
const logger = new Logger('TypebotController');
|
||||
|
||||
export class TypebotController {
|
||||
constructor(private readonly typebotService: TypebotService) {}
|
||||
|
||||
public async createTypebot(instance: InstanceDto, data: TypebotDto) {
|
||||
logger.verbose('requested createTypebot from ' + instance.instanceName + ' instance');
|
||||
|
||||
if (!data.enabled) {
|
||||
logger.verbose('typebot disabled');
|
||||
data.url = '';
|
||||
data.typebot = '';
|
||||
data.expire = 0;
|
||||
data.sessions = [];
|
||||
} else {
|
||||
const saveData = await this.typebotService.find(instance);
|
||||
|
||||
if (saveData.enabled) {
|
||||
logger.verbose('typebot enabled');
|
||||
data.sessions = saveData.sessions;
|
||||
}
|
||||
}
|
||||
|
||||
return this.typebotService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findTypebot(instance: InstanceDto) {
|
||||
logger.verbose('requested findTypebot from ' + instance.instanceName + ' instance');
|
||||
return this.typebotService.find(instance);
|
||||
}
|
||||
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
logger.verbose('requested changeStatus from ' + instance.instanceName + ' instance');
|
||||
return this.typebotService.changeStatus(instance, data);
|
||||
}
|
||||
|
||||
public async startTypebot(instance: InstanceDto, data: any) {
|
||||
logger.verbose('requested startTypebot from ' + instance.instanceName + ' instance');
|
||||
return this.typebotService.startTypebot(instance, data);
|
||||
}
|
||||
}
|
||||
27
src/api/integrations/typebot/dto/typebot.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export class Session {
|
||||
remoteJid?: string;
|
||||
sessionId?: string;
|
||||
status?: string;
|
||||
createdAt?: number;
|
||||
updateAt?: number;
|
||||
prefilledVariables?: PrefilledVariables;
|
||||
}
|
||||
|
||||
export class PrefilledVariables {
|
||||
remoteJid?: string;
|
||||
pushName?: string;
|
||||
messageType?: string;
|
||||
additionalData?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export class TypebotDto {
|
||||
enabled?: boolean;
|
||||
url: string;
|
||||
typebot?: string;
|
||||
expire?: number;
|
||||
keyword_finish?: string;
|
||||
delay_message?: number;
|
||||
unknown_message?: string;
|
||||
listening_from_me?: boolean;
|
||||
sessions?: Session[];
|
||||
}
|
||||
58
src/api/integrations/typebot/models/typebot.model.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Schema } from 'mongoose';
|
||||
|
||||
import { dbserver } from '../../../../libs/db.connect';
|
||||
|
||||
class Session {
|
||||
remoteJid?: string;
|
||||
sessionId?: string;
|
||||
status?: string;
|
||||
createdAt?: number;
|
||||
updateAt?: number;
|
||||
prefilledVariables?: {
|
||||
remoteJid?: string;
|
||||
pushName?: string;
|
||||
additionalData?: { [key: string]: any };
|
||||
};
|
||||
}
|
||||
|
||||
export class TypebotRaw {
|
||||
_id?: string;
|
||||
enabled?: boolean;
|
||||
url: string;
|
||||
typebot?: string;
|
||||
expire?: number;
|
||||
keyword_finish?: string;
|
||||
delay_message?: number;
|
||||
unknown_message?: string;
|
||||
listening_from_me?: boolean;
|
||||
sessions?: Session[];
|
||||
}
|
||||
|
||||
const typebotSchema = new Schema<TypebotRaw>({
|
||||
_id: { type: String, _id: true },
|
||||
enabled: { type: Boolean, required: true },
|
||||
url: { type: String, required: true },
|
||||
typebot: { type: String, required: true },
|
||||
expire: { type: Number, required: true },
|
||||
keyword_finish: { type: String, required: true },
|
||||
delay_message: { type: Number, required: true },
|
||||
unknown_message: { type: String, required: true },
|
||||
listening_from_me: { type: Boolean, required: true },
|
||||
sessions: [
|
||||
{
|
||||
remoteJid: { type: String, required: true },
|
||||
sessionId: { type: String, required: true },
|
||||
status: { type: String, required: true },
|
||||
createdAt: { type: Number, required: true },
|
||||
updateAt: { type: Number, required: true },
|
||||
prefilledVariables: {
|
||||
remoteJid: { type: String, required: false },
|
||||
pushName: { type: String, required: false },
|
||||
additionalData: { type: Schema.Types.Mixed, required: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const TypebotModel = dbserver?.model(TypebotRaw.name, typebotSchema, 'typebot');
|
||||
export type ITypebotModel = typeof TypebotModel;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { ConfigService } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { IInsert, Repository } from '../../../abstract/abstract.repository';
|
||||
import { ITypebotModel, TypebotRaw } from '../../../models';
|
||||
|
||||
export class TypebotRepository extends Repository {
|
||||
constructor(private readonly typebotModel: ITypebotModel, private readonly configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
private readonly logger = new Logger('TypebotRepository');
|
||||
|
||||
public async create(data: TypebotRaw, instance: string): Promise<IInsert> {
|
||||
try {
|
||||
this.logger.verbose('creating typebot');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('saving typebot to db');
|
||||
const insert = await this.typebotModel.replaceOne({ _id: instance }, { ...data }, { upsert: true });
|
||||
|
||||
this.logger.verbose('typebot saved to db: ' + insert.modifiedCount + ' typebot');
|
||||
return { insertCount: insert.modifiedCount };
|
||||
}
|
||||
|
||||
this.logger.verbose('saving typebot to store');
|
||||
|
||||
this.writeStore<TypebotRaw>({
|
||||
path: join(this.storePath, 'typebot'),
|
||||
fileName: instance,
|
||||
data,
|
||||
});
|
||||
|
||||
this.logger.verbose('typebot saved to store in path: ' + join(this.storePath, 'typebot') + '/' + instance);
|
||||
|
||||
this.logger.verbose('typebot created');
|
||||
return { insertCount: 1 };
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
public async find(instance: string): Promise<TypebotRaw> {
|
||||
try {
|
||||
this.logger.verbose('finding typebot');
|
||||
if (this.dbSettings.ENABLED) {
|
||||
this.logger.verbose('finding typebot in db');
|
||||
return await this.typebotModel.findOne({ _id: instance });
|
||||
}
|
||||
|
||||
this.logger.verbose('finding typebot in store');
|
||||
return JSON.parse(
|
||||
readFileSync(join(this.storePath, 'typebot', instance + '.json'), {
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
) as TypebotRaw;
|
||||
} catch (error) {
|
||||
return {
|
||||
enabled: false,
|
||||
url: '',
|
||||
typebot: '',
|
||||
expire: 0,
|
||||
sessions: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/api/integrations/typebot/routes/typebot.router.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import {
|
||||
instanceNameSchema,
|
||||
typebotSchema,
|
||||
typebotStartSchema,
|
||||
typebotStatusSchema,
|
||||
} from '../../../../validate/validate.schema';
|
||||
import { RouterBroker } from '../../../abstract/abstract.router';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { HttpStatus } from '../../../routes/index.router';
|
||||
import { typebotController } from '../../../server.module';
|
||||
import { TypebotDto } from '../dto/typebot.dto';
|
||||
|
||||
const logger = new Logger('TypebotRouter');
|
||||
|
||||
export class TypebotRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in setTypebot');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<TypebotDto>({
|
||||
request: req,
|
||||
schema: typebotSchema,
|
||||
ClassRef: TypebotDto,
|
||||
execute: (instance, data) => typebotController.createTypebot(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in findTypebot');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceNameSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => typebotController.findTypebot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in changeStatusTypebot');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: typebotStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => typebotController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('start'), ...guards, async (req, res) => {
|
||||
logger.verbose('request received in startTypebot');
|
||||
logger.verbose('request body: ');
|
||||
logger.verbose(req.body);
|
||||
|
||||
logger.verbose('request query: ');
|
||||
logger.verbose(req.query);
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: typebotStartSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => typebotController.startTypebot(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router = Router();
|
||||
}
|
||||
966
src/api/integrations/typebot/services/typebot.service.ts
Normal file
@@ -0,0 +1,966 @@
|
||||
import axios from 'axios';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
|
||||
import { ConfigService, Typebot } from '../../../../config/env.config';
|
||||
import { Logger } from '../../../../config/logger.config';
|
||||
import { InstanceDto } from '../../../dto/instance.dto';
|
||||
import { MessageRaw } from '../../../models';
|
||||
import { WAMonitoringService } from '../../../services/monitor.service';
|
||||
import { Events } from '../../../types/wa.types';
|
||||
import { Session, TypebotDto } from '../dto/typebot.dto';
|
||||
|
||||
export class TypebotService {
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {
|
||||
this.eventEmitter.on('typebot:end', async (data) => {
|
||||
const keep_open = this.configService.get<Typebot>('TYPEBOT').KEEP_OPEN;
|
||||
if (keep_open) return;
|
||||
|
||||
await this.clearSessions(data.instance, data.remoteJid);
|
||||
});
|
||||
}
|
||||
|
||||
private readonly logger = new Logger(TypebotService.name);
|
||||
|
||||
public create(instance: InstanceDto, data: TypebotDto) {
|
||||
this.logger.verbose('create typebot: ' + instance.instanceName);
|
||||
this.waMonitor.waInstances[instance.instanceName].setTypebot(data);
|
||||
|
||||
return { typebot: { ...instance, typebot: data } };
|
||||
}
|
||||
|
||||
public async find(instance: InstanceDto): Promise<TypebotDto> {
|
||||
try {
|
||||
this.logger.verbose('find typebot: ' + instance.instanceName);
|
||||
const result = await this.waMonitor.waInstances[instance.instanceName].findTypebot();
|
||||
|
||||
if (Object.keys(result).length === 0) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { enabled: false, url: '', typebot: '', expire: 0, sessions: [] };
|
||||
}
|
||||
}
|
||||
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
const findData = await this.find(instance);
|
||||
|
||||
const session = findData.sessions.find((session) => session.remoteJid === remoteJid);
|
||||
|
||||
if (session) {
|
||||
if (status === 'closed') {
|
||||
findData.sessions.splice(findData.sessions.indexOf(session), 1);
|
||||
|
||||
const typebotData = {
|
||||
enabled: findData.enabled,
|
||||
url: findData.url,
|
||||
typebot: findData.typebot,
|
||||
expire: findData.expire,
|
||||
keyword_finish: findData.keyword_finish,
|
||||
delay_message: findData.delay_message,
|
||||
unknown_message: findData.unknown_message,
|
||||
listening_from_me: findData.listening_from_me,
|
||||
sessions: findData.sessions,
|
||||
};
|
||||
|
||||
this.create(instance, typebotData);
|
||||
|
||||
return { typebot: { ...instance, typebot: typebotData } };
|
||||
}
|
||||
|
||||
findData.sessions.map((session) => {
|
||||
if (session.remoteJid === remoteJid) {
|
||||
session.status = status;
|
||||
}
|
||||
});
|
||||
} else if (status === 'paused') {
|
||||
const session: Session = {
|
||||
remoteJid: remoteJid,
|
||||
sessionId: Math.floor(Math.random() * 10000000000).toString(),
|
||||
status: status,
|
||||
createdAt: Date.now(),
|
||||
updateAt: Date.now(),
|
||||
prefilledVariables: {
|
||||
remoteJid: remoteJid,
|
||||
pushName: '',
|
||||
additionalData: {},
|
||||
},
|
||||
};
|
||||
findData.sessions.push(session);
|
||||
}
|
||||
|
||||
const typebotData = {
|
||||
enabled: findData.enabled,
|
||||
url: findData.url,
|
||||
typebot: findData.typebot,
|
||||
expire: findData.expire,
|
||||
keyword_finish: findData.keyword_finish,
|
||||
delay_message: findData.delay_message,
|
||||
unknown_message: findData.unknown_message,
|
||||
listening_from_me: findData.listening_from_me,
|
||||
sessions: findData.sessions,
|
||||
};
|
||||
|
||||
this.create(instance, typebotData);
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
url: findData.url,
|
||||
typebot: findData.typebot,
|
||||
session,
|
||||
});
|
||||
|
||||
return { typebot: { ...instance, typebot: typebotData } };
|
||||
}
|
||||
|
||||
public async clearSessions(instance: InstanceDto, remoteJid: string) {
|
||||
const findTypebot = await this.find(instance);
|
||||
const sessions = (findTypebot.sessions as Session[]) ?? [];
|
||||
|
||||
const sessionWithRemoteJid = sessions.filter((session) => session.remoteJid === remoteJid);
|
||||
|
||||
if (sessionWithRemoteJid.length > 0) {
|
||||
sessionWithRemoteJid.forEach((session) => {
|
||||
sessions.splice(sessions.indexOf(session), 1);
|
||||
});
|
||||
|
||||
const typebotData = {
|
||||
enabled: findTypebot.enabled,
|
||||
url: findTypebot.url,
|
||||
typebot: findTypebot.typebot,
|
||||
expire: findTypebot.expire,
|
||||
keyword_finish: findTypebot.keyword_finish,
|
||||
delay_message: findTypebot.delay_message,
|
||||
unknown_message: findTypebot.unknown_message,
|
||||
listening_from_me: findTypebot.listening_from_me,
|
||||
sessions,
|
||||
};
|
||||
|
||||
this.create(instance, typebotData);
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
public async startTypebot(instance: InstanceDto, data: any) {
|
||||
if (data.remoteJid === 'status@broadcast') return;
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const url = data.url;
|
||||
const typebot = data.typebot;
|
||||
const startSession = data.startSession;
|
||||
const variables = data.variables;
|
||||
const findTypebot = await this.find(instance);
|
||||
const expire = findTypebot.expire;
|
||||
const keyword_finish = findTypebot.keyword_finish;
|
||||
const delay_message = findTypebot.delay_message;
|
||||
const unknown_message = findTypebot.unknown_message;
|
||||
const listening_from_me = findTypebot.listening_from_me;
|
||||
|
||||
const prefilledVariables = {
|
||||
remoteJid: remoteJid,
|
||||
instanceName: instance.instanceName,
|
||||
};
|
||||
|
||||
if (variables?.length) {
|
||||
variables.forEach((variable: { name: string | number; value: string }) => {
|
||||
prefilledVariables[variable.name] = variable.value;
|
||||
});
|
||||
}
|
||||
|
||||
if (startSession) {
|
||||
const newSessions = await this.clearSessions(instance, remoteJid);
|
||||
|
||||
const response = await this.createNewSession(instance, {
|
||||
enabled: findTypebot.enabled,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
remoteJid: remoteJid,
|
||||
expire: expire,
|
||||
keyword_finish: keyword_finish,
|
||||
delay_message: delay_message,
|
||||
unknown_message: unknown_message,
|
||||
listening_from_me: listening_from_me,
|
||||
sessions: newSessions,
|
||||
prefilledVariables: prefilledVariables,
|
||||
});
|
||||
|
||||
if (response.sessionId) {
|
||||
await this.sendWAMessage(instance, remoteJid, response.messages, response.input, response.clientSideActions);
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
remoteJid: remoteJid,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
prefilledVariables: prefilledVariables,
|
||||
sessionId: `${response.sessionId}`,
|
||||
});
|
||||
} else {
|
||||
throw new Error('Session ID not found in response');
|
||||
}
|
||||
} else {
|
||||
const id = Math.floor(Math.random() * 10000000000).toString();
|
||||
|
||||
try {
|
||||
const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION;
|
||||
let url: string;
|
||||
let reqData: {};
|
||||
if (version === 'latest') {
|
||||
url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`;
|
||||
|
||||
reqData = {
|
||||
prefilledVariables: prefilledVariables,
|
||||
};
|
||||
} else {
|
||||
url = `${data.url}/api/v1/sendMessage`;
|
||||
|
||||
reqData = {
|
||||
startParams: {
|
||||
publicId: data.typebot,
|
||||
prefilledVariables: prefilledVariables,
|
||||
},
|
||||
};
|
||||
}
|
||||
const request = await axios.post(url, reqData);
|
||||
|
||||
await this.sendWAMessage(
|
||||
instance,
|
||||
remoteJid,
|
||||
request.data.messages,
|
||||
request.data.input,
|
||||
request.data.clientSideActions,
|
||||
);
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
|
||||
remoteJid: remoteJid,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
variables: variables,
|
||||
sessionId: id,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
typebot: {
|
||||
...instance,
|
||||
typebot: {
|
||||
url: url,
|
||||
remoteJid: remoteJid,
|
||||
typebot: typebot,
|
||||
prefilledVariables: prefilledVariables,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getTypeMessage(msg: any) {
|
||||
this.logger.verbose('get type message');
|
||||
const types = {
|
||||
conversation: msg.conversation,
|
||||
extendedTextMessage: msg.extendedTextMessage?.text,
|
||||
audioMessage: msg.audioMessage?.url,
|
||||
imageMessage: msg.imageMessage?.url,
|
||||
videoMessage: msg.videoMessage?.url,
|
||||
documentMessage: msg.documentMessage?.fileName,
|
||||
contactMessage: msg.contactMessage?.displayName,
|
||||
locationMessage: msg.locationMessage?.degreesLatitude,
|
||||
viewOnceMessageV2:
|
||||
msg.viewOnceMessageV2?.message?.imageMessage?.url ||
|
||||
msg.viewOnceMessageV2?.message?.videoMessage?.url ||
|
||||
msg.viewOnceMessageV2?.message?.audioMessage?.url,
|
||||
listResponseMessage: msg.listResponseMessage?.singleSelectReply?.selectedRowId,
|
||||
responseRowId: msg.listResponseMessage?.singleSelectReply?.selectedRowId,
|
||||
};
|
||||
|
||||
const messageType = Object.keys(types).find((key) => types[key] !== undefined) || 'unknown';
|
||||
|
||||
this.logger.verbose('Type message: ' + JSON.stringify(types));
|
||||
return { ...types, messageType };
|
||||
}
|
||||
|
||||
private getMessageContent(types: any) {
|
||||
this.logger.verbose('get message content');
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
|
||||
|
||||
const result = typeKey ? types[typeKey] : undefined;
|
||||
|
||||
this.logger.verbose('message content: ' + result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getConversationMessage(msg: any) {
|
||||
this.logger.verbose('get conversation message');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const messageContent = this.getMessageContent(types);
|
||||
|
||||
this.logger.verbose('conversation message: ' + messageContent);
|
||||
|
||||
return messageContent;
|
||||
}
|
||||
|
||||
private getAudioMessageContent(msg: any) {
|
||||
this.logger.verbose('get audio message content');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const audioContent = types.audioMessage;
|
||||
|
||||
this.logger.verbose('audio message URL: ' + audioContent);
|
||||
|
||||
return audioContent;
|
||||
}
|
||||
|
||||
private getImageMessageContent(msg: any) {
|
||||
this.logger.verbose('get image message content');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const imageContent = types.imageMessage;
|
||||
|
||||
this.logger.verbose('image message URL: ' + imageContent);
|
||||
|
||||
return imageContent;
|
||||
}
|
||||
|
||||
private getVideoMessageContent(msg: any) {
|
||||
this.logger.verbose('get video message content');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const videoContent = types.videoMessage;
|
||||
|
||||
this.logger.verbose('video message URL: ' + videoContent);
|
||||
|
||||
return videoContent;
|
||||
}
|
||||
|
||||
private getDocumentMessageContent(msg: any) {
|
||||
this.logger.verbose('get document message content');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const documentContent = types.documentMessage;
|
||||
|
||||
this.logger.verbose('document message fileName: ' + documentContent);
|
||||
|
||||
return documentContent;
|
||||
}
|
||||
|
||||
private getContactMessageContent(msg: any) {
|
||||
this.logger.verbose('get contact message content');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const contactContent = types.contactMessage;
|
||||
|
||||
this.logger.verbose('contact message displayName: ' + contactContent);
|
||||
|
||||
return contactContent;
|
||||
}
|
||||
|
||||
private getLocationMessageContent(msg: any) {
|
||||
this.logger.verbose('get location message content');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const locationContent = types.locationMessage;
|
||||
|
||||
this.logger.verbose('location message degreesLatitude: ' + locationContent);
|
||||
|
||||
return locationContent;
|
||||
}
|
||||
|
||||
private getViewOnceMessageV2Content(msg: any) {
|
||||
this.logger.verbose('get viewOnceMessageV2 content');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const viewOnceContent = types.viewOnceMessageV2;
|
||||
|
||||
this.logger.verbose('viewOnceMessageV2 URL: ' + viewOnceContent);
|
||||
|
||||
return viewOnceContent;
|
||||
}
|
||||
|
||||
private getListResponseMessageContent(msg: any) {
|
||||
this.logger.verbose('get listResponseMessage content');
|
||||
|
||||
const types = this.getTypeMessage(msg);
|
||||
|
||||
const listResponseContent = types.listResponseMessage || types.responseRowId;
|
||||
|
||||
this.logger.verbose('listResponseMessage selectedRowId: ' + listResponseContent);
|
||||
|
||||
return listResponseContent;
|
||||
}
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
if (data.remoteJid === 'status@broadcast') return;
|
||||
const id = Math.floor(Math.random() * 10000000000).toString();
|
||||
|
||||
try {
|
||||
const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION;
|
||||
let url: string;
|
||||
let reqData: {};
|
||||
if (version === 'latest') {
|
||||
url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`;
|
||||
|
||||
reqData = {
|
||||
prefilledVariables: {
|
||||
...data.prefilledVariables,
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName || data.prefilledVariables?.pushName || '',
|
||||
instanceName: instance.instanceName,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
url = `${data.url}/api/v1/sendMessage`;
|
||||
|
||||
reqData = {
|
||||
startParams: {
|
||||
publicId: data.typebot,
|
||||
prefilledVariables: {
|
||||
...data.prefilledVariables,
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName || data.prefilledVariables?.pushName || '',
|
||||
instanceName: instance.instanceName,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const request = await axios.post(url, reqData);
|
||||
|
||||
if (request?.data?.sessionId) {
|
||||
data.sessions.push({
|
||||
remoteJid: data.remoteJid,
|
||||
sessionId: `${id}-${request.data.sessionId}`,
|
||||
status: 'opened',
|
||||
createdAt: Date.now(),
|
||||
updateAt: Date.now(),
|
||||
prefilledVariables: {
|
||||
...data.prefilledVariables,
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName || '',
|
||||
instanceName: instance.instanceName,
|
||||
},
|
||||
});
|
||||
|
||||
const typebotData = {
|
||||
enabled: data.enabled,
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
expire: data.expire,
|
||||
keyword_finish: data.keyword_finish,
|
||||
delay_message: data.delay_message,
|
||||
unknown_message: data.unknown_message,
|
||||
listening_from_me: data.listening_from_me,
|
||||
sessions: data.sessions,
|
||||
};
|
||||
|
||||
this.create(instance, typebotData);
|
||||
}
|
||||
return request.data;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public async sendWAMessage(
|
||||
instance: InstanceDto,
|
||||
remoteJid: string,
|
||||
messages: any[],
|
||||
input: any[],
|
||||
clientSideActions: any[],
|
||||
) {
|
||||
processMessages(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
messages,
|
||||
input,
|
||||
clientSideActions,
|
||||
this.eventEmitter,
|
||||
applyFormatting,
|
||||
).catch((err) => {
|
||||
console.error('Erro ao processar mensagens:', err);
|
||||
});
|
||||
|
||||
function findItemAndGetSecondsToWait(array, targetId) {
|
||||
if (!array) return null;
|
||||
|
||||
for (const item of array) {
|
||||
if (item.lastBubbleBlockId === targetId) {
|
||||
return item.wait?.secondsToWaitFor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyFormatting(element) {
|
||||
let text = '';
|
||||
|
||||
if (element.text) {
|
||||
text += element.text;
|
||||
}
|
||||
|
||||
if (
|
||||
element.children &&
|
||||
(element.type === 'p' ||
|
||||
element.type === 'a' ||
|
||||
element.type === 'inline-variable' ||
|
||||
element.type === 'variable')
|
||||
) {
|
||||
for (const child of element.children) {
|
||||
text += applyFormatting(child);
|
||||
}
|
||||
}
|
||||
|
||||
let formats = '';
|
||||
|
||||
if (element.bold) {
|
||||
formats += '*';
|
||||
}
|
||||
|
||||
if (element.italic) {
|
||||
formats += '_';
|
||||
}
|
||||
|
||||
if (element.underline) {
|
||||
formats += '~';
|
||||
}
|
||||
|
||||
let formattedText = `${formats}${text}${formats.split('').reverse().join('')}`;
|
||||
|
||||
if (element.url) {
|
||||
formattedText = element.children[0]?.text ? `[${formattedText}]\n(${element.url})` : `${element.url}`;
|
||||
}
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
|
||||
async function processMessages(instance, messages, input, clientSideActions, eventEmitter, applyFormatting) {
|
||||
for (const message of messages) {
|
||||
if (message.type === 'text') {
|
||||
let formattedText = '';
|
||||
|
||||
for (const richText of message.content.richText) {
|
||||
for (const element of richText.children) {
|
||||
formattedText += applyFormatting(element);
|
||||
}
|
||||
formattedText += '\n';
|
||||
}
|
||||
|
||||
formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, '');
|
||||
|
||||
await instance.textMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: instance.localTypebot.delay_message || 1000,
|
||||
presence: 'composing',
|
||||
},
|
||||
textMessage: {
|
||||
text: formattedText,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (message.type === 'image') {
|
||||
await instance.mediaMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: instance.localTypebot.delay_message || 1000,
|
||||
presence: 'composing',
|
||||
},
|
||||
mediaMessage: {
|
||||
mediatype: 'image',
|
||||
media: message.content.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (message.type === 'video') {
|
||||
await instance.mediaMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: instance.localTypebot.delay_message || 1000,
|
||||
presence: 'composing',
|
||||
},
|
||||
mediaMessage: {
|
||||
mediatype: 'video',
|
||||
media: message.content.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (message.type === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: instance.localTypebot.delay_message || 1000,
|
||||
presence: 'recording',
|
||||
encoding: true,
|
||||
},
|
||||
audioMessage: {
|
||||
audio: message.content.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const wait = findItemAndGetSecondsToWait(clientSideActions, message.id);
|
||||
|
||||
if (wait) {
|
||||
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (input) {
|
||||
if (input.type === 'choice input') {
|
||||
let formattedText = '';
|
||||
|
||||
const items = input.items;
|
||||
|
||||
for (const item of items) {
|
||||
formattedText += `▶️ ${item.content}\n`;
|
||||
}
|
||||
|
||||
formattedText = formattedText.replace(/\n$/, '');
|
||||
|
||||
await instance.textMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: instance.localTypebot.delay_message || 1000,
|
||||
presence: 'composing',
|
||||
},
|
||||
textMessage: {
|
||||
text: formattedText,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
eventEmitter.emit('typebot:end', {
|
||||
instance: instance,
|
||||
remoteJid: remoteJid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async sendTypebot(instance: InstanceDto, remoteJid: string, msg: MessageRaw) {
|
||||
const findTypebot = await this.find(instance);
|
||||
const url = findTypebot.url;
|
||||
const typebot = findTypebot.typebot;
|
||||
const sessions = (findTypebot.sessions as Session[]) ?? [];
|
||||
const expire = findTypebot.expire;
|
||||
const keyword_finish = findTypebot.keyword_finish;
|
||||
const delay_message = findTypebot.delay_message;
|
||||
const unknown_message = findTypebot.unknown_message;
|
||||
const listening_from_me = findTypebot.listening_from_me;
|
||||
const messageType = this.getTypeMessage(msg.message).messageType;
|
||||
|
||||
const session = sessions.find((session) => session.remoteJid === remoteJid);
|
||||
|
||||
try {
|
||||
if (session && expire && expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const diff = now - session.updateAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > expire) {
|
||||
const newSessions = await this.clearSessions(instance, remoteJid);
|
||||
|
||||
const data = await this.createNewSession(instance, {
|
||||
enabled: findTypebot.enabled,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
expire: expire,
|
||||
keyword_finish: keyword_finish,
|
||||
delay_message: delay_message,
|
||||
unknown_message: unknown_message,
|
||||
listening_from_me: listening_from_me,
|
||||
sessions: newSessions,
|
||||
remoteJid: remoteJid,
|
||||
pushName: msg.pushName,
|
||||
});
|
||||
|
||||
await this.sendWAMessage(instance, remoteJid, data.messages, data.input, data.clientSideActions);
|
||||
|
||||
if (data.messages.length === 0) {
|
||||
const content = this.getConversationMessage(msg.message);
|
||||
|
||||
if (!content) {
|
||||
if (unknown_message) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: delay_message || 1000,
|
||||
presence: 'composing',
|
||||
},
|
||||
textMessage: {
|
||||
text: unknown_message,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyword_finish && content.toLowerCase() === keyword_finish.toLowerCase()) {
|
||||
const newSessions = await this.clearSessions(instance, remoteJid);
|
||||
|
||||
const typebotData = {
|
||||
enabled: findTypebot.enabled,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
expire: expire,
|
||||
keyword_finish: keyword_finish,
|
||||
delay_message: delay_message,
|
||||
unknown_message: unknown_message,
|
||||
listening_from_me: listening_from_me,
|
||||
sessions: newSessions,
|
||||
};
|
||||
|
||||
this.create(instance, typebotData);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION;
|
||||
let urlTypebot: string;
|
||||
let reqData: {};
|
||||
if (version === 'latest') {
|
||||
urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`;
|
||||
reqData = {
|
||||
message: content,
|
||||
};
|
||||
} else {
|
||||
urlTypebot = `${url}/api/v1/sendMessage`;
|
||||
reqData = {
|
||||
message: content,
|
||||
sessionId: data.sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
const request = await axios.post(urlTypebot, reqData);
|
||||
|
||||
await this.sendWAMessage(
|
||||
instance,
|
||||
remoteJid,
|
||||
request.data.messages,
|
||||
request.data.input,
|
||||
request.data.clientSideActions,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
enabled: findTypebot.enabled,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
expire: expire,
|
||||
keyword_finish: keyword_finish,
|
||||
delay_message: delay_message,
|
||||
unknown_message: unknown_message,
|
||||
listening_from_me: listening_from_me,
|
||||
sessions: sessions,
|
||||
remoteJid: remoteJid,
|
||||
pushName: msg.pushName,
|
||||
prefilledVariables: {
|
||||
messageType: messageType,
|
||||
},
|
||||
});
|
||||
|
||||
await this.sendWAMessage(instance, remoteJid, data.messages, data.input, data.clientSideActions);
|
||||
|
||||
if (data.messages.length === 0) {
|
||||
const content = this.getConversationMessage(msg.message);
|
||||
|
||||
if (!content) {
|
||||
if (unknown_message) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: delay_message || 1000,
|
||||
presence: 'composing',
|
||||
},
|
||||
textMessage: {
|
||||
text: unknown_message,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyword_finish && content.toLowerCase() === keyword_finish.toLowerCase()) {
|
||||
const newSessions = await this.clearSessions(instance, remoteJid);
|
||||
|
||||
const typebotData = {
|
||||
enabled: findTypebot.enabled,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
expire: expire,
|
||||
keyword_finish: keyword_finish,
|
||||
delay_message: delay_message,
|
||||
unknown_message: unknown_message,
|
||||
listening_from_me: listening_from_me,
|
||||
sessions: newSessions,
|
||||
};
|
||||
|
||||
this.create(instance, typebotData);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let request: any;
|
||||
try {
|
||||
const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION;
|
||||
let urlTypebot: string;
|
||||
let reqData: {};
|
||||
if (version === 'latest') {
|
||||
urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`;
|
||||
reqData = {
|
||||
message: content,
|
||||
};
|
||||
} else {
|
||||
urlTypebot = `${url}/api/v1/sendMessage`;
|
||||
reqData = {
|
||||
message: content,
|
||||
sessionId: data.sessionId,
|
||||
};
|
||||
}
|
||||
request = await axios.post(urlTypebot, reqData);
|
||||
|
||||
await this.sendWAMessage(
|
||||
instance,
|
||||
remoteJid,
|
||||
request.data.messages,
|
||||
request.data.input,
|
||||
request.data.clientSideActions,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sessions.map((session) => {
|
||||
if (session.remoteJid === remoteJid) {
|
||||
session.updateAt = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
const typebotData = {
|
||||
enabled: findTypebot.enabled,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
expire: expire,
|
||||
keyword_finish: keyword_finish,
|
||||
delay_message: delay_message,
|
||||
unknown_message: unknown_message,
|
||||
listening_from_me: listening_from_me,
|
||||
sessions,
|
||||
};
|
||||
|
||||
this.create(instance, typebotData);
|
||||
|
||||
const content = this.getConversationMessage(msg.message);
|
||||
|
||||
if (!content) {
|
||||
if (unknown_message) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
options: {
|
||||
delay: delay_message || 1000,
|
||||
presence: 'composing',
|
||||
},
|
||||
textMessage: {
|
||||
text: unknown_message,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyword_finish && content.toLowerCase() === keyword_finish.toLowerCase()) {
|
||||
const newSessions = await this.clearSessions(instance, remoteJid);
|
||||
|
||||
const typebotData = {
|
||||
enabled: findTypebot.enabled,
|
||||
url: url,
|
||||
typebot: typebot,
|
||||
expire: expire,
|
||||
keyword_finish: keyword_finish,
|
||||
delay_message: delay_message,
|
||||
unknown_message: unknown_message,
|
||||
listening_from_me: listening_from_me,
|
||||
sessions: newSessions,
|
||||
};
|
||||
|
||||
this.create(instance, typebotData);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION;
|
||||
let urlTypebot: string;
|
||||
let reqData: {};
|
||||
if (version === 'latest') {
|
||||
urlTypebot = `${url}/api/v1/sessions/${session.sessionId.split('-')[1]}/continueChat`;
|
||||
reqData = {
|
||||
message: content,
|
||||
};
|
||||
} else {
|
||||
urlTypebot = `${url}/api/v1/sendMessage`;
|
||||
reqData = {
|
||||
message: content,
|
||||
sessionId: session.sessionId.split('-')[1],
|
||||
};
|
||||
}
|
||||
const request = await axios.post(urlTypebot, reqData);
|
||||
|
||||
await this.sendWAMessage(
|
||||
instance,
|
||||
remoteJid,
|
||||
request.data.messages,
|
||||
request.data.input,
|
||||
request.data.clientSideActions,
|
||||
);
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||