mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-19 11:52:20 -06:00
Compare commits
521 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2085b59ea | ||
|
|
2513f96178 | ||
|
|
70a2c18146 | ||
|
|
b8953f1431 | ||
|
|
a91a4ad19c | ||
|
|
00ba227e15 | ||
|
|
c02d37028b | ||
|
|
14771ab84e | ||
|
|
20352e77ef | ||
|
|
78f7618d04 | ||
|
|
57b19d85d5 | ||
|
|
b325500310 | ||
|
|
4681576cfc | ||
|
|
7a99fba556 | ||
|
|
6e652d6ea2 | ||
|
|
33a922995b | ||
|
|
74cb65c4ea | ||
|
|
f9c4255500 | ||
|
|
af423cef28 | ||
|
|
40ce6b56ca | ||
|
|
4945345519 | ||
|
|
ab9e0edad6 | ||
|
|
3390958314 | ||
|
|
a8343a8739 | ||
|
|
dab66fc8c8 | ||
|
|
03a44cf9b2 | ||
|
|
fb11f3f99c | ||
|
|
d4eb61f64d | ||
|
|
4f043f9576 | ||
|
|
79f4a22217 | ||
|
|
20dd1b1660 | ||
|
|
fe8280ab7b | ||
|
|
bc11d0f751 | ||
|
|
095e435561 | ||
|
|
96f4b80d46 | ||
|
|
9cdb897a0f | ||
|
|
b5c67774dc | ||
|
|
7f8293f4c6 | ||
|
|
a62f9ebe46 | ||
|
|
69726f0dc2 | ||
|
|
6101c8d651 | ||
|
|
c66485ef98 | ||
|
|
4a25cd1ff7 | ||
|
|
23f54d1d96 | ||
|
|
5191438acf | ||
|
|
96d3ec2017 | ||
|
|
cf95c027eb | ||
|
|
9b7ca4bfb7 | ||
|
|
d490f8f576 | ||
|
|
68e847d10e | ||
|
|
b98cd11fb1 | ||
|
|
5386d7171b | ||
|
|
f59cae7ee2 | ||
|
|
0e358cf9c1 | ||
|
|
6954472070 | ||
|
|
afd0e01ddb | ||
|
|
b3dae7a68e | ||
|
|
44d4781f6f | ||
|
|
e304b1dcdf | ||
|
|
f8f2153cb4 | ||
|
|
192c34caa0 | ||
|
|
cdef7dc9f9 | ||
|
|
85798b209c | ||
|
|
71ebecbed3 | ||
|
|
419300b31f | ||
|
|
9fd40a411a | ||
|
|
d98fa5259e | ||
|
|
05886ec684 | ||
|
|
89d4d341f6 | ||
|
|
1ca829c00b | ||
|
|
e321609b93 | ||
|
|
196617507e | ||
|
|
6da71f5161 | ||
|
|
5d0278a589 | ||
|
|
2fb3eac383 | ||
|
|
4a5696eda9 | ||
|
|
11520481ba | ||
|
|
95a53d33ef | ||
|
|
d458c978f3 | ||
|
|
bd35d7977c | ||
|
|
e92961e7b0 | ||
|
|
f11d490f7a | ||
|
|
37319966db | ||
|
|
630f5c5624 | ||
|
|
3e690fe9e2 | ||
|
|
09429e68fe | ||
|
|
9acccf723d | ||
|
|
3b920f93c5 | ||
|
|
85936dcaed | ||
|
|
333ef3eeb8 | ||
|
|
8dd51b0302 | ||
|
|
e6ec706a38 | ||
|
|
53101d4571 | ||
|
|
c7b5abce6e | ||
|
|
5b1b5ff9d2 | ||
|
|
505490d237 | ||
|
|
675745ae3c | ||
|
|
6dfbfe2d83 | ||
|
|
1c247498d8 | ||
|
|
419324837c | ||
|
|
17f97fb051 | ||
|
|
f7e7a6c901 | ||
|
|
b35b33ca50 | ||
|
|
3efe69ada3 | ||
|
|
287c679ce4 | ||
|
|
918697866f | ||
|
|
3c917af602 | ||
|
|
52798fd761 | ||
|
|
34446d188e | ||
|
|
ef31b6de1f | ||
|
|
d7afe5d7ab | ||
|
|
72fb2f408b | ||
|
|
e4f7856ca9 | ||
|
|
e86b6463fd | ||
|
|
0302944654 | ||
|
|
1b0d81b022 | ||
|
|
854d357518 | ||
|
|
6e8da4a8dc | ||
|
|
e92e98dd22 | ||
|
|
cdd2e59755 | ||
|
|
1a1d9fc957 | ||
|
|
8ea4d65bc2 | ||
|
|
af713dee55 | ||
|
|
ee9ccb55ca | ||
|
|
977f686233 | ||
|
|
a38caeaf5f | ||
|
|
873fdcb22a | ||
|
|
2e077a77ef | ||
|
|
4be818a436 | ||
|
|
a7badb9af5 | ||
|
|
ba5fb567eb | ||
|
|
1070caf131 | ||
|
|
9766e10ce9 | ||
|
|
cc5612822f | ||
|
|
83c1040114 | ||
|
|
b4d1148d6a | ||
|
|
bd94423e5b | ||
|
|
94670cbca3 | ||
|
|
bda3f5f146 | ||
|
|
7724fe3a4f | ||
|
|
8b5f49592a | ||
|
|
6c0082cd7a | ||
|
|
3391b2ce4b | ||
|
|
39606240da | ||
|
|
f4fbc4afc6 | ||
|
|
70905e7338 | ||
|
|
07cccb7c7f | ||
|
|
5f0862a759 | ||
|
|
029d68e2cd | ||
|
|
a1fae85531 | ||
|
|
1afa8df556 | ||
|
|
bfb044b234 | ||
|
|
ae99ec7a0e | ||
|
|
22c379aa36 | ||
|
|
62c00c3db2 | ||
|
|
afc2927837 | ||
|
|
c17b48bca0 | ||
|
|
534c54a171 | ||
|
|
8603e6def0 | ||
|
|
bcf2febf48 | ||
|
|
a02ecc88f5 | ||
|
|
bc451e8493 | ||
|
|
1eb2c848f7 | ||
|
|
7cfc359be9 | ||
|
|
421e762c2d | ||
|
|
9e1f9cbb83 | ||
|
|
44e0ff2250 | ||
|
|
614ad7cbdf | ||
|
|
77b3b331f8 | ||
|
|
e469dc132f | ||
|
|
fa14abac5a | ||
|
|
9db56560e4 | ||
|
|
786e57603f | ||
|
|
a53e0a8694 | ||
|
|
16e4bba108 | ||
|
|
bd19fff264 | ||
|
|
94285ecb90 | ||
|
|
3500fbe27f | ||
|
|
cb76381466 | ||
|
|
98502f6555 | ||
|
|
3fc77e4c76 | ||
|
|
19e291178c | ||
|
|
7682a679d1 | ||
|
|
97ca23a7b0 | ||
|
|
95bd85b6e3 | ||
|
|
64fc7a05ac | ||
|
|
39aaf29d54 | ||
|
|
22e99f7934 | ||
|
|
dd0dfd447c | ||
|
|
fb18267ac5 | ||
|
|
fc5965938e | ||
|
|
623efd86a2 | ||
|
|
3297364c10 | ||
|
|
373a531e88 | ||
|
|
e081533f02 | ||
|
|
17fd407d8d | ||
|
|
0cdc67effe | ||
|
|
ddaf32be76 | ||
|
|
0b2d8a752f | ||
|
|
9cda6a2f99 | ||
|
|
d2263af3e8 | ||
|
|
0239638232 | ||
|
|
3459d61eff | ||
|
|
5330121c49 | ||
|
|
53c1c218c4 | ||
|
|
06378e5d6b | ||
|
|
6a83e89394 | ||
|
|
d24540d6dd | ||
|
|
2af7b24013 | ||
|
|
6f47a54fae | ||
|
|
edde059fa1 | ||
|
|
dcb09b87fe | ||
|
|
bbf142cf39 | ||
|
|
da51b6bd76 | ||
|
|
8b15c11817 | ||
|
|
272a4de236 | ||
|
|
65111481b9 | ||
|
|
0ca109e9d6 | ||
|
|
5b817028a9 | ||
|
|
6a0fc19702 | ||
|
|
9cedf31eed | ||
|
|
f9567fbeaa | ||
|
|
d673c83a93 | ||
|
|
e16bb0e580 | ||
|
|
09120aa026 | ||
|
|
fbfa364df9 | ||
|
|
624b37e2aa | ||
|
|
fcde1f9acc | ||
|
|
cc01787501 | ||
|
|
c30bae4c3a | ||
|
|
2545013040 | ||
|
|
c53a96e757 | ||
|
|
a2d8642e1c | ||
|
|
9c530c69cf | ||
|
|
348a4ff251 | ||
|
|
fc00916345 | ||
|
|
69b4f1aa02 | ||
|
|
7cccda10bb | ||
|
|
33c808b195 | ||
|
|
d3ee370bdc | ||
|
|
7e5740b462 | ||
|
|
0b33a76394 | ||
|
|
6a0b024b13 | ||
|
|
fda6b0d50e | ||
|
|
9ec6847683 | ||
|
|
c745fbad64 | ||
|
|
40ea8bf356 | ||
|
|
0699ad4bb0 | ||
|
|
70a4fe8f6e | ||
|
|
71124755b0 | ||
|
|
a1cc504777 | ||
|
|
0fd2e04286 | ||
|
|
4f2b0c42f3 | ||
|
|
401b0359cd | ||
|
|
b1f3c5cc5f | ||
|
|
362736ea71 | ||
|
|
2544c10592 | ||
|
|
383805aa95 | ||
|
|
71101807bb | ||
|
|
38f089f04c | ||
|
|
6d63f2fb6e | ||
|
|
bb0b9b94ff | ||
|
|
a449fdf0ef | ||
|
|
ec9de49647 | ||
|
|
a7a9de2903 | ||
|
|
99b0c86278 | ||
|
|
e6a72bd829 | ||
|
|
341a0d884f | ||
|
|
383bac090a | ||
|
|
60a58ca037 | ||
|
|
e5989f3d47 | ||
|
|
bff3bf564b | ||
|
|
c74eee8e52 | ||
|
|
d1a28ea4f7 | ||
|
|
d2f1985913 | ||
|
|
90e27cc7d8 | ||
|
|
c4ddfe6804 | ||
|
|
aaa103a842 | ||
|
|
b2809b6f3e | ||
|
|
d52256718d | ||
|
|
8f0ede4207 | ||
|
|
95827a2d70 | ||
|
|
b064e512e2 | ||
|
|
db5f0d0891 | ||
|
|
ccbd866e42 | ||
|
|
dc67039b39 | ||
|
|
eeedfb0e2a | ||
|
|
3ab75faff7 | ||
|
|
095754d173 | ||
|
|
b94b452597 | ||
|
|
2ded19752f | ||
|
|
4c8c7ee19b | ||
|
|
8c6f95fbef | ||
|
|
402b37d7b4 | ||
|
|
09f79c94be | ||
|
|
7c7dca9da9 | ||
|
|
1d2e029b54 | ||
|
|
3f8d89e970 | ||
|
|
abda9e2113 | ||
|
|
2a020928e8 | ||
|
|
b436e5b0b0 | ||
|
|
9889035ddc | ||
|
|
07ce09d8e2 | ||
|
|
0d2a7ad50b | ||
|
|
7e8044a777 | ||
|
|
645f305cd6 | ||
|
|
829032dc08 | ||
|
|
3d40b0850b | ||
|
|
17bd108251 | ||
|
|
f695e8bdc9 | ||
|
|
bf59ff1287 | ||
|
|
ce1680f515 | ||
|
|
119ceba1ca | ||
|
|
9710fbdac4 | ||
|
|
658dae0b59 | ||
|
|
b89f1144b4 | ||
|
|
d196590862 | ||
|
|
c5c354ffe7 | ||
|
|
36757dddc2 | ||
|
|
027401b839 | ||
|
|
6e7dd51679 | ||
|
|
f8b1c6e0fa | ||
|
|
cee2bc4d71 | ||
|
|
043df62a8f | ||
|
|
53ae521863 | ||
|
|
13bdbc268c | ||
|
|
ea9c3fbbe0 | ||
|
|
22a958616d | ||
|
|
32cde710b8 | ||
|
|
d9aa111800 | ||
|
|
fb41ab14e8 | ||
|
|
5998fcf940 | ||
|
|
2198a86ae4 | ||
|
|
8a54efe11c | ||
|
|
4fadf64bae | ||
|
|
2f1df734e0 | ||
|
|
247c39fe39 | ||
|
|
ada63b58af | ||
|
|
5192f49a4f | ||
|
|
48b5fd41e0 | ||
|
|
5720bdc0ef | ||
|
|
ab2364b9a3 | ||
|
|
278add6b11 | ||
|
|
8f632a6f5c | ||
|
|
a4ac798b43 | ||
|
|
fc513f1d1d | ||
|
|
6f1667abb5 | ||
|
|
98b8419b8d | ||
|
|
59479f9a21 | ||
|
|
ff77bc018a | ||
|
|
33f7f2932d | ||
|
|
c939ed2337 | ||
|
|
a49c97996c | ||
|
|
da72edfb03 | ||
|
|
b51526aff3 | ||
|
|
b93ee2e023 | ||
|
|
b58d9e957f | ||
|
|
e5137e1aac | ||
|
|
6b120e5da2 | ||
|
|
342dacc398 | ||
|
|
d75c37e233 | ||
|
|
36df38d78b | ||
|
|
68c6ad4f91 | ||
|
|
8685f2fdc4 | ||
|
|
5a50381a8e | ||
|
|
736ca5e4b6 | ||
|
|
6c1355b64b | ||
|
|
95401cf9b0 | ||
|
|
0c5d28bb6c | ||
|
|
0d1e7c08c9 | ||
|
|
d665474404 | ||
|
|
9a72b90ab2 | ||
|
|
b143363c5d | ||
|
|
56a8e09ba8 | ||
|
|
023e030802 | ||
|
|
e27db0612f | ||
|
|
e51b6e9270 | ||
|
|
867e8493aa | ||
|
|
8135994340 | ||
|
|
427c994993 | ||
|
|
da74611769 | ||
|
|
91e7a32209 | ||
|
|
4137984b5d | ||
|
|
96821f5d9a | ||
|
|
3c2ea5c67c | ||
|
|
4a5d7a91e2 | ||
|
|
9109b140a9 | ||
|
|
ff5a8adc71 | ||
|
|
b09638600e | ||
|
|
fc84e0f327 | ||
|
|
c1494ca035 | ||
|
|
7ea46a05ca | ||
|
|
f8f1cbf4a2 | ||
|
|
79b1c6bb1c | ||
|
|
169d2f797b | ||
|
|
db9cdbfc38 | ||
|
|
6afcc958c5 | ||
|
|
2166aad1d3 | ||
|
|
14fea2f5e0 | ||
|
|
9122dae262 | ||
|
|
96549664c9 | ||
|
|
fa19c7fa89 | ||
|
|
503728e1e7 | ||
|
|
f11e3247f0 | ||
|
|
7f10a0eecd | ||
|
|
a1085b4a4d | ||
|
|
b1b2d18e5d | ||
|
|
37a9e316a8 | ||
|
|
f340f7716f | ||
|
|
dab843f118 | ||
|
|
f7db442a94 | ||
|
|
3a04f7587e | ||
|
|
666c0b514d | ||
|
|
ab5eb80edd | ||
|
|
b0219e5e5a | ||
|
|
d7b4965d60 | ||
|
|
cfe6bd9ae0 | ||
|
|
ac58f58bbc | ||
|
|
cb08f6b152 | ||
|
|
a817d62067 | ||
|
|
540467293c | ||
|
|
616ae0a7eb | ||
|
|
d598c4ed0b | ||
|
|
ca451bfacc | ||
|
|
f971f388d5 | ||
|
|
236b0f9b26 | ||
|
|
0f2498bbaa | ||
|
|
2816a16387 | ||
|
|
a82669b6fa | ||
|
|
72a33ae59f | ||
|
|
425d340956 | ||
|
|
f2872cf59a | ||
|
|
1773f2738e | ||
|
|
18626c9846 | ||
|
|
6212ee3eb0 | ||
|
|
8c877029e5 | ||
|
|
0000c1c05c | ||
|
|
16daf9be8f | ||
|
|
e0c960cc54 | ||
|
|
08ceb803c8 | ||
|
|
6b72550286 | ||
|
|
df0990df0f | ||
|
|
60c9e231be | ||
|
|
2fb6318011 | ||
|
|
070159568e | ||
|
|
bfba702fde | ||
|
|
d5c2cfb4f9 | ||
|
|
cdab5e2ae8 | ||
|
|
682eaa995f | ||
|
|
62ea22a06a | ||
|
|
370025b8f9 | ||
|
|
5d13f7055b | ||
|
|
c4e87c160e | ||
|
|
ee8e937fad | ||
|
|
d5a7e03ec2 | ||
|
|
52b6b61ac9 | ||
|
|
6e6711a5af | ||
|
|
013fa9dc08 | ||
|
|
a42bc988ec | ||
|
|
3582cd38fb | ||
|
|
bfd8c08987 | ||
|
|
ecbbc5b090 | ||
|
|
b7e15be418 | ||
|
|
1d5508736e | ||
|
|
cefe3ef6c3 | ||
|
|
09a33a423e | ||
|
|
e092a80aeb | ||
|
|
99a533afc1 | ||
|
|
b603021f56 | ||
|
|
7bf0fd1c36 | ||
|
|
52cf4fa8b8 | ||
|
|
ef7574273c | ||
|
|
d797d8177c | ||
|
|
e7ed1446ee | ||
|
|
5eaabfb1eb | ||
|
|
59383d5944 | ||
|
|
d87d7c0775 | ||
|
|
753f4ba141 | ||
|
|
7ef8afa9b3 | ||
|
|
c5fd81ddbf | ||
|
|
8f855b4bfd | ||
|
|
084be1cee2 | ||
|
|
997aceeebf | ||
|
|
c8410bd146 | ||
|
|
3d51b45e2b | ||
|
|
50d84d1a08 | ||
|
|
fd1f08a41e | ||
|
|
8dc27919b1 | ||
|
|
6591a67ab6 | ||
|
|
08192a03ca | ||
|
|
8f86c9d758 | ||
|
|
0e5f9e3b77 | ||
|
|
1665654676 | ||
|
|
133eddd742 | ||
|
|
c55312d206 | ||
|
|
52216ec08e | ||
|
|
7a01cdf0ef | ||
|
|
65a9c78d86 | ||
|
|
f6ccd58dee | ||
|
|
fbccf2eb2a | ||
|
|
23640a71b8 | ||
|
|
fce3e55e91 | ||
|
|
9f39ec2110 | ||
|
|
89c4c194df | ||
|
|
a4e7baa41c | ||
|
|
e22ff6c0d9 | ||
|
|
11d31123ac | ||
|
|
0fdc47e8f0 | ||
|
|
60db8081bd | ||
|
|
37b003f169 | ||
|
|
891c3eb5d3 | ||
|
|
3b99699f1a | ||
|
|
e1de70542b | ||
|
|
c10680df41 | ||
|
|
171f460f3b | ||
|
|
6d0ad5f3db | ||
|
|
f34115fdcb | ||
|
|
f9705c07dc | ||
|
|
e986768716 | ||
|
|
34769e2293 | ||
|
|
5401ecd2c4 |
@@ -1,5 +1,6 @@
|
||||
.git
|
||||
*Dockerfile*
|
||||
*docker-compose*
|
||||
.env
|
||||
node_modules
|
||||
dist
|
||||
38
.env.example
38
.env.example
@@ -3,6 +3,9 @@ SERVER_PORT=8080
|
||||
# Server URL - Set your application url
|
||||
SERVER_URL=http://localhost:8080
|
||||
|
||||
SSL_CONF_PRIVKEY=/path/to/cert.key
|
||||
SSL_CONF_FULLCHAIN=/path/to/cert.crt
|
||||
|
||||
SENTRY_DSN=
|
||||
|
||||
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
|
||||
@@ -24,13 +27,17 @@ EVENT_EMITTER_MAX_LISTENERS=50
|
||||
# If you don't even want an expiration, enter the value false
|
||||
DEL_INSTANCE=false
|
||||
|
||||
# Provider: postgresql | mysql
|
||||
# Provider: postgresql | mysql | psql_bouncer
|
||||
DATABASE_PROVIDER=postgresql
|
||||
DATABASE_CONNECTION_URI='postgresql://user:pass@localhost:5432/evolution?schema=public'
|
||||
DATABASE_CONNECTION_URI='postgresql://user:pass@postgres:5432/evolution_db?schema=evolution_api'
|
||||
# Client name for the database connection
|
||||
# It is used to separate an API installation from another that uses the same database.
|
||||
DATABASE_CONNECTION_CLIENT_NAME=evolution_exchange
|
||||
|
||||
# Bouncer connection: used only when the database provider is set to 'psql_bouncer'.
|
||||
# Defines the PostgreSQL URL with pgbouncer enabled (pgbouncer=true).
|
||||
# DATABASE_BOUNCER_CONNECTION_URI=postgresql://user:pass@pgbouncer:5432/evolution_db?pgbouncer=true&schema=evolution_api
|
||||
|
||||
# Choose the data you want to save in the application's database
|
||||
DATABASE_SAVE_DATA_INSTANCE=true
|
||||
DATABASE_SAVE_DATA_NEW_MESSAGE=true
|
||||
@@ -47,8 +54,11 @@ DATABASE_DELETE_MESSAGE=true
|
||||
RABBITMQ_ENABLED=false
|
||||
RABBITMQ_URI=amqp://localhost
|
||||
RABBITMQ_EXCHANGE_NAME=evolution
|
||||
RABBITMQ_FRAME_MAX=8192
|
||||
# Global events - By enabling this variable, events from all instances are sent in the same event queue.
|
||||
RABBITMQ_GLOBAL_ENABLED=false
|
||||
# Prefix key to queue name
|
||||
RABBITMQ_PREFIX_KEY=evolution
|
||||
# Choose the events you want to send to RabbitMQ
|
||||
RABBITMQ_EVENTS_APPLICATION_STARTUP=false
|
||||
RABBITMQ_EVENTS_INSTANCE_CREATE=false
|
||||
@@ -60,6 +70,7 @@ RABBITMQ_EVENTS_MESSAGES_EDITED=false
|
||||
RABBITMQ_EVENTS_MESSAGES_UPDATE=false
|
||||
RABBITMQ_EVENTS_MESSAGES_DELETE=false
|
||||
RABBITMQ_EVENTS_SEND_MESSAGE=false
|
||||
RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
|
||||
RABBITMQ_EVENTS_CONTACTS_SET=false
|
||||
RABBITMQ_EVENTS_CONTACTS_UPSERT=false
|
||||
RABBITMQ_EVENTS_CONTACTS_UPDATE=false
|
||||
@@ -106,6 +117,7 @@ PUSHER_EVENTS_MESSAGES_EDITED=true
|
||||
PUSHER_EVENTS_MESSAGES_UPDATE=true
|
||||
PUSHER_EVENTS_MESSAGES_DELETE=true
|
||||
PUSHER_EVENTS_SEND_MESSAGE=true
|
||||
PUSHER_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
PUSHER_EVENTS_CONTACTS_SET=true
|
||||
PUSHER_EVENTS_CONTACTS_UPSERT=true
|
||||
PUSHER_EVENTS_CONTACTS_UPDATE=true
|
||||
@@ -147,6 +159,7 @@ WEBHOOK_EVENTS_MESSAGES_EDITED=true
|
||||
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
@@ -171,6 +184,15 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
||||
WEBHOOK_EVENTS_ERRORS=false
|
||||
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||
|
||||
WEBHOOK_REQUEST_TIMEOUT_MS=60000
|
||||
WEBHOOK_RETRY_MAX_ATTEMPTS=10
|
||||
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
|
||||
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
|
||||
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
|
||||
WEBHOOK_RETRY_JITTER_FACTOR=0.2
|
||||
# Comma separated list of HTTP status codes that should not trigger retries
|
||||
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
|
||||
|
||||
# Name that will be displayed on smartphone connection
|
||||
CONFIG_SESSION_PHONE_CLIENT=Evolution API
|
||||
# Browser Name = Chrome | Firefox | Edge | Opera | Safari
|
||||
@@ -178,7 +200,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
|
||||
# Whatsapp Web version for baileys channel
|
||||
# https://web.whatsapp.com/check-update?version=0&platform=web
|
||||
CONFIG_SESSION_PHONE_VERSION=2.3000.1015901307
|
||||
|
||||
|
||||
# Set qrcode display limit
|
||||
QRCODE_LIMIT=30
|
||||
@@ -208,6 +230,12 @@ OPENAI_ENABLED=false
|
||||
# Dify - Environment variables
|
||||
DIFY_ENABLED=false
|
||||
|
||||
# n8n - Environment variables
|
||||
N8N_ENABLED=false
|
||||
|
||||
# EvoAI - Environment variables
|
||||
EVOAI_ENABLED=false
|
||||
|
||||
# Cache - Environment variables
|
||||
# Redis Cache enabled
|
||||
CACHE_REDIS_ENABLED=true
|
||||
@@ -248,6 +276,10 @@ S3_USE_SSL=true
|
||||
# S3_USE_SSL=true
|
||||
# S3_REGION=eu-south
|
||||
|
||||
# Evolution Audio Converter - Environment variables - https://github.com/EvolutionAPI/evolution-audio-converter
|
||||
# API_AUDIO_CONVERTER=http://localhost:4040/process-audio
|
||||
# API_AUDIO_CONVERTER_KEY=429683C4C977415CAAFCCE10F7D57E11
|
||||
|
||||
# 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=429683C4C977415CAAFCCE10F7D57E11
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
sourceType: 'CommonJS',
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
warnOnUnsupportedTypeScriptVersion: false,
|
||||
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'simple-import-sort', 'import'],
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
|
||||
|
||||
28
.github/workflows/check_code_quality.yml
vendored
Normal file
28
.github/workflows/check_code_quality.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Check Code Quality
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
check-lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install packages
|
||||
run: npm install
|
||||
|
||||
- name: Check linting
|
||||
run: npm run lint:check
|
||||
|
||||
- name: Check build
|
||||
run: npm run db:generate
|
||||
|
||||
- name: Check build
|
||||
run: npm run build
|
||||
2
.github/workflows/publish_docker_image.yml
vendored
2
.github/workflows/publish_docker_image.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: atendai/evolution-api
|
||||
images: evoapicloud/evolution-api
|
||||
tags: type=semver,pattern=v{{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: atendai/evolution-api
|
||||
images: evoapicloud/evolution-api
|
||||
tags: homolog
|
||||
|
||||
- name: Set up QEMU
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: atendai/evolution-api
|
||||
images: evoapicloud/evolution-api
|
||||
tags: latest
|
||||
|
||||
- name: Set up QEMU
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,6 +2,8 @@
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
.cursor*
|
||||
|
||||
/Docker/.env
|
||||
|
||||
.vscode
|
||||
@@ -21,7 +23,6 @@ lerna-debug.log*
|
||||
# Package
|
||||
/yarn.lock
|
||||
/pnpm-lock.yaml
|
||||
/package-lock.json
|
||||
|
||||
# IDEs
|
||||
.vscode/*
|
||||
|
||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -1,4 +1,99 @@
|
||||
# 2.2.0 (develop)
|
||||
# 2.3.2 (2025-09-02)
|
||||
|
||||
### Features
|
||||
|
||||
* Add support to socks proxy
|
||||
|
||||
### Fixed
|
||||
|
||||
* Added key id into webhook payload in n8n service
|
||||
* Enhance RabbitMQ controller with improved connection management and shutdown procedures
|
||||
* Convert outgoing images to JPEG before sending with Chatwoot
|
||||
* Update baileys dependency to version 6.7.19
|
||||
|
||||
# 2.3.1 (2025-07-29)
|
||||
|
||||
### Feature
|
||||
|
||||
* Add BaileysMessageProcessor for improved message handling and integrate rxjs for asynchronous processing
|
||||
* Enhance message processing with retry logic for error handling
|
||||
|
||||
### Fixed
|
||||
|
||||
* Update Baileys Version
|
||||
* Update Dockerhub Repository and Delete Config Session Variable
|
||||
* Fixed sending variables in typebot
|
||||
* Add unreadMessages in the response
|
||||
* Phone number as message ID for Evo AI
|
||||
* Fix upload to s3 when media message
|
||||
* Simplify edited message check in BaileysStartupService
|
||||
* Avoid corrupting URLs with query strings
|
||||
* Removed CONFIG_SESSION_PHONE_VERSION environment variable
|
||||
|
||||
# 2.3.0 (2025-06-17 09:19)
|
||||
|
||||
### Feature
|
||||
|
||||
* Add support to get Catalogs and Collections with new routes: '{{baseUrl}}/chat/fetchCatalogs' and '{{baseUrl}}/chat/fetchCollections'
|
||||
* Add NATS integration support to the event system
|
||||
* Add message location support meta
|
||||
* Add S3_SKIP_POLICY env variable to disable setBucketPolicy for incompatible providers
|
||||
* Add EvoAI integration with models, services, and routes
|
||||
* Add N8n integration with models, services, and routes
|
||||
|
||||
### Fixed
|
||||
|
||||
* Shell injection vulnerability
|
||||
* Update Baileys Version v6.7.18
|
||||
* Audio send duplicate from chatwoot
|
||||
* Chatwoot csat creating new conversation in another language
|
||||
* Refactor SQS controller to correct bug in sqs events by instance
|
||||
* Adjustin cloud api send audio and video
|
||||
* Preserve animation in GIF and WebP stickers
|
||||
* Preventing use conversation from other inbox for the same user
|
||||
* Ensure full WhatsApp compatibility for audio conversion (libopus, 48kHz, mono)
|
||||
* Enhance message fetching and processing logic
|
||||
* Added lid on whatsapp numbers router
|
||||
* Now if the CONFIG_SESSION_PHONE_VERSION variable is not filled in it automatically searches for the most updated version
|
||||
|
||||
### Security
|
||||
|
||||
* Change execSync to execFileSync
|
||||
* Enhance WebSocket authentication and connection handling
|
||||
|
||||
# 2.2.3 (2025-02-03 11:52)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix cache in local file system
|
||||
* Update Baileys Version
|
||||
|
||||
# 2.2.2 (2025-01-31 06:55)
|
||||
|
||||
### Features
|
||||
|
||||
* Added prefix key to queue name in RabbitMQ
|
||||
|
||||
### Fixed
|
||||
|
||||
* Update Baileys Version
|
||||
|
||||
# 2.2.1 (2025-01-22 14:37)
|
||||
|
||||
### Features
|
||||
|
||||
* Retry system for send webhooks
|
||||
* Message filtering to support timestamp range queries
|
||||
* Chats filtering to support timestamp range queries
|
||||
|
||||
### Fixed
|
||||
|
||||
* Correction of webhook global
|
||||
* Fixed send audio with whatsapp cloud api
|
||||
* Refactor on fetch chats
|
||||
* Refactor on Evolution Channel
|
||||
|
||||
# 2.2.0 (2024-10-18 10:00)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -8,6 +103,8 @@
|
||||
* Added unreadMessages to chats
|
||||
* Pusher event integration
|
||||
* Add support for splitMessages and timePerChar in Integrations
|
||||
* Audio Converter via API
|
||||
* Send PTV messages with Baileys
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -19,6 +116,8 @@
|
||||
* Add indexes to improve performance in Evolution
|
||||
* Add logical or permanent message deletion based on env config
|
||||
* Add support for fetching multiple instances by key
|
||||
* Update instance.controller.ts to filter by instanceName
|
||||
* Receive template button reply message
|
||||
|
||||
# 2.1.2 (2024-10-06 10:09)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if [ "$DOCKER_ENV" != "true" ]; then
|
||||
export_env_vars
|
||||
fi
|
||||
|
||||
if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" ]]; then
|
||||
if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then
|
||||
export DATABASE_URL
|
||||
echo "Deploying migrations for $DATABASE_PROVIDER"
|
||||
echo "Database URL: $DATABASE_URL"
|
||||
|
||||
@@ -6,7 +6,7 @@ if [ "$DOCKER_ENV" != "true" ]; then
|
||||
export_env_vars
|
||||
fi
|
||||
|
||||
if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" ]]; then
|
||||
if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then
|
||||
export DATABASE_URL
|
||||
echo "Generating database for $DATABASE_PROVIDER"
|
||||
echo "Database URL: $DATABASE_URL"
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
evolution_v2:
|
||||
image: atendai/evolution-api:v2.1.2
|
||||
image: evoapicloud/evolution-api:v2.3.1
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
networks:
|
||||
@@ -34,6 +34,7 @@ services:
|
||||
- RABBITMQ_EVENTS_MESSAGES_UPDATE=false
|
||||
- RABBITMQ_EVENTS_MESSAGES_DELETE=false
|
||||
- RABBITMQ_EVENTS_SEND_MESSAGE=false
|
||||
- RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_SET=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_UPSERT=false
|
||||
- RABBITMQ_EVENTS_CONTACTS_UPDATE=false
|
||||
@@ -71,6 +72,7 @@ services:
|
||||
- WEBHOOK_EVENTS_MESSAGES_UPDATE=true
|
||||
- WEBHOOK_EVENTS_MESSAGES_DELETE=true
|
||||
- WEBHOOK_EVENTS_SEND_MESSAGE=true
|
||||
- WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_SET=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_UPSERT=true
|
||||
- WEBHOOK_EVENTS_CONTACTS_UPDATE=true
|
||||
@@ -92,7 +94,6 @@ services:
|
||||
- WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||
- CONFIG_SESSION_PHONE_CLIENT=Evolution API V2
|
||||
- CONFIG_SESSION_PHONE_NAME=Chrome
|
||||
- CONFIG_SESSION_PHONE_VERSION=2.3000.1015901307
|
||||
- QRCODE_LIMIT=30
|
||||
- OPENAI_ENABLED=true
|
||||
- DIFY_ENABLED=true
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@@ -1,17 +1,19 @@
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN apk update && \
|
||||
apk add git ffmpeg wget curl bash
|
||||
apk add --no-cache git ffmpeg wget curl bash openssl
|
||||
|
||||
LABEL version="2.2.0" description="Api to control whatsapp features through http requests."
|
||||
LABEL version="2.3.1" description="Api to control whatsapp features through http requests."
|
||||
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
|
||||
LABEL contact="contato@atendai.com"
|
||||
LABEL contact="contato@evolution-api.com"
|
||||
|
||||
WORKDIR /evolution
|
||||
|
||||
COPY ./package.json ./tsconfig.json ./
|
||||
COPY ./package*.json ./
|
||||
COPY ./tsconfig.json ./
|
||||
COPY ./tsup.config.ts ./
|
||||
|
||||
RUN npm install -f
|
||||
RUN npm ci --silent
|
||||
|
||||
COPY ./src ./src
|
||||
COPY ./public ./public
|
||||
@@ -19,7 +21,6 @@ COPY ./prisma ./prisma
|
||||
COPY ./manager ./manager
|
||||
COPY ./.env.example ./.env
|
||||
COPY ./runWithProvider.js ./
|
||||
COPY ./tsup.config.ts ./
|
||||
|
||||
COPY ./Docker ./Docker
|
||||
|
||||
@@ -32,9 +33,10 @@ RUN npm run build
|
||||
FROM node:20-alpine AS final
|
||||
|
||||
RUN apk update && \
|
||||
apk add tzdata ffmpeg bash
|
||||
apk add tzdata ffmpeg bash openssl
|
||||
|
||||
ENV TZ=America/Sao_Paulo
|
||||
ENV DOCKER_ENV=true
|
||||
|
||||
WORKDIR /evolution
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -8,7 +8,7 @@ a. LOGO and copyright information: In the process of using Evolution API's front
|
||||
|
||||
b. Usage Notification Requirement: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
|
||||
|
||||
Please contact contato@atendai.com to inquire about licensing matters.
|
||||
Please contact contato@evolution-api.com to inquire about licensing matters.
|
||||
|
||||
2. As a contributor, you should agree that:
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -2,6 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://hub.docker.com/r/evoapicloud/evolution-api)
|
||||
[](https://evolution-api.com/whatsapp)
|
||||
[](https://evolution-api.com/discord)
|
||||
[](https://evolution-api.com/postman)
|
||||
@@ -75,10 +76,6 @@ To continuously improve our services, we have implemented telemetry that collect
|
||||
Join our Evolution Pro community for expert support and a weekly call to answer questions. Visit the link below to learn more and subscribe:
|
||||
|
||||
[Click here to learn more](https://evolution-api.com/suporte-pro)
|
||||
<br>
|
||||
<a href="https://evolution-api.com/suporte-pro">
|
||||
<img src="./public/images/evolution-pro.png" alt="Subscribe" width="600">
|
||||
</a>
|
||||
|
||||
# Donate to the project.
|
||||
|
||||
@@ -91,6 +88,7 @@ https://github.com/sponsors/EvolutionAPI
|
||||
We are proud to collaborate with the following content creators who have contributed valuable insights and tutorials about Evolution API:
|
||||
|
||||
- [Promovaweb](https://www.youtube.com/@promovaweb)
|
||||
- [Sandeco](https://www.youtube.com/@canalsandeco)
|
||||
- [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG)
|
||||
- [Francis MNO](https://www.youtube.com/@FrancisMNO)
|
||||
- [Pablo Cabral](https://youtube.com/@pablocabral)
|
||||
@@ -115,8 +113,8 @@ Evolution API is licensed under the Apache License 2.0, with the following addit
|
||||
|
||||
2. **Usage Notification Requirement**: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
|
||||
|
||||
Please contact contato@atendai.com to inquire about licensing matters.
|
||||
Please contact contato@evolution-api.com to inquire about licensing matters.
|
||||
|
||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
© 2024 Evolution API
|
||||
© 2025 Evolution API
|
||||
|
||||
@@ -1,24 +1,75 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
container_name: evolution_api
|
||||
image: atendai/evolution-api:v2.0.9-rc
|
||||
image: evoapicloud/evolution-api:latest
|
||||
restart: always
|
||||
depends_on:
|
||||
- redis
|
||||
- evolution-postgres
|
||||
ports:
|
||||
- 8080:8080
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
- evolution_instances:/evolution/instances
|
||||
networks:
|
||||
- evolution-net
|
||||
- dokploy-network
|
||||
env_file:
|
||||
- .env
|
||||
expose:
|
||||
- 8080
|
||||
- "8080"
|
||||
|
||||
redis:
|
||||
container_name: evolution_redis
|
||||
image: redis:latest
|
||||
restart: always
|
||||
command: >
|
||||
redis-server --port 6379 --appendonly yes
|
||||
volumes:
|
||||
- evolution_redis:/data
|
||||
networks:
|
||||
evolution-net:
|
||||
aliases:
|
||||
- evolution-redis
|
||||
dokploy-network:
|
||||
aliases:
|
||||
- evolution-redis
|
||||
expose:
|
||||
- "6379"
|
||||
|
||||
evolution-postgres:
|
||||
container_name: evolution_postgres
|
||||
image: postgres:15
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- max_connections=1000
|
||||
- -c
|
||||
- listen_addresses=*
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DATABASE}
|
||||
- POSTGRES_USER=${POSTGRES_USERNAME}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- evolution-net
|
||||
- dokploy-network
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
volumes:
|
||||
evolution_instances:
|
||||
|
||||
evolution_redis:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
evolution-net:
|
||||
name: evolution-net
|
||||
driver: bridge
|
||||
dokploy-network:
|
||||
external: true
|
||||
150
local_install.sh
Executable file
150
local_install.sh
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Definir cores para melhor legibilidade
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Função para log
|
||||
log() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
# Verificar se está rodando como root
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
log_error "Este script não deve ser executado como root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verificar sistema operacional
|
||||
OS="$(uname -s)"
|
||||
case "${OS}" in
|
||||
Linux*)
|
||||
if [ ! -x "$(command -v curl)" ]; then
|
||||
log_warning "Curl não está instalado. Tentando instalar..."
|
||||
if [ -x "$(command -v apt-get)" ]; then
|
||||
sudo apt-get update && sudo apt-get install -y curl
|
||||
elif [ -x "$(command -v yum)" ]; then
|
||||
sudo yum install -y curl
|
||||
else
|
||||
log_error "Não foi possível instalar curl automaticamente. Por favor, instale manualmente."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
Darwin*)
|
||||
if [ ! -x "$(command -v curl)" ]; then
|
||||
log_error "Curl não está instalado. Por favor, instale o Xcode Command Line Tools."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
log_error "Sistema operacional não suportado: ${OS}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verificar conexão com a internet antes de prosseguir
|
||||
if ! ping -c 1 8.8.8.8 &> /dev/null; then
|
||||
log_error "Sem conexão com a internet. Por favor, verifique sua conexão."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Adicionar verificação de espaço em disco
|
||||
REQUIRED_SPACE=1000000 # 1GB em KB
|
||||
AVAILABLE_SPACE=$(df -k . | awk 'NR==2 {print $4}')
|
||||
if [ $AVAILABLE_SPACE -lt $REQUIRED_SPACE ]; then
|
||||
log_error "Espaço em disco insuficiente. Necessário pelo menos 1GB livre."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Adicionar tratamento de erro para comandos npm
|
||||
npm_install_with_retry() {
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
log "Tentativa $attempt de $max_attempts para npm install"
|
||||
if npm install; then
|
||||
return 0
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
[ $attempt -le $max_attempts ] && log_warning "Falha na instalação. Tentando novamente em 5 segundos..." && sleep 5
|
||||
done
|
||||
|
||||
log_error "Falha ao executar npm install após $max_attempts tentativas"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Adicionar timeout para comandos
|
||||
execute_with_timeout() {
|
||||
timeout 300 $@ || log_error "Comando excedeu o tempo limite de 5 minutos: $@"
|
||||
}
|
||||
|
||||
# Verificar se o NVM já está instalado
|
||||
if [ -d "$HOME/.nvm" ]; then
|
||||
log "NVM já está instalado."
|
||||
else
|
||||
log "Instalando NVM..."
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
fi
|
||||
|
||||
# Carregar o NVM no ambiente atual
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||
|
||||
# Verificar se a versão do Node.js já está instalada
|
||||
if command -v node >/dev/null 2>&1 && [ "$(node -v)" = "v20.10.0" ]; then
|
||||
log "Node.js v20.10.0 já está instalado."
|
||||
else
|
||||
log "Instalando Node.js v20.10.0..."
|
||||
nvm install v20.10.0
|
||||
fi
|
||||
|
||||
nvm use v20.10.0
|
||||
|
||||
# Verificar as versões instaladas
|
||||
log "Verificando as versões instaladas:"
|
||||
log "Node.js: $(node -v)"
|
||||
log "npm: $(npm -v)"
|
||||
|
||||
# Instala dependências do projeto
|
||||
log "Instalando dependências do projeto..."
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
|
||||
# Deploy do banco de dados
|
||||
log "Deploy do banco de dados..."
|
||||
npm run db:generate
|
||||
npm run db:deploy
|
||||
|
||||
# Iniciar o projeto
|
||||
log "Iniciando o projeto..."
|
||||
if [ "$1" = "-dev" ]; then
|
||||
npm run dev:server
|
||||
else
|
||||
npm run build
|
||||
npm run start:prod
|
||||
fi
|
||||
|
||||
log "Instalação concluída com sucesso!"
|
||||
|
||||
# Criar arquivo de log
|
||||
LOGFILE="./installation_log_$(date +%Y%m%d_%H%M%S).log"
|
||||
exec 1> >(tee -a "$LOGFILE")
|
||||
exec 2>&1
|
||||
|
||||
# Adicionar trap para limpeza em caso de interrupção
|
||||
cleanup() {
|
||||
log "Limpando recursos temporários..."
|
||||
# Adicione comandos de limpeza aqui
|
||||
}
|
||||
trap cleanup EXIT
|
||||
381
manager/dist/assets/index-CFAZX6IV.js
vendored
381
manager/dist/assets/index-CFAZX6IV.js
vendored
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-CXH2BdD4.css
vendored
Normal file
1
manager/dist/assets/index-CXH2BdD4.css
vendored
Normal file
File diff suppressed because one or more lines are too long
381
manager/dist/assets/index-D-oOjDYe.js
vendored
Normal file
381
manager/dist/assets/index-D-oOjDYe.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
manager/dist/assets/index-DNOCacL_.css
vendored
1
manager/dist/assets/index-DNOCacL_.css
vendored
File diff suppressed because one or more lines are too long
6
manager/dist/index.html
vendored
6
manager/dist/index.html
vendored
@@ -2,11 +2,11 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/assets/images/evolution-logo.png" />
|
||||
<link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Evolution Manager</title>
|
||||
<script type="module" crossorigin src="/assets/index-CFAZX6IV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DNOCacL_.css">
|
||||
<script type="module" crossorigin src="/assets/index-D-oOjDYe.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CXH2BdD4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
12231
package-lock.json
generated
Normal file
12231
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
110
package.json
110
package.json
@@ -1,20 +1,23 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.2",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "./dist/main.js",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && tsup",
|
||||
"start": "tsnd -r tsconfig-paths/register --files --transpile-only ./src/main.ts",
|
||||
"start": "tsx ./src/main.ts",
|
||||
"start:prod": "node dist/main",
|
||||
"dev:server": "clear && tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
|
||||
"test": "clear && tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
|
||||
"dev:server": "tsx watch ./src/main.ts",
|
||||
"test": "tsx watch ./test/all.test.ts",
|
||||
"lint": "eslint --fix --ext .ts src",
|
||||
"lint:check": "eslint --ext .ts src",
|
||||
"db:generate": "node runWithProvider.js \"npx prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
|
||||
"db:deploy": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate deploy --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
|
||||
"db:deploy:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate deploy --schema prisma\\DATABASE_PROVIDER-schema.prisma\"",
|
||||
"db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
|
||||
"db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\""
|
||||
"db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"",
|
||||
"db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -38,7 +41,7 @@
|
||||
],
|
||||
"author": {
|
||||
"name": "Davidson Gomes",
|
||||
"email": "contato@atendai.com"
|
||||
"email": "contato@evolution-api.com"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
@@ -47,71 +50,82 @@
|
||||
"homepage": "https://github.com/EvolutionAPI/evolution-api#readme",
|
||||
"dependencies": {
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@aws-sdk/client-sqs": "^3.569.0",
|
||||
"@aws-sdk/client-sqs": "^3.723.0",
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
"@figuro/chatwoot-sdk": "^1.1.16",
|
||||
"@hapi/boom": "^10.0.1",
|
||||
"@prisma/client": "^5.15.0",
|
||||
"@sentry/node": "^8.28.0",
|
||||
"amqplib": "^0.10.3",
|
||||
"axios": "^1.6.5",
|
||||
"baileys": "github:EvolutionAPI/Baileys",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"@sentry/node": "^8.47.0",
|
||||
"amqplib": "^0.10.5",
|
||||
"audio-decode": "^2.2.3",
|
||||
"axios": "^1.7.9",
|
||||
"baileys": "github:WhiskeySockets/Baileys",
|
||||
"class-validator": "^0.14.1",
|
||||
"compression": "^1.7.4",
|
||||
"compression": "^1.7.5",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"express": "^4.18.2",
|
||||
"express": "^4.21.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"form-data": "^4.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"form-data": "^4.0.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"i18next": "^23.7.19",
|
||||
"jimp": "^0.16.13",
|
||||
"jimp": "^1.6.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsonschema": "^1.4.1",
|
||||
"link-preview-js": "^3.0.4",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"link-preview-js": "^3.0.13",
|
||||
"long": "^5.2.3",
|
||||
"mime": "^3.0.0",
|
||||
"minio": "^8.0.1",
|
||||
"mediainfo.js": "^0.3.4",
|
||||
"mime": "^4.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"minio": "^8.0.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nats": "^2.29.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^4.52.7",
|
||||
"pg": "^8.11.3",
|
||||
"openai": "^4.77.3",
|
||||
"pg": "^8.13.1",
|
||||
"pino": "^8.11.0",
|
||||
"prisma": "^5.15.0",
|
||||
"prisma": "^6.1.0",
|
||||
"pusher": "^5.2.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"redis": "^4.6.5",
|
||||
"sharp": "^0.32.2",
|
||||
"socket.io": "^4.7.1",
|
||||
"tsup": "^8.2.4",
|
||||
"uuid": "^9.0.0"
|
||||
"redis": "^4.7.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"sharp": "^0.34.2",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tsup": "^8.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/mime": "3.0.0",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/mime": "^4.0.0",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/qrcode-terminal": "^0.12.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.5.4"
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `disconnectionAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Media` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Session` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `Chat` ADD COLUMN `unreadMessages` INTEGER NOT NULL DEFAULT 0,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Chatwoot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Contact` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Dify` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `DifySetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `EvolutionBot` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `EvolutionBotSetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Flowise` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `FlowiseSetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Instance` MODIFY `disconnectionAt` TIMESTAMP NULL,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `IsOnWhatsapp` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Label` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Media` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Message` MODIFY `status` VARCHAR(30) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiBot` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiCreds` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiSetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Proxy` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Rabbitmq` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Session` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Setting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Sqs` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Template` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Typebot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `TypebotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Webhook` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Websocket` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Pusher` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT false,
|
||||
`appId` VARCHAR(100) NOT NULL,
|
||||
`key` VARCHAR(100) NOT NULL,
|
||||
`secret` VARCHAR(100) NOT NULL,
|
||||
`cluster` VARCHAR(100) NOT NULL,
|
||||
`useTLS` BOOLEAN NOT NULL DEFAULT false,
|
||||
`events` JSON NOT NULL,
|
||||
`createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `Pusher_instanceId_key`(`instanceId`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `Chat_remoteJid_idx` ON `Chat`(`remoteJid`);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `Contact_remoteJid_idx` ON `Contact`(`remoteJid`);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `Setting_instanceId_idx` ON `Setting`(`instanceId`);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `Webhook_instanceId_idx` ON `Webhook`(`instanceId`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Pusher` ADD CONSTRAINT `Pusher_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `Chat` RENAME INDEX `Chat_instanceId_fkey` TO `Chat_instanceId_idx`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `Contact` RENAME INDEX `Contact_instanceId_fkey` TO `Contact_instanceId_idx`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `Message` RENAME INDEX `Message_instanceId_fkey` TO `Message_instanceId_idx`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `MessageUpdate` RENAME INDEX `MessageUpdate_instanceId_fkey` TO `MessageUpdate_instanceId_idx`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `MessageUpdate` RENAME INDEX `MessageUpdate_messageId_fkey` TO `MessageUpdate_messageId_idx`;
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `disconnectionAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Media` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Session` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `createdAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- You are about to alter the column `updatedAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
|
||||
- A unique constraint covering the columns `[instanceId,remoteJid]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `Chat` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Chatwoot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Contact` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Dify` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `DifySetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `EvolutionBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `EvolutionBotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Flowise` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `FlowiseSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Instance` MODIFY `disconnectionAt` TIMESTAMP NULL,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `IsOnWhatsapp` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Label` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Media` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiCreds` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `OpenaiSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Proxy` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Pusher` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Rabbitmq` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Session` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Setting` ADD COLUMN `wavoipToken` VARCHAR(100) NULL,
|
||||
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Sqs` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Template` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Typebot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `TypebotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Webhook` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Websocket` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updatedAt` TIMESTAMP NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `Chat_instanceId_remoteJid_key` ON `Chat`(`instanceId`, `remoteJid`);
|
||||
@@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `Nats` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT false,
|
||||
`events` JSON NOT NULL,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `Nats_instanceId_key` ON `Nats`(`instanceId`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Nats` ADD CONSTRAINT `Nats_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
|
||||
-- AlterTable
|
||||
SET @column_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'Setting'
|
||||
AND column_name = 'wavoipToken'
|
||||
);
|
||||
|
||||
SET @sql := IF(@column_exists = 0,
|
||||
'ALTER TABLE Setting ADD COLUMN wavoipToken VARCHAR(100);',
|
||||
'SELECT "Column already exists";'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
ALTER TABLE Chat ADD CONSTRAINT unique_remote_instance UNIQUE (remoteJid, instanceId);
|
||||
@@ -0,0 +1,62 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `N8n` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT true,
|
||||
`description` VARCHAR(255),
|
||||
`webhookUrl` VARCHAR(255),
|
||||
`basicAuthUser` VARCHAR(255),
|
||||
`basicAuthPass` VARCHAR(255),
|
||||
`expire` INTEGER DEFAULT 0,
|
||||
`keywordFinish` VARCHAR(100),
|
||||
`delayMessage` INTEGER,
|
||||
`unknownMessage` VARCHAR(100),
|
||||
`listeningFromMe` BOOLEAN DEFAULT false,
|
||||
`stopBotFromMe` BOOLEAN DEFAULT false,
|
||||
`keepOpen` BOOLEAN DEFAULT false,
|
||||
`debounceTime` INTEGER,
|
||||
`ignoreJids` JSON,
|
||||
`splitMessages` BOOLEAN DEFAULT false,
|
||||
`timePerChar` INTEGER DEFAULT 50,
|
||||
`triggerType` ENUM('all', 'keyword', 'none') NULL,
|
||||
`triggerOperator` ENUM('contains', 'equals', 'startsWith', 'endsWith', 'regex') NULL,
|
||||
`triggerValue` VARCHAR(191) NULL,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `N8nSetting` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`expire` INTEGER DEFAULT 0,
|
||||
`keywordFinish` VARCHAR(100),
|
||||
`delayMessage` INTEGER,
|
||||
`unknownMessage` VARCHAR(100),
|
||||
`listeningFromMe` BOOLEAN DEFAULT false,
|
||||
`stopBotFromMe` BOOLEAN DEFAULT false,
|
||||
`keepOpen` BOOLEAN DEFAULT false,
|
||||
`debounceTime` INTEGER,
|
||||
`ignoreJids` JSON,
|
||||
`splitMessages` BOOLEAN DEFAULT false,
|
||||
`timePerChar` INTEGER DEFAULT 50,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`n8nIdFallback` VARCHAR(100),
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `N8nSetting_instanceId_key` ON `N8nSetting`(`instanceId`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `N8n` ADD CONSTRAINT `N8n_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `N8nSetting` ADD CONSTRAINT `N8nSetting_n8nIdFallback_fkey` FOREIGN KEY (`n8nIdFallback`) REFERENCES `N8n`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `N8nSetting` ADD CONSTRAINT `N8nSetting_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,61 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `Evoai` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT true,
|
||||
`description` VARCHAR(255),
|
||||
`agentUrl` VARCHAR(255),
|
||||
`apiKey` VARCHAR(255),
|
||||
`expire` INTEGER DEFAULT 0,
|
||||
`keywordFinish` VARCHAR(100),
|
||||
`delayMessage` INTEGER,
|
||||
`unknownMessage` VARCHAR(100),
|
||||
`listeningFromMe` BOOLEAN DEFAULT false,
|
||||
`stopBotFromMe` BOOLEAN DEFAULT false,
|
||||
`keepOpen` BOOLEAN DEFAULT false,
|
||||
`debounceTime` INTEGER,
|
||||
`ignoreJids` JSON,
|
||||
`splitMessages` BOOLEAN DEFAULT false,
|
||||
`timePerChar` INTEGER DEFAULT 50,
|
||||
`triggerType` ENUM('all', 'keyword', 'none') NULL,
|
||||
`triggerOperator` ENUM('contains', 'equals', 'startsWith', 'endsWith', 'regex') NULL,
|
||||
`triggerValue` VARCHAR(191) NULL,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `EvoaiSetting` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`expire` INTEGER DEFAULT 0,
|
||||
`keywordFinish` VARCHAR(100),
|
||||
`delayMessage` INTEGER,
|
||||
`unknownMessage` VARCHAR(100),
|
||||
`listeningFromMe` BOOLEAN DEFAULT false,
|
||||
`stopBotFromMe` BOOLEAN DEFAULT false,
|
||||
`keepOpen` BOOLEAN DEFAULT false,
|
||||
`debounceTime` INTEGER,
|
||||
`ignoreJids` JSON,
|
||||
`splitMessages` BOOLEAN DEFAULT false,
|
||||
`timePerChar` INTEGER DEFAULT 50,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP NOT NULL,
|
||||
`evoaiIdFallback` VARCHAR(100),
|
||||
`instanceId` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX `EvoaiSetting_instanceId_key` ON `EvoaiSetting`(`instanceId`);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Evoai` ADD CONSTRAINT `Evoai_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `EvoaiSetting` ADD CONSTRAINT `EvoaiSetting_evoaiIdFallback_fkey` FOREIGN KEY (`evoaiIdFallback`) REFERENCES `Evoai`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `EvoaiSetting` ADD CONSTRAINT `EvoaiSetting_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
ALTER TABLE `Media` DROP INDEX `Media_fileName_key`;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `Typebot` ADD COLUMN `splitMessages` BOOLEAN DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER DEFAULT 50;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `TypebotSetting` ADD COLUMN `splitMessages` BOOLEAN DEFAULT false,
|
||||
ADD COLUMN `timePerChar` INTEGER DEFAULT 50;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `IsOnWhatsapp` ADD COLUMN `lid` VARCHAR(100);
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "mysql"
|
||||
@@ -86,6 +86,7 @@ model Instance {
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
Rabbitmq Rabbitmq?
|
||||
Nats Nats?
|
||||
Sqs Sqs?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
@@ -99,12 +100,13 @@ model Instance {
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
integrationSessions IntegrationSession[]
|
||||
IntegrationSession IntegrationSession[]
|
||||
EvolutionBot EvolutionBot[]
|
||||
EvolutionBotSetting EvolutionBotSetting?
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
@@ -125,6 +127,8 @@ model Chat {
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@unique([instanceId, remoteJid])
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
}
|
||||
@@ -169,6 +173,7 @@ model Message {
|
||||
|
||||
sessionId String?
|
||||
session IntegrationSession? @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@@ -184,6 +189,7 @@ model MessageUpdate {
|
||||
messageId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([messageId])
|
||||
}
|
||||
@@ -200,6 +206,7 @@ model Webhook {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@@ -263,10 +270,12 @@ model Setting {
|
||||
readMessages Boolean @default(false)
|
||||
readStatus Boolean @default(false)
|
||||
syncFullHistory Boolean @default(false)
|
||||
wavoipToken String? @db.VarChar(100)
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@@ -280,6 +289,16 @@ model Rabbitmq {
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Nats {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
events Json @db.Json
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Sqs {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false)
|
||||
@@ -380,7 +399,7 @@ model IntegrationSession {
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @unique @db.VarChar(500)
|
||||
fileName String @db.VarChar(500)
|
||||
type String @db.VarChar(100)
|
||||
mimetype String @db.VarChar(100)
|
||||
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
@@ -625,3 +644,100 @@ model IsOnWhatsapp {
|
||||
createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
}
|
||||
|
||||
model N8n {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.TinyInt(1)
|
||||
description String? @db.VarChar(255)
|
||||
webhookUrl String? @db.VarChar(255)
|
||||
basicAuthUser String? @db.VarChar(255)
|
||||
basicAuthPass String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Int
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Int
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false)
|
||||
stopBotFromMe Boolean? @default(false)
|
||||
keepOpen Boolean? @default(false)
|
||||
debounceTime Int? @db.Int
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false)
|
||||
timePerChar Int? @default(50) @db.Int
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
N8nSetting N8nSetting[]
|
||||
}
|
||||
|
||||
model N8nSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Int
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Int
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false)
|
||||
stopBotFromMe Boolean? @default(false)
|
||||
keepOpen Boolean? @default(false)
|
||||
debounceTime Int? @db.Int
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false)
|
||||
timePerChar Int? @default(50) @db.Int
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
||||
n8nIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Evoai {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.TinyInt(1)
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Int
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Int
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false)
|
||||
stopBotFromMe Boolean? @default(false)
|
||||
keepOpen Boolean? @default(false)
|
||||
debounceTime Int? @db.Int
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false)
|
||||
timePerChar Int? @default(50) @db.Int
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvoaiSetting EvoaiSetting[]
|
||||
}
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Int
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Int
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false)
|
||||
stopBotFromMe Boolean? @default(false)
|
||||
keepOpen Boolean? @default(false)
|
||||
debounceTime Int? @db.Int
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false)
|
||||
timePerChar Int? @default(50) @db.Int
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Setting" ADD COLUMN IF NOT EXISTS "wavoipToken" VARCHAR(100);
|
||||
@@ -0,0 +1,17 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Nats" (
|
||||
"id" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"events" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Nats_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Nats_instanceId_key" ON "Nats"("instanceId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Nats" ADD CONSTRAINT "Nats_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,62 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "N8n" (
|
||||
"id" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"description" VARCHAR(255),
|
||||
"webhookUrl" VARCHAR(255),
|
||||
"basicAuthUser" VARCHAR(255),
|
||||
"basicAuthPass" VARCHAR(255),
|
||||
"expire" INTEGER DEFAULT 0,
|
||||
"keywordFinish" VARCHAR(100),
|
||||
"delayMessage" INTEGER,
|
||||
"unknownMessage" VARCHAR(100),
|
||||
"listeningFromMe" BOOLEAN DEFAULT false,
|
||||
"stopBotFromMe" BOOLEAN DEFAULT false,
|
||||
"keepOpen" BOOLEAN DEFAULT false,
|
||||
"debounceTime" INTEGER,
|
||||
"ignoreJids" JSONB,
|
||||
"splitMessages" BOOLEAN DEFAULT false,
|
||||
"timePerChar" INTEGER DEFAULT 50,
|
||||
"triggerType" "TriggerType",
|
||||
"triggerOperator" "TriggerOperator",
|
||||
"triggerValue" TEXT,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "N8n_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "N8nSetting" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expire" INTEGER DEFAULT 0,
|
||||
"keywordFinish" VARCHAR(100),
|
||||
"delayMessage" INTEGER,
|
||||
"unknownMessage" VARCHAR(100),
|
||||
"listeningFromMe" BOOLEAN DEFAULT false,
|
||||
"stopBotFromMe" BOOLEAN DEFAULT false,
|
||||
"keepOpen" BOOLEAN DEFAULT false,
|
||||
"debounceTime" INTEGER,
|
||||
"ignoreJids" JSONB,
|
||||
"splitMessages" BOOLEAN DEFAULT false,
|
||||
"timePerChar" INTEGER DEFAULT 50,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"n8nIdFallback" VARCHAR(100),
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "N8nSetting_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "N8nSetting_instanceId_key" ON "N8nSetting"("instanceId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "N8n" ADD CONSTRAINT "N8n_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "N8nSetting" ADD CONSTRAINT "N8nSetting_n8nIdFallback_fkey" FOREIGN KEY ("n8nIdFallback") REFERENCES "N8n"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "N8nSetting" ADD CONSTRAINT "N8nSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,61 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Evoai" (
|
||||
"id" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"description" VARCHAR(255),
|
||||
"agentUrl" VARCHAR(255),
|
||||
"apiKey" VARCHAR(255),
|
||||
"expire" INTEGER DEFAULT 0,
|
||||
"keywordFinish" VARCHAR(100),
|
||||
"delayMessage" INTEGER,
|
||||
"unknownMessage" VARCHAR(100),
|
||||
"listeningFromMe" BOOLEAN DEFAULT false,
|
||||
"stopBotFromMe" BOOLEAN DEFAULT false,
|
||||
"keepOpen" BOOLEAN DEFAULT false,
|
||||
"debounceTime" INTEGER,
|
||||
"ignoreJids" JSONB,
|
||||
"splitMessages" BOOLEAN DEFAULT false,
|
||||
"timePerChar" INTEGER DEFAULT 50,
|
||||
"triggerType" "TriggerType",
|
||||
"triggerOperator" "TriggerOperator",
|
||||
"triggerValue" TEXT,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Evoai_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "EvoaiSetting" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expire" INTEGER DEFAULT 0,
|
||||
"keywordFinish" VARCHAR(100),
|
||||
"delayMessage" INTEGER,
|
||||
"unknownMessage" VARCHAR(100),
|
||||
"listeningFromMe" BOOLEAN DEFAULT false,
|
||||
"stopBotFromMe" BOOLEAN DEFAULT false,
|
||||
"keepOpen" BOOLEAN DEFAULT false,
|
||||
"debounceTime" INTEGER,
|
||||
"ignoreJids" JSONB,
|
||||
"splitMessages" BOOLEAN DEFAULT false,
|
||||
"timePerChar" INTEGER DEFAULT 50,
|
||||
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP NOT NULL,
|
||||
"evoaiIdFallback" VARCHAR(100),
|
||||
"instanceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "EvoaiSetting_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "EvoaiSetting_instanceId_key" ON "EvoaiSetting"("instanceId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Evoai" ADD CONSTRAINT "Evoai_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "EvoaiSetting" ADD CONSTRAINT "EvoaiSetting_evoaiIdFallback_fkey" FOREIGN KEY ("evoaiIdFallback") REFERENCES "Evoai"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "EvoaiSetting" ADD CONSTRAINT "EvoaiSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Media_fileName_key";
|
||||
@@ -0,0 +1,7 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Typebot" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false,
|
||||
ADD COLUMN "timePerChar" INTEGER DEFAULT 50;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TypebotSetting" ADD COLUMN "splitMessages" BOOLEAN DEFAULT false,
|
||||
ADD COLUMN "timePerChar" INTEGER DEFAULT 50;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "IsOnWhatsapp" ADD COLUMN "lid" VARCHAR(100);
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -86,6 +86,7 @@ model Instance {
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
Rabbitmq Rabbitmq?
|
||||
Nats Nats?
|
||||
Sqs Sqs?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
@@ -99,12 +100,16 @@ model Instance {
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
integrationSessions IntegrationSession[]
|
||||
IntegrationSession IntegrationSession[]
|
||||
EvolutionBot EvolutionBot[]
|
||||
EvolutionBotSetting EvolutionBotSetting?
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
N8nSetting N8nSetting[]
|
||||
Evoai Evoai[]
|
||||
EvoaiSetting EvoaiSetting?
|
||||
}
|
||||
|
||||
model Session {
|
||||
@@ -125,6 +130,7 @@ model Chat {
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
}
|
||||
@@ -168,6 +174,7 @@ model Message {
|
||||
|
||||
sessionId String?
|
||||
session IntegrationSession? @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@@ -183,6 +190,7 @@ model MessageUpdate {
|
||||
messageId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([messageId])
|
||||
}
|
||||
@@ -199,6 +207,7 @@ model Webhook {
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@@ -264,10 +273,12 @@ model Setting {
|
||||
readMessages Boolean @default(false) @db.Boolean
|
||||
readStatus Boolean @default(false) @db.Boolean
|
||||
syncFullHistory Boolean @default(false) @db.Boolean
|
||||
wavoipToken String? @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
@@ -281,6 +292,16 @@ model Rabbitmq {
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Nats {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Sqs {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
@@ -336,6 +357,8 @@ model Typebot {
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
TypebotSetting TypebotSetting[]
|
||||
@@ -353,6 +376,8 @@ model TypebotSetting {
|
||||
debounceTime Int? @db.Integer
|
||||
typebotIdFallback String? @db.VarChar(100)
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
|
||||
@@ -362,7 +387,7 @@ model TypebotSetting {
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @unique @db.VarChar(500)
|
||||
fileName String @db.VarChar(500)
|
||||
type String @db.VarChar(100)
|
||||
mimetype String @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Date
|
||||
@@ -623,6 +648,104 @@ model IsOnWhatsapp {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @unique @db.VarChar(100)
|
||||
jidOptions String
|
||||
lid String? @db.VarChar(100)
|
||||
createdAt DateTime @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
}
|
||||
|
||||
model N8n {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
webhookUrl String? @db.VarChar(255)
|
||||
basicAuthUser String? @db.VarChar(255)
|
||||
basicAuthPass String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
N8nSetting N8nSetting[]
|
||||
}
|
||||
|
||||
model N8nSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
||||
n8nIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Evoai {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvoaiSetting EvoaiSetting[]
|
||||
}
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
752
prisma/psql_bouncer-schema.prisma
Normal file
752
prisma/psql_bouncer-schema.prisma
Normal file
@@ -0,0 +1,752 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_BOUNCER_CONNECTION_URI")
|
||||
directUrl = env("DATABASE_CONNECTION_URI")
|
||||
}
|
||||
|
||||
enum InstanceConnectionStatus {
|
||||
open
|
||||
close
|
||||
connecting
|
||||
}
|
||||
|
||||
enum DeviceMessage {
|
||||
ios
|
||||
android
|
||||
web
|
||||
unknown
|
||||
desktop
|
||||
}
|
||||
|
||||
enum SessionStatus {
|
||||
opened
|
||||
closed
|
||||
paused
|
||||
}
|
||||
|
||||
enum TriggerType {
|
||||
all
|
||||
keyword
|
||||
none
|
||||
advanced
|
||||
}
|
||||
|
||||
enum TriggerOperator {
|
||||
contains
|
||||
equals
|
||||
startsWith
|
||||
endsWith
|
||||
regex
|
||||
}
|
||||
|
||||
enum OpenaiBotType {
|
||||
assistant
|
||||
chatCompletion
|
||||
}
|
||||
|
||||
enum DifyBotType {
|
||||
chatBot
|
||||
textGenerator
|
||||
agent
|
||||
workflow
|
||||
}
|
||||
|
||||
model Instance {
|
||||
id String @id @default(cuid())
|
||||
name String @unique @db.VarChar(255)
|
||||
connectionStatus InstanceConnectionStatus @default(open)
|
||||
ownerJid String? @db.VarChar(100)
|
||||
profileName String? @db.VarChar(100)
|
||||
profilePicUrl String? @db.VarChar(500)
|
||||
integration String? @db.VarChar(100)
|
||||
number String? @db.VarChar(100)
|
||||
businessId String? @db.VarChar(100)
|
||||
token String? @db.VarChar(255)
|
||||
clientName String? @db.VarChar(100)
|
||||
disconnectionReasonCode Int? @db.Integer
|
||||
disconnectionObject Json? @db.JsonB
|
||||
disconnectionAt DateTime? @db.Timestamp
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Chat Chat[]
|
||||
Contact Contact[]
|
||||
Message Message[]
|
||||
Webhook Webhook?
|
||||
Chatwoot Chatwoot?
|
||||
Label Label[]
|
||||
Proxy Proxy?
|
||||
Setting Setting?
|
||||
Rabbitmq Rabbitmq?
|
||||
Nats Nats?
|
||||
Sqs Sqs?
|
||||
Websocket Websocket?
|
||||
Typebot Typebot[]
|
||||
Session Session?
|
||||
MessageUpdate MessageUpdate[]
|
||||
TypebotSetting TypebotSetting?
|
||||
Media Media[]
|
||||
OpenaiCreds OpenaiCreds[]
|
||||
OpenaiBot OpenaiBot[]
|
||||
OpenaiSetting OpenaiSetting?
|
||||
Template Template[]
|
||||
Dify Dify[]
|
||||
DifySetting DifySetting?
|
||||
IntegrationSession IntegrationSession[]
|
||||
EvolutionBot EvolutionBot[]
|
||||
EvolutionBotSetting EvolutionBotSetting?
|
||||
Flowise Flowise[]
|
||||
FlowiseSetting FlowiseSetting?
|
||||
Pusher Pusher?
|
||||
N8n N8n[]
|
||||
N8nSetting N8nSetting[]
|
||||
Evoai Evoai[]
|
||||
EvoaiSetting EvoaiSetting?
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionId String @unique
|
||||
creds String? @db.Text
|
||||
createdAt DateTime @default(now()) @db.Timestamp
|
||||
Instance Instance @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @db.VarChar(100)
|
||||
name String? @db.VarChar(100)
|
||||
labels Json? @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
unreadMessages Int @default(0)
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([remoteJid])
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @db.VarChar(100)
|
||||
pushName String? @db.VarChar(100)
|
||||
profilePicUrl String? @db.VarChar(500)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@unique([remoteJid, instanceId])
|
||||
@@index([remoteJid])
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id @default(cuid())
|
||||
key Json @db.JsonB
|
||||
pushName String? @db.VarChar(100)
|
||||
participant String? @db.VarChar(100)
|
||||
messageType String @db.VarChar(100)
|
||||
message Json @db.JsonB
|
||||
contextInfo Json? @db.JsonB
|
||||
source DeviceMessage
|
||||
messageTimestamp Int @db.Integer
|
||||
chatwootMessageId Int? @db.Integer
|
||||
chatwootInboxId Int? @db.Integer
|
||||
chatwootConversationId Int? @db.Integer
|
||||
chatwootContactInboxSourceId String? @db.VarChar(100)
|
||||
chatwootIsRead Boolean? @db.Boolean
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
MessageUpdate MessageUpdate[]
|
||||
Media Media?
|
||||
webhookUrl String? @db.VarChar(500)
|
||||
status String? @db.VarChar(30)
|
||||
|
||||
sessionId String?
|
||||
session IntegrationSession? @relation(fields: [sessionId], references: [id])
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model MessageUpdate {
|
||||
id String @id @default(cuid())
|
||||
keyId String @db.VarChar(100)
|
||||
remoteJid String @db.VarChar(100)
|
||||
fromMe Boolean @db.Boolean
|
||||
participant String? @db.VarChar(100)
|
||||
pollUpdates Json? @db.JsonB
|
||||
status String @db.VarChar(30)
|
||||
Message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||
messageId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@index([instanceId])
|
||||
@@index([messageId])
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
id String @id @default(cuid())
|
||||
url String @db.VarChar(500)
|
||||
headers Json? @db.JsonB
|
||||
enabled Boolean? @default(true) @db.Boolean
|
||||
events Json? @db.JsonB
|
||||
webhookByEvents Boolean? @default(false) @db.Boolean
|
||||
webhookBase64 Boolean? @default(false) @db.Boolean
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model Chatwoot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean? @default(true) @db.Boolean
|
||||
accountId String? @db.VarChar(100)
|
||||
token String? @db.VarChar(100)
|
||||
url String? @db.VarChar(500)
|
||||
nameInbox String? @db.VarChar(100)
|
||||
signMsg Boolean? @default(false) @db.Boolean
|
||||
signDelimiter String? @db.VarChar(100)
|
||||
number String? @db.VarChar(100)
|
||||
reopenConversation Boolean? @default(false) @db.Boolean
|
||||
conversationPending Boolean? @default(false) @db.Boolean
|
||||
mergeBrazilContacts Boolean? @default(false) @db.Boolean
|
||||
importContacts Boolean? @default(false) @db.Boolean
|
||||
importMessages Boolean? @default(false) @db.Boolean
|
||||
daysLimitImportMessages Int? @db.Integer
|
||||
organization String? @db.VarChar(100)
|
||||
logo String? @db.VarChar(500)
|
||||
ignoreJids Json?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Label {
|
||||
id String @id @default(cuid())
|
||||
labelId String? @db.VarChar(100)
|
||||
name String @db.VarChar(100)
|
||||
color String @db.VarChar(100)
|
||||
predefinedId String? @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
|
||||
@@unique([labelId, instanceId])
|
||||
}
|
||||
|
||||
model Proxy {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
host String @db.VarChar(100)
|
||||
port String @db.VarChar(100)
|
||||
protocol String @db.VarChar(100)
|
||||
username String @db.VarChar(100)
|
||||
password String @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Setting {
|
||||
id String @id @default(cuid())
|
||||
rejectCall Boolean @default(false) @db.Boolean
|
||||
msgCall String? @db.VarChar(100)
|
||||
groupsIgnore Boolean @default(false) @db.Boolean
|
||||
alwaysOnline Boolean @default(false) @db.Boolean
|
||||
readMessages Boolean @default(false) @db.Boolean
|
||||
readStatus Boolean @default(false) @db.Boolean
|
||||
syncFullHistory Boolean @default(false) @db.Boolean
|
||||
wavoipToken String? @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
|
||||
@@index([instanceId])
|
||||
}
|
||||
|
||||
model Rabbitmq {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Nats {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Sqs {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Websocket {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Pusher {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(false) @db.Boolean
|
||||
appId String @db.VarChar(100)
|
||||
key String @db.VarChar(100)
|
||||
secret String @db.VarChar(100)
|
||||
cluster String @db.VarChar(100)
|
||||
useTLS Boolean @default(false) @db.Boolean
|
||||
events Json @db.JsonB
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Typebot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
url String @db.VarChar(500)
|
||||
typebot String @db.VarChar(100)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime? @updatedAt @db.Timestamp
|
||||
ignoreJids Json?
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
TypebotSetting TypebotSetting[]
|
||||
}
|
||||
|
||||
model TypebotSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
typebotIdFallback String? @db.VarChar(100)
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
fileName String @db.VarChar(500)
|
||||
type String @db.VarChar(100)
|
||||
mimetype String @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Date
|
||||
Message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||
messageId String @unique
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
}
|
||||
|
||||
model OpenaiCreds {
|
||||
id String @id @default(cuid())
|
||||
name String? @unique @db.VarChar(255)
|
||||
apiKey String? @unique @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
OpenaiAssistant OpenaiBot[]
|
||||
OpenaiSetting OpenaiSetting?
|
||||
}
|
||||
|
||||
model OpenaiBot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
botType OpenaiBotType
|
||||
assistantId String? @db.VarChar(255)
|
||||
functionUrl String? @db.VarChar(500)
|
||||
model String? @db.VarChar(100)
|
||||
systemMessages Json? @db.JsonB
|
||||
assistantMessages Json? @db.JsonB
|
||||
userMessages Json? @db.JsonB
|
||||
maxTokens Int? @db.Integer
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
ignoreJids Json?
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade)
|
||||
openaiCredsId String
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
OpenaiSetting OpenaiSetting[]
|
||||
}
|
||||
|
||||
model IntegrationSession {
|
||||
id String @id @default(cuid())
|
||||
sessionId String @db.VarChar(255)
|
||||
remoteJid String @db.VarChar(100)
|
||||
pushName String?
|
||||
status SessionStatus
|
||||
awaitUser Boolean @default(false) @db.Boolean
|
||||
context Json?
|
||||
type String? @db.VarChar(100)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Message Message[]
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
parameters Json? @db.JsonB
|
||||
|
||||
botId String?
|
||||
}
|
||||
|
||||
model OpenaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
speechToText Boolean? @default(false) @db.Boolean
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
OpenaiCreds OpenaiCreds? @relation(fields: [openaiCredsId], references: [id])
|
||||
openaiCredsId String @unique
|
||||
Fallback OpenaiBot? @relation(fields: [openaiIdFallback], references: [id])
|
||||
openaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Template {
|
||||
id String @id @default(cuid())
|
||||
templateId String @unique @db.VarChar(255)
|
||||
name String @unique @db.VarChar(255)
|
||||
template Json @db.JsonB
|
||||
webhookUrl String? @db.VarChar(500)
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
}
|
||||
|
||||
model Dify {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
botType DifyBotType
|
||||
apiUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
DifySetting DifySetting[]
|
||||
}
|
||||
|
||||
model DifySetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Dify? @relation(fields: [difyIdFallback], references: [id])
|
||||
difyIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model EvolutionBot {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
apiUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvolutionBotSetting EvolutionBotSetting[]
|
||||
}
|
||||
|
||||
model EvolutionBotSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback EvolutionBot? @relation(fields: [botIdFallback], references: [id])
|
||||
botIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Flowise {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
apiUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
FlowiseSetting FlowiseSetting[]
|
||||
}
|
||||
|
||||
model FlowiseSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Flowise? @relation(fields: [flowiseIdFallback], references: [id])
|
||||
flowiseIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model IsOnWhatsapp {
|
||||
id String @id @default(cuid())
|
||||
remoteJid String @unique @db.VarChar(100)
|
||||
jidOptions String
|
||||
lid String? @db.VarChar(100)
|
||||
createdAt DateTime @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
}
|
||||
|
||||
model N8n {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
webhookUrl String? @db.VarChar(255)
|
||||
basicAuthUser String? @db.VarChar(255)
|
||||
basicAuthPass String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
N8nSetting N8nSetting[]
|
||||
}
|
||||
|
||||
model N8nSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback N8n? @relation(fields: [n8nIdFallback], references: [id])
|
||||
n8nIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
|
||||
model Evoai {
|
||||
id String @id @default(cuid())
|
||||
enabled Boolean @default(true) @db.Boolean
|
||||
description String? @db.VarChar(255)
|
||||
agentUrl String? @db.VarChar(255)
|
||||
apiKey String? @db.VarChar(255)
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
triggerType TriggerType?
|
||||
triggerOperator TriggerOperator?
|
||||
triggerValue String?
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String
|
||||
EvoaiSetting EvoaiSetting[]
|
||||
}
|
||||
|
||||
model EvoaiSetting {
|
||||
id String @id @default(cuid())
|
||||
expire Int? @default(0) @db.Integer
|
||||
keywordFinish String? @db.VarChar(100)
|
||||
delayMessage Int? @db.Integer
|
||||
unknownMessage String? @db.VarChar(100)
|
||||
listeningFromMe Boolean? @default(false) @db.Boolean
|
||||
stopBotFromMe Boolean? @default(false) @db.Boolean
|
||||
keepOpen Boolean? @default(false) @db.Boolean
|
||||
debounceTime Int? @db.Integer
|
||||
ignoreJids Json?
|
||||
splitMessages Boolean? @default(false) @db.Boolean
|
||||
timePerChar Int? @default(50) @db.Integer
|
||||
createdAt DateTime? @default(now()) @db.Timestamp
|
||||
updatedAt DateTime @updatedAt @db.Timestamp
|
||||
Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id])
|
||||
evoaiIdFallback String? @db.VarChar(100)
|
||||
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
||||
instanceId String @unique
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
@@ -1,19 +1,48 @@
|
||||
const dotenv = require('dotenv');
|
||||
const { execSync } = require('child_process');
|
||||
const { existsSync } = require('fs');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const { DATABASE_PROVIDER } = process.env;
|
||||
const databaseProviderDefault = DATABASE_PROVIDER ?? "postgresql"
|
||||
const databaseProviderDefault = DATABASE_PROVIDER ?? 'postgresql';
|
||||
|
||||
if (!DATABASE_PROVIDER) {
|
||||
console.error(`DATABASE_PROVIDER is not set in the .env file, using default: ${databaseProviderDefault}`);
|
||||
// process.exit(1);
|
||||
console.warn(`DATABASE_PROVIDER is not set in the .env file, using default: ${databaseProviderDefault}`);
|
||||
}
|
||||
|
||||
const command = process.argv
|
||||
// Função para determinar qual pasta de migrations usar
|
||||
// Função para determinar qual pasta de migrations usar
|
||||
function getMigrationsFolder(provider) {
|
||||
switch (provider) {
|
||||
case 'psql_bouncer':
|
||||
return 'postgresql-migrations'; // psql_bouncer usa as migrations do postgresql
|
||||
default:
|
||||
return `${provider}-migrations`;
|
||||
}
|
||||
}
|
||||
|
||||
const migrationsFolder = getMigrationsFolder(databaseProviderDefault);
|
||||
|
||||
let command = process.argv
|
||||
.slice(2)
|
||||
.join(' ')
|
||||
.replace(/\DATABASE_PROVIDER/g, databaseProviderDefault);
|
||||
.replace(/DATABASE_PROVIDER/g, databaseProviderDefault);
|
||||
|
||||
// Substituir referências à pasta de migrations pela pasta correta
|
||||
const migrationsPattern = new RegExp(`${databaseProviderDefault}-migrations`, 'g');
|
||||
command = command.replace(migrationsPattern, migrationsFolder);
|
||||
|
||||
if (command.includes('rmdir') && existsSync('prisma\\migrations')) {
|
||||
try {
|
||||
execSync('rmdir /S /Q prisma\\migrations', { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
console.error(`Error removing directory: prisma\\migrations`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (command.includes('rmdir')) {
|
||||
console.warn(`Directory 'prisma\\migrations' does not exist, skipping removal.`);
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
|
||||
15
src/api/controllers/business.controller.ts
Normal file
15
src/api/controllers/business.controller.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
|
||||
export class BusinessController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data);
|
||||
}
|
||||
|
||||
public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data);
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,10 @@ export class ChatController {
|
||||
return await this.waMonitor.waInstances[instanceName].fetchChats(query);
|
||||
}
|
||||
|
||||
public async findChatByRemoteJid({ instanceName }: InstanceDto, remoteJid: string) {
|
||||
return await this.waMonitor.waInstances[instanceName].findChatByRemoteJid(remoteJid);
|
||||
}
|
||||
|
||||
public async sendPresence({ instanceName }: InstanceDto, data: SendPresenceDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].sendPresence(data);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,9 @@ export class InstanceController {
|
||||
instanceId,
|
||||
integration: instanceData.integration,
|
||||
instanceName: instanceData.instanceName,
|
||||
ownerJid: instanceData.ownerJid,
|
||||
profileName: instanceData.profileName,
|
||||
profilePicUrl: instanceData.profilePicUrl,
|
||||
hash,
|
||||
number: instanceData.number,
|
||||
businessId: instanceData.businessId,
|
||||
@@ -119,6 +122,7 @@ export class InstanceController {
|
||||
readMessages: instanceData.readMessages === true,
|
||||
readStatus: instanceData.readStatus === true,
|
||||
syncFullHistory: instanceData.syncFullHistory === true,
|
||||
wavoipToken: instanceData.wavoipToken || '',
|
||||
};
|
||||
|
||||
await this.settingsService.create(instance, settings);
|
||||
@@ -166,6 +170,9 @@ export class InstanceController {
|
||||
rabbitmq: {
|
||||
enabled: instanceData?.rabbitmq?.enabled,
|
||||
},
|
||||
nats: {
|
||||
enabled: instanceData?.nats?.enabled,
|
||||
},
|
||||
sqs: {
|
||||
enabled: instanceData?.sqs?.enabled,
|
||||
},
|
||||
@@ -254,6 +261,9 @@ export class InstanceController {
|
||||
rabbitmq: {
|
||||
enabled: instanceData?.rabbitmq?.enabled,
|
||||
},
|
||||
nats: {
|
||||
enabled: instanceData?.nats?.enabled,
|
||||
},
|
||||
sqs: {
|
||||
enabled: instanceData?.sqs?.enabled,
|
||||
},
|
||||
@@ -382,7 +392,9 @@ export class InstanceController {
|
||||
return this.waMonitor.instanceInfoById(instanceId, number);
|
||||
}
|
||||
|
||||
return this.waMonitor.instanceInfo();
|
||||
const instanceNames = instanceName ? [instanceName] : null;
|
||||
|
||||
return this.waMonitor.instanceInfo(instanceNames);
|
||||
}
|
||||
|
||||
public async setPresence({ instanceName }: InstanceDto, data: SetPresenceDto) {
|
||||
@@ -407,15 +419,11 @@ export class InstanceController {
|
||||
|
||||
public async deleteInstance({ instanceName }: InstanceDto) {
|
||||
const { instance } = await this.connectionState({ instanceName });
|
||||
|
||||
if (instance.state === 'open') {
|
||||
throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected');
|
||||
}
|
||||
try {
|
||||
const waInstances = this.waMonitor.waInstances[instanceName];
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) waInstances?.clearCacheChatwoot();
|
||||
|
||||
if (instance.state === 'connecting') {
|
||||
if (instance.state === 'connecting' || instance.state === 'open') {
|
||||
await this.logout({ instanceName });
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ import axios from 'axios';
|
||||
const logger = new Logger('ProxyController');
|
||||
|
||||
export class ProxyController {
|
||||
constructor(private readonly proxyService: ProxyService, private readonly waMonitor: WAMonitoringService) {}
|
||||
constructor(
|
||||
private readonly proxyService: ProxyService,
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
) {}
|
||||
|
||||
public async createProxy(instance: InstanceDto, data: ProxyDto) {
|
||||
if (!this.waMonitor.waInstances[instance.instanceName]) {
|
||||
@@ -50,15 +53,21 @@ export class ProxyController {
|
||||
httpsAgent: makeProxyAgent(proxy),
|
||||
});
|
||||
|
||||
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: ');
|
||||
const result = response?.data !== serverIp?.data;
|
||||
if (result) {
|
||||
logger.info('testProxy: proxy connection successful');
|
||||
} else {
|
||||
logger.error('testProxy error: ');
|
||||
logger.warn("testProxy: proxy connection doesn't change the origin IP");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error('testProxy error: axios error: ' + error.message);
|
||||
} else {
|
||||
logger.error('testProxy error: unexpected error: ' + error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SendLocationDto,
|
||||
SendMediaDto,
|
||||
SendPollDto,
|
||||
SendPtvDto,
|
||||
SendReactionDto,
|
||||
SendStatusDto,
|
||||
SendStickerDto,
|
||||
@@ -16,6 +17,16 @@ import {
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { isBase64, isURL } from 'class-validator';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
const regex = emojiRegex();
|
||||
|
||||
function isEmoji(str: string) {
|
||||
if (str === '') return true;
|
||||
|
||||
const match = str.match(regex);
|
||||
return match?.length === 1 && match[0] === str;
|
||||
}
|
||||
|
||||
export class SendMessageController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
@@ -39,6 +50,13 @@ export class SendMessageController {
|
||||
throw new BadRequestException('Owned media must be a url or base64');
|
||||
}
|
||||
|
||||
public async sendPtv({ instanceName }: InstanceDto, data: SendPtvDto, file?: any) {
|
||||
if (file || isURL(data?.video) || isBase64(data?.video)) {
|
||||
return await this.waMonitor.waInstances[instanceName].ptvMessage(data, file);
|
||||
}
|
||||
throw new BadRequestException('Owned media must be a url or base64');
|
||||
}
|
||||
|
||||
public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto, file?: any) {
|
||||
if (file || isURL(data.sticker) || isBase64(data.sticker)) {
|
||||
return await this.waMonitor.waInstances[instanceName].mediaSticker(data, file);
|
||||
@@ -73,8 +91,8 @@ export class SendMessageController {
|
||||
}
|
||||
|
||||
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
|
||||
if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) {
|
||||
throw new BadRequestException('"reaction" must be an emoji');
|
||||
if (!isEmoji(data.reaction)) {
|
||||
throw new BadRequestException('Reaction must be a single emoji or empty string');
|
||||
}
|
||||
return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
|
||||
}
|
||||
|
||||
14
src/api/dto/business.dto.ts
Normal file
14
src/api/dto/business.dto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export class NumberDto {
|
||||
number: string;
|
||||
}
|
||||
|
||||
export class getCatalogDto {
|
||||
number?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export class getCollectionsDto {
|
||||
number?: string;
|
||||
limit?: number;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export class OnWhatsAppDto {
|
||||
public readonly exists: boolean,
|
||||
public readonly number: string,
|
||||
public readonly name?: string,
|
||||
public readonly lid?: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IntegrationDto } from '@api/integrations/integration.dto';
|
||||
import { JsonValue } from '@prisma/client/runtime/library';
|
||||
import { WAPresence } from 'baileys';
|
||||
|
||||
export class InstanceDto extends IntegrationDto {
|
||||
@@ -10,6 +11,9 @@ export class InstanceDto extends IntegrationDto {
|
||||
integration?: string;
|
||||
token?: string;
|
||||
status?: string;
|
||||
ownerJid?: string;
|
||||
profileName?: string;
|
||||
profilePicUrl?: string;
|
||||
// settings
|
||||
rejectCall?: boolean;
|
||||
msgCall?: string;
|
||||
@@ -18,12 +22,35 @@ export class InstanceDto extends IntegrationDto {
|
||||
readMessages?: boolean;
|
||||
readStatus?: boolean;
|
||||
syncFullHistory?: boolean;
|
||||
wavoipToken?: string;
|
||||
// proxy
|
||||
proxyHost?: string;
|
||||
proxyPort?: string;
|
||||
proxyProtocol?: string;
|
||||
proxyUsername?: string;
|
||||
proxyPassword?: string;
|
||||
webhook?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
headers?: JsonValue;
|
||||
url?: string;
|
||||
byEvents?: boolean;
|
||||
base64?: boolean;
|
||||
};
|
||||
chatwootAccountId?: string;
|
||||
chatwootConversationPending?: boolean;
|
||||
chatwootAutoCreate?: boolean;
|
||||
chatwootDaysLimitImportMessages?: number;
|
||||
chatwootImportContacts?: boolean;
|
||||
chatwootImportMessages?: boolean;
|
||||
chatwootLogo?: string;
|
||||
chatwootMergeBrazilContacts?: boolean;
|
||||
chatwootNameInbox?: string;
|
||||
chatwootOrganization?: string;
|
||||
chatwootReopenConversation?: boolean;
|
||||
chatwootSignMsg?: boolean;
|
||||
chatwootToken?: string;
|
||||
chatwootUrl?: string;
|
||||
}
|
||||
|
||||
export class SetPresenceDto {
|
||||
|
||||
@@ -44,6 +44,7 @@ export class Metadata {
|
||||
mentionsEveryOne?: boolean;
|
||||
mentioned?: string[];
|
||||
encoding?: boolean;
|
||||
notConvertSticker?: boolean;
|
||||
}
|
||||
|
||||
export class SendTextDto extends Metadata {
|
||||
@@ -70,7 +71,7 @@ export class SendPollDto extends Metadata {
|
||||
messageSecret?: Uint8Array;
|
||||
}
|
||||
|
||||
export type MediaType = 'image' | 'document' | 'video' | 'audio';
|
||||
export type MediaType = 'image' | 'document' | 'video' | 'audio' | 'ptv';
|
||||
|
||||
export class SendMediaDto extends Metadata {
|
||||
mediatype: MediaType;
|
||||
@@ -82,6 +83,10 @@ export class SendMediaDto extends Metadata {
|
||||
media: string;
|
||||
}
|
||||
|
||||
export class SendPtvDto extends Metadata {
|
||||
video: string;
|
||||
}
|
||||
|
||||
export class SendStickerDto extends Metadata {
|
||||
sticker: string;
|
||||
}
|
||||
@@ -90,15 +95,21 @@ export class SendAudioDto extends Metadata {
|
||||
audio: string;
|
||||
}
|
||||
|
||||
export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
|
||||
export type TypeButton = 'reply' | 'copy' | 'url' | 'call' | 'pix';
|
||||
|
||||
export type KeyType = 'phone' | 'email' | 'cpf' | 'cnpj' | 'random';
|
||||
|
||||
export class Button {
|
||||
type: TypeButton;
|
||||
displayText: string;
|
||||
displayText?: string;
|
||||
id?: string;
|
||||
url?: string;
|
||||
copyCode?: string;
|
||||
phoneNumber?: string;
|
||||
currency?: string;
|
||||
name?: string;
|
||||
keyType?: KeyType;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export class SendButtonsDto extends Metadata {
|
||||
|
||||
@@ -6,4 +6,5 @@ export class SettingsDto {
|
||||
readMessages?: boolean;
|
||||
readStatus?: boolean;
|
||||
syncFullHistory?: boolean;
|
||||
wavoipToken?: string;
|
||||
}
|
||||
|
||||
@@ -75,8 +75,6 @@ export class ChannelController {
|
||||
data.prismaRepository,
|
||||
data.cache,
|
||||
data.chatwootCache,
|
||||
data.baileysCache,
|
||||
data.providerFiles,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@ import { Router } from 'express';
|
||||
|
||||
import { EvolutionRouter } from './evolution/evolution.router';
|
||||
import { MetaRouter } from './meta/meta.router';
|
||||
import { BaileysRouter } from './whatsapp/baileys.router';
|
||||
|
||||
export class ChannelRouter {
|
||||
public readonly router: Router;
|
||||
|
||||
constructor(configService: any) {
|
||||
constructor(configService: any, ...guards: any[]) {
|
||||
this.router = Router();
|
||||
|
||||
this.router.use('/', new EvolutionRouter(configService).router);
|
||||
this.router.use('/', new MetaRouter(configService).router);
|
||||
this.router.use('/baileys', new BaileysRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
import { MediaMessage, Options, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
|
||||
import { ProviderFiles } from '@api/provider/sessions';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import {
|
||||
MediaMessage,
|
||||
Options,
|
||||
SendAudioDto,
|
||||
SendButtonsDto,
|
||||
SendMediaDto,
|
||||
SendTextDto,
|
||||
} from '@api/dto/sendMessage.dto';
|
||||
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { chatbotController } from '@api/server.module';
|
||||
import { CacheService } from '@api/services/cache.service';
|
||||
import { ChannelStartupService } from '@api/services/channel.service';
|
||||
import { Events, wa } from '@api/types/wa.types';
|
||||
import { Chatwoot, ConfigService, Openai } from '@config/env.config';
|
||||
import { Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
|
||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||
import { status } from '@utils/renderStatus';
|
||||
import { isURL } from 'class-validator';
|
||||
import { createJid } from '@utils/createJid';
|
||||
import axios from 'axios';
|
||||
import { isBase64, isURL } from 'class-validator';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import mime from 'mime';
|
||||
import FormData from 'form-data';
|
||||
import mimeTypes from 'mime-types';
|
||||
import { join } from 'path';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export class EvolutionStartupService extends ChannelStartupService {
|
||||
@@ -20,8 +31,6 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
public readonly prismaRepository: PrismaRepository,
|
||||
public readonly cache: CacheService,
|
||||
public readonly chatwootCache: CacheService,
|
||||
public readonly baileysCache: CacheService,
|
||||
private readonly providerFiles: ProviderFiles,
|
||||
) {
|
||||
super(configService, eventEmitter, prismaRepository, chatwootCache);
|
||||
|
||||
@@ -56,8 +65,34 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
await this.closeClient();
|
||||
}
|
||||
|
||||
public setInstance(instance: InstanceDto) {
|
||||
this.logger.setInstance(instance.instanceId);
|
||||
|
||||
this.instance.name = instance.instanceName;
|
||||
this.instance.id = instance.instanceId;
|
||||
this.instance.integration = instance.integration;
|
||||
this.instance.number = instance.number;
|
||||
this.instance.token = instance.token;
|
||||
this.instance.businessId = instance.businessId;
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
this.chatwootService.eventWhatsapp(
|
||||
Events.STATUS_INSTANCE,
|
||||
{
|
||||
instanceName: this.instance.name,
|
||||
instanceId: this.instance.id,
|
||||
integration: instance.integration,
|
||||
},
|
||||
{
|
||||
instance: this.instance.name,
|
||||
status: 'created',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async profilePicture(number: string) {
|
||||
const jid = this.createJid(number);
|
||||
const jid = createJid(number);
|
||||
|
||||
return {
|
||||
wuid: jid,
|
||||
@@ -78,11 +113,12 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
public async connectToWhatsapp(data?: any): Promise<any> {
|
||||
if (!data) return;
|
||||
if (!data) {
|
||||
this.loadChatwoot();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loadChatwoot();
|
||||
|
||||
this.eventHandler(data);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
@@ -99,6 +135,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
id: received.key.id || v4(),
|
||||
remoteJid: received.key.remoteJid,
|
||||
fromMe: received.key.fromMe,
|
||||
profilePicUrl: received.profilePicUrl,
|
||||
};
|
||||
messageRaw = {
|
||||
key,
|
||||
@@ -110,7 +147,9 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED) {
|
||||
const isAudio = received?.message?.audioMessage;
|
||||
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED && isAudio) {
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
@@ -126,11 +165,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
openAiDefaultSettings.speechToText &&
|
||||
received?.message?.audioMessage
|
||||
) {
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
received,
|
||||
this.client.updateMediaMessage,
|
||||
);
|
||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +200,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
|
||||
await this.updateContact({
|
||||
remoteJid: messageRaw.key.remoteJid,
|
||||
pushName: messageRaw.key.fromMe ? '' : messageRaw.key.fromMe == null ? '' : received.pushName,
|
||||
pushName: messageRaw.pushName,
|
||||
profilePicUrl: received.profilePicUrl,
|
||||
});
|
||||
}
|
||||
@@ -175,11 +210,6 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
private async updateContact(data: { remoteJid: string; pushName?: string; profilePicUrl?: string }) {
|
||||
const contact = await this.prismaRepository.contact.findFirst({
|
||||
where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
|
||||
});
|
||||
|
||||
if (contact) {
|
||||
const contactRaw: any = {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data?.pushName,
|
||||
@@ -187,36 +217,41 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
profilePicUrl: data?.profilePicUrl,
|
||||
};
|
||||
|
||||
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
||||
const existingContact = await this.prismaRepository.contact.findFirst({
|
||||
where: {
|
||||
remoteJid: data.remoteJid,
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingContact) {
|
||||
await this.prismaRepository.contact.updateMany({
|
||||
where: {
|
||||
remoteJid: data.remoteJid,
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
data: contactRaw,
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.contact.create({
|
||||
data: contactRaw,
|
||||
});
|
||||
}
|
||||
|
||||
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
await this.chatwootService.eventWhatsapp(
|
||||
Events.CONTACTS_UPDATE,
|
||||
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
||||
{
|
||||
instanceName: this.instance.name,
|
||||
instanceId: this.instanceId,
|
||||
integration: this.instance.integration,
|
||||
},
|
||||
contactRaw,
|
||||
);
|
||||
}
|
||||
|
||||
await this.prismaRepository.contact.updateMany({
|
||||
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
|
||||
data: contactRaw,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const contactRaw: any = {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data?.pushName,
|
||||
instanceId: this.instanceId,
|
||||
profilePicUrl: data?.profilePicUrl,
|
||||
};
|
||||
|
||||
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
|
||||
|
||||
await this.prismaRepository.contact.create({
|
||||
data: contactRaw,
|
||||
});
|
||||
|
||||
const chat = await this.prismaRepository.chat.findFirst({
|
||||
where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
|
||||
});
|
||||
@@ -247,7 +282,13 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
});
|
||||
}
|
||||
|
||||
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
|
||||
protected async sendMessageWithTyping(
|
||||
number: string,
|
||||
message: any,
|
||||
options?: Options,
|
||||
file?: any,
|
||||
isIntegration = false,
|
||||
) {
|
||||
try {
|
||||
let quoted: any;
|
||||
let webhookUrl: any;
|
||||
@@ -272,64 +313,194 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
webhookUrl = options.webhookUrl;
|
||||
}
|
||||
|
||||
let audioFile;
|
||||
|
||||
const messageId = v4();
|
||||
|
||||
let messageRaw: any = {
|
||||
let messageRaw: any;
|
||||
|
||||
if (message?.mediaType === 'image') {
|
||||
messageRaw = {
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
base64: isBase64(message.media) ? message.media : undefined,
|
||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'imageMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
status: status[1],
|
||||
};
|
||||
|
||||
if (message?.mediaType === 'image') {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
message: {
|
||||
mediaUrl: message.media,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'imageMessage',
|
||||
};
|
||||
} else if (message?.mediaType === 'video') {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
mediaUrl: message.media,
|
||||
base64: isBase64(message.media) ? message.media : undefined,
|
||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'videoMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (message?.mediaType === 'audio') {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
mediaUrl: message.media,
|
||||
base64: isBase64(message.media) ? message.media : undefined,
|
||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'audioMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
const buffer = Buffer.from(message.media, 'base64');
|
||||
audioFile = {
|
||||
buffer,
|
||||
mimetype: 'audio/mp4',
|
||||
originalname: `${messageId}.mp4`,
|
||||
};
|
||||
} else if (message?.mediaType === 'document') {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
mediaUrl: message.media,
|
||||
base64: isBase64(message.media) ? message.media : undefined,
|
||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'documentMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (message.buttonMessage) {
|
||||
messageRaw = {
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
...message.buttonMessage,
|
||||
buttons: message.buttonMessage.buttons,
|
||||
footer: message.buttonMessage.footer,
|
||||
body: message.buttonMessage.body,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'buttonMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (message.listMessage) {
|
||||
messageRaw = {
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
...message.listMessage,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'listMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
...message,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'conversation',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
}
|
||||
|
||||
if (messageRaw.message.contextInfo) {
|
||||
messageRaw.contextInfo = {
|
||||
...messageRaw.message.contextInfo,
|
||||
};
|
||||
}
|
||||
|
||||
if (messageRaw.contextInfo?.stanzaId) {
|
||||
const key: any = {
|
||||
id: messageRaw.contextInfo.stanzaId,
|
||||
};
|
||||
|
||||
const findMessage = await this.prismaRepository.message.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
key,
|
||||
},
|
||||
});
|
||||
|
||||
if (findMessage) {
|
||||
messageRaw.contextInfo.quotedMessage = findMessage.message;
|
||||
}
|
||||
}
|
||||
|
||||
const base64 = messageRaw.message.base64;
|
||||
delete messageRaw.message.base64;
|
||||
|
||||
if (base64 || file || audioFile) {
|
||||
if (this.configService.get<S3>('S3').ENABLE) {
|
||||
try {
|
||||
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||
const hasRealMedia = this.hasValidMediaContent(messageRaw);
|
||||
|
||||
if (!hasRealMedia) {
|
||||
this.logger.warn('Message detected as media but contains no valid media content');
|
||||
} else {
|
||||
const fileBuffer = audioFile?.buffer || file?.buffer;
|
||||
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
|
||||
|
||||
let mediaType: string;
|
||||
let mimetype = audioFile?.mimetype || file.mimetype;
|
||||
|
||||
if (messageRaw.messageType === 'documentMessage') {
|
||||
mediaType = 'document';
|
||||
mimetype = !mimetype ? 'application/pdf' : mimetype;
|
||||
} else if (messageRaw.messageType === 'imageMessage') {
|
||||
mediaType = 'image';
|
||||
mimetype = !mimetype ? 'image/png' : mimetype;
|
||||
} else if (messageRaw.messageType === 'audioMessage') {
|
||||
mediaType = 'audio';
|
||||
mimetype = !mimetype ? 'audio/mp4' : mimetype;
|
||||
} else if (messageRaw.messageType === 'videoMessage') {
|
||||
mediaType = 'video';
|
||||
mimetype = !mimetype ? 'video/mp4' : mimetype;
|
||||
}
|
||||
|
||||
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
|
||||
|
||||
const size = buffer.byteLength;
|
||||
|
||||
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
|
||||
|
||||
await s3Service.uploadFile(fullName, buffer, size, {
|
||||
'Content-Type': mimetype,
|
||||
});
|
||||
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(messageRaw);
|
||||
|
||||
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
||||
@@ -375,6 +546,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
},
|
||||
null,
|
||||
isIntegration,
|
||||
);
|
||||
return res;
|
||||
@@ -396,7 +568,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
mediaMessage.fileName = 'video.mp4';
|
||||
}
|
||||
|
||||
let mimetype: string;
|
||||
let mimetype: string | false;
|
||||
|
||||
const prepareMedia: any = {
|
||||
caption: mediaMessage?.caption,
|
||||
@@ -407,9 +579,9 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
};
|
||||
|
||||
if (isURL(mediaMessage.media)) {
|
||||
mimetype = mime.getType(mediaMessage.media);
|
||||
mimetype = mimeTypes.lookup(mediaMessage.media);
|
||||
} else {
|
||||
mimetype = mime.getType(mediaMessage.fileName);
|
||||
mimetype = mimeTypes.lookup(mediaMessage.fileName);
|
||||
}
|
||||
|
||||
prepareMedia.mimetype = mimetype;
|
||||
@@ -439,34 +611,79 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
},
|
||||
file,
|
||||
isIntegration,
|
||||
);
|
||||
|
||||
return mediaSent;
|
||||
}
|
||||
|
||||
public async processAudio(audio: string, number: string) {
|
||||
public async processAudio(audio: string, number: string, file: any) {
|
||||
number = number.replace(/\D/g, '');
|
||||
const hash = `${number}-${new Date().getTime()}`;
|
||||
|
||||
let mimetype: string;
|
||||
if (process.env.API_AUDIO_CONVERTER) {
|
||||
try {
|
||||
this.logger.verbose('Using audio converter API');
|
||||
const formData = new FormData();
|
||||
|
||||
if (file) {
|
||||
formData.append('file', file.buffer, {
|
||||
filename: file.originalname,
|
||||
contentType: file.mimetype,
|
||||
});
|
||||
} else if (isURL(audio)) {
|
||||
formData.append('url', audio);
|
||||
} else {
|
||||
formData.append('base64', audio);
|
||||
}
|
||||
|
||||
formData.append('format', 'mp4');
|
||||
|
||||
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
apikey: process.env.API_AUDIO_CONVERTER_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response?.data?.audio) {
|
||||
throw new InternalServerErrorException('Failed to convert audio');
|
||||
}
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp4`,
|
||||
mediaType: 'audio',
|
||||
media: response?.data?.audio,
|
||||
mimetype: 'audio/mpeg',
|
||||
};
|
||||
|
||||
return prepareMedia;
|
||||
} catch (error) {
|
||||
this.logger.error(error?.response?.data || error);
|
||||
throw new InternalServerErrorException(error?.response?.data?.message || error?.toString() || error);
|
||||
}
|
||||
} else {
|
||||
let mimetype: string;
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp3`,
|
||||
mediaType: 'audio',
|
||||
media: audio,
|
||||
mimetype: 'audio/mpeg',
|
||||
};
|
||||
|
||||
if (isURL(audio)) {
|
||||
mimetype = mime.getType(audio);
|
||||
mimetype = mimeTypes.lookup(audio).toString();
|
||||
} else {
|
||||
mimetype = mime.getType(prepareMedia.fileName);
|
||||
mimetype = mimeTypes.lookup(prepareMedia.fileName).toString();
|
||||
}
|
||||
|
||||
prepareMedia.mimetype = mimetype;
|
||||
|
||||
return prepareMedia;
|
||||
}
|
||||
}
|
||||
|
||||
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
|
||||
const mediaData: SendAudioDto = { ...data };
|
||||
@@ -478,7 +695,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
throw new Error('File or buffer is undefined.');
|
||||
}
|
||||
|
||||
const message = await this.processAudio(mediaData.audio, data.number);
|
||||
const message = await this.processAudio(mediaData.audio, data.number, file);
|
||||
|
||||
const audioSent = await this.sendMessageWithTyping(
|
||||
data.number,
|
||||
@@ -491,14 +708,34 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
},
|
||||
file,
|
||||
isIntegration,
|
||||
);
|
||||
|
||||
return audioSent;
|
||||
}
|
||||
|
||||
public async buttonMessage() {
|
||||
throw new BadRequestException('Method not available on Evolution Channel');
|
||||
public async buttonMessage(data: SendButtonsDto, isIntegration = false) {
|
||||
return await this.sendMessageWithTyping(
|
||||
data.number,
|
||||
{
|
||||
buttonMessage: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
footer: data.footer,
|
||||
buttons: data.buttons,
|
||||
},
|
||||
},
|
||||
{
|
||||
delay: data?.delay,
|
||||
presence: 'composing',
|
||||
quoted: data?.quoted,
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
},
|
||||
null,
|
||||
isIntegration,
|
||||
);
|
||||
}
|
||||
public async locationMessage() {
|
||||
throw new BadRequestException('Method not available on Evolution Channel');
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NumberBusiness } from '@api/dto/chat.dto';
|
||||
import {
|
||||
Button,
|
||||
ContactMessage,
|
||||
MediaMessage,
|
||||
Options,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
SendReactionDto,
|
||||
SendTemplateDto,
|
||||
SendTextDto,
|
||||
TypeButton,
|
||||
} from '@api/dto/sendMessage.dto';
|
||||
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
|
||||
import { ProviderFiles } from '@api/provider/sessions';
|
||||
@@ -24,16 +22,14 @@ import { ChannelStartupService } from '@api/services/channel.service';
|
||||
import { Events, wa } from '@api/types/wa.types';
|
||||
import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config';
|
||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||
import { createJid } from '@utils/createJid';
|
||||
import { status } from '@utils/renderStatus';
|
||||
import axios from 'axios';
|
||||
import { proto } from 'baileys';
|
||||
import { arrayUnique, isURL } from 'class-validator';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import FormData from 'form-data';
|
||||
import { createReadStream } from 'fs';
|
||||
import mime from 'mime';
|
||||
import mimeTypes from 'mime-types';
|
||||
import { join } from 'path';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export class BusinessStartupService extends ChannelStartupService {
|
||||
constructor(
|
||||
@@ -74,6 +70,10 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
await this.closeClient();
|
||||
}
|
||||
|
||||
private isMediaMessage(message: any) {
|
||||
return message.document || message.image || message.audio || message.video;
|
||||
}
|
||||
|
||||
private async post(message: any, params: string) {
|
||||
try {
|
||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||
@@ -88,7 +88,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
public async profilePicture(number: string) {
|
||||
const jid = this.createJid(number);
|
||||
const jid = createJid(number);
|
||||
|
||||
return {
|
||||
wuid: jid,
|
||||
@@ -132,9 +132,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
|
||||
this.eventHandler(content);
|
||||
|
||||
this.phoneNumber = this.createJid(
|
||||
content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id,
|
||||
);
|
||||
this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new InternalServerErrorException(error?.toString());
|
||||
@@ -148,11 +146,20 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||
urlServer = `${urlServer}/${version}/${id}`;
|
||||
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||
|
||||
// Primeiro, obtenha a URL do arquivo
|
||||
let result = await axios.get(urlServer, { headers });
|
||||
result = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
|
||||
|
||||
// Depois, baixe o arquivo usando a URL retornada
|
||||
result = await axios.get(result.data.url, {
|
||||
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
return result.data;
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
this.logger.error(`Error downloading media: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +167,23 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const message = received.messages[0];
|
||||
let content: any = message.type + 'Message';
|
||||
content = { [content]: message[message.type] };
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private messageAudioJson(received: any) {
|
||||
const message = received.messages[0];
|
||||
let content: any = {
|
||||
audioMessage: {
|
||||
...message.audio,
|
||||
ptt: message.audio.voice || false, // Define se é mensagem de voz
|
||||
},
|
||||
};
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -193,17 +216,77 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
private messageTextJson(received: any) {
|
||||
let content: any;
|
||||
// Verificar que received y received.messages existen
|
||||
if (!received || !received.messages || received.messages.length === 0) {
|
||||
this.logger.error('Error: received object or messages array is undefined or empty');
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = received.messages[0];
|
||||
let content: any;
|
||||
|
||||
// Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
|
||||
if (!message.text) {
|
||||
// Si no hay texto, manejamos diferente según el tipo de mensaje
|
||||
if (message.type === 'sticker') {
|
||||
content = { stickerMessage: {} };
|
||||
} else if (message.type === 'location') {
|
||||
content = {
|
||||
locationMessage: {
|
||||
degreesLatitude: message.location?.latitude,
|
||||
degreesLongitude: message.location?.longitude,
|
||||
name: message.location?.name,
|
||||
address: message.location?.address,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Para otros tipos de mensajes sin texto, creamos un contenido genérico
|
||||
this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
|
||||
content = { [message.type + 'Message']: message[message.type] || {} };
|
||||
}
|
||||
|
||||
// Añadir contexto si existe
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Si el mensaje tiene texto, procesamos normalmente
|
||||
if (!received.metadata || !received.metadata.phone_number_id) {
|
||||
this.logger.error('Error: metadata or phone_number_id is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (message.from === received.metadata.phone_number_id) {
|
||||
content = {
|
||||
extendedTextMessage: { text: message.text.body },
|
||||
};
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
} else {
|
||||
content = { conversation: message.text.body };
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
if (message.context) {
|
||||
content = { ...content, contextInfo: { stanzaId: message.context.id } };
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private messageLocationJson(received: any) {
|
||||
const message = received.messages[0];
|
||||
let content: any = {
|
||||
locationMessage: {
|
||||
degreesLatitude: message.location.latitude,
|
||||
degreesLongitude: message.location.longitude,
|
||||
name: message.location?.name,
|
||||
address: message.location?.address,
|
||||
},
|
||||
};
|
||||
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -231,7 +314,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
if (!contact.phones[0]?.wa_id) {
|
||||
contact.phones[0].wa_id = this.createJid(contact.phones[0].phone);
|
||||
contact.phones[0].wa_id = createJid(contact.phones[0].phone);
|
||||
}
|
||||
|
||||
result +=
|
||||
@@ -284,6 +367,12 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
case 'template':
|
||||
messageType = 'conversation';
|
||||
break;
|
||||
case 'location':
|
||||
messageType = 'locationMessage';
|
||||
break;
|
||||
case 'sticker':
|
||||
messageType = 'stickerMessage';
|
||||
break;
|
||||
default:
|
||||
messageType = 'conversation';
|
||||
break;
|
||||
@@ -300,22 +389,36 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
if (received.contacts) pushName = received.contacts[0].profile.name;
|
||||
|
||||
if (received.messages) {
|
||||
const message = received.messages[0]; // Añadir esta línea para definir message
|
||||
|
||||
const key = {
|
||||
id: received.messages[0].id,
|
||||
id: message.id,
|
||||
remoteJid: this.phoneNumber,
|
||||
fromMe: received.messages[0].from === received.metadata.phone_number_id,
|
||||
fromMe: message.from === received.metadata.phone_number_id,
|
||||
};
|
||||
if (
|
||||
received?.messages[0].document ||
|
||||
received?.messages[0].image ||
|
||||
received?.messages[0].audio ||
|
||||
received?.messages[0].video
|
||||
) {
|
||||
|
||||
if (message.type === 'sticker') {
|
||||
this.logger.log('Procesando mensaje de tipo sticker');
|
||||
messageRaw = {
|
||||
key,
|
||||
pushName,
|
||||
message: this.messageMediaJson(received),
|
||||
contextInfo: this.messageMediaJson(received)?.contextInfo,
|
||||
message: {
|
||||
stickerMessage: message.sticker || {},
|
||||
},
|
||||
messageType: 'stickerMessage',
|
||||
messageTimestamp: parseInt(message.timestamp) as number,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (this.isMediaMessage(message)) {
|
||||
const messageContent =
|
||||
message.type === 'audio' ? this.messageAudioJson(received) : this.messageMediaJson(received);
|
||||
|
||||
messageRaw = {
|
||||
key,
|
||||
pushName,
|
||||
message: messageContent,
|
||||
contextInfo: messageContent?.contextInfo,
|
||||
messageType: this.renderMessageType(received.messages[0].type),
|
||||
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
|
||||
source: 'unknown',
|
||||
@@ -326,6 +429,12 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
try {
|
||||
const message: any = received;
|
||||
|
||||
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||
const hasRealMedia = this.hasValidMediaContent(messageRaw);
|
||||
|
||||
if (!hasRealMedia) {
|
||||
this.logger.warn('Message detected as media but contains no valid media content');
|
||||
} else {
|
||||
const id = message.messages[0][message.messages[0].type].id;
|
||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||
@@ -333,17 +442,24 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||
const result = await axios.get(urlServer, { headers });
|
||||
|
||||
const buffer = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
|
||||
const buffer = await axios.get(result.data.url, {
|
||||
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
const mediaType = message.messages[0].document
|
||||
? 'document'
|
||||
: message.messages[0].image
|
||||
? 'image'
|
||||
: message.messages[0].audio
|
||||
? 'audio'
|
||||
: 'video';
|
||||
let mediaType;
|
||||
|
||||
const mimetype = result.headers['content-type'];
|
||||
if (message.messages[0].document) {
|
||||
mediaType = 'document';
|
||||
} else if (message.messages[0].image) {
|
||||
mediaType = 'image';
|
||||
} else if (message.messages[0].audio) {
|
||||
mediaType = 'audio';
|
||||
} else {
|
||||
mediaType = 'video';
|
||||
}
|
||||
|
||||
const mimetype = result.data?.mime_type || result.headers['content-type'];
|
||||
|
||||
const contentDisposition = result.headers['content-disposition'];
|
||||
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
|
||||
@@ -354,17 +470,32 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
// Para áudio, garantir extensão correta baseada no mimetype
|
||||
if (mediaType === 'audio') {
|
||||
if (mimetype.includes('ogg')) {
|
||||
fileName = `${message.messages[0].id}.ogg`;
|
||||
} else if (mimetype.includes('mp3')) {
|
||||
fileName = `${message.messages[0].id}.mp3`;
|
||||
} else if (mimetype.includes('m4a')) {
|
||||
fileName = `${message.messages[0].id}.m4a`;
|
||||
}
|
||||
}
|
||||
|
||||
const size = result.headers['content-length'] || buffer.data.byteLength;
|
||||
|
||||
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
|
||||
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
|
||||
|
||||
await s3Service.uploadFile(fullName, buffer.data, size, {
|
||||
'Content-Type': mimetype,
|
||||
});
|
||||
|
||||
const createdMessage = await this.prismaRepository.message.create({
|
||||
data: messageRaw,
|
||||
});
|
||||
|
||||
await this.prismaRepository.media.create({
|
||||
data: {
|
||||
messageId: received.messages[0].id,
|
||||
messageId: createdMessage.id,
|
||||
instanceId: this.instanceId,
|
||||
type: mediaType,
|
||||
fileName: fullName,
|
||||
@@ -375,13 +506,74 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
messageRaw.message.base64 = buffer.data.toString('base64');
|
||||
|
||||
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
openAiDefaultSettings &&
|
||||
openAiDefaultSettings.openaiCredsId &&
|
||||
openAiDefaultSettings.speechToText
|
||||
) {
|
||||
try {
|
||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
{
|
||||
message: {
|
||||
mediaUrl: messageRaw.message.mediaUrl,
|
||||
...messageRaw,
|
||||
},
|
||||
},
|
||||
)}`;
|
||||
} catch (speechError) {
|
||||
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
||||
}
|
||||
} else {
|
||||
const buffer = await this.downloadMediaMessage(received?.messages[0]);
|
||||
|
||||
messageRaw.message.base64 = buffer.toString('base64');
|
||||
|
||||
// Processar OpenAI speech-to-text para áudio mesmo sem S3
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
||||
try {
|
||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
{
|
||||
message: {
|
||||
base64: messageRaw.message.base64,
|
||||
...messageRaw,
|
||||
},
|
||||
},
|
||||
)}`;
|
||||
} catch (speechError) {
|
||||
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (received?.messages[0].interactive) {
|
||||
messageRaw = {
|
||||
@@ -452,30 +644,6 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
// await this.client.readMessages([received.key]);
|
||||
}
|
||||
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED) {
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
include: {
|
||||
OpenaiCreds: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
openAiDefaultSettings &&
|
||||
openAiDefaultSettings.openaiCredsId &&
|
||||
openAiDefaultSettings.speechToText &&
|
||||
received?.message?.audioMessage
|
||||
) {
|
||||
messageRaw.message.speechToText = await this.openaiService.speechToText(
|
||||
openAiDefaultSettings.OpenaiCreds,
|
||||
received,
|
||||
this.client.updateMediaMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(messageRaw);
|
||||
|
||||
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
||||
@@ -501,9 +669,11 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isMediaMessage(message) && message.type !== 'sticker') {
|
||||
await this.prismaRepository.message.create({
|
||||
data: messageRaw,
|
||||
});
|
||||
}
|
||||
|
||||
const contact = await this.prismaRepository.contact.findFirst({
|
||||
where: { instanceId: this.instanceId, remoteJid: key.remoteJid },
|
||||
@@ -702,17 +872,54 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
protected async eventHandler(content: any) {
|
||||
try {
|
||||
// Registro para depuración
|
||||
this.logger.log('Contenido recibido en eventHandler:');
|
||||
this.logger.log(JSON.stringify(content, null, 2));
|
||||
|
||||
const database = this.configService.get<Database>('DATABASE');
|
||||
const settings = await this.findSettings();
|
||||
|
||||
// Si hay mensajes, verificar primero el tipo
|
||||
if (content.messages && content.messages.length > 0) {
|
||||
const message = content.messages[0];
|
||||
this.logger.log(`Tipo de mensaje recibido: ${message.type}`);
|
||||
|
||||
// Verificamos el tipo de mensaje antes de procesarlo
|
||||
if (
|
||||
message.type === 'text' ||
|
||||
message.type === 'image' ||
|
||||
message.type === 'video' ||
|
||||
message.type === 'audio' ||
|
||||
message.type === 'document' ||
|
||||
message.type === 'sticker' ||
|
||||
message.type === 'location' ||
|
||||
message.type === 'contacts' ||
|
||||
message.type === 'interactive' ||
|
||||
message.type === 'button' ||
|
||||
message.type === 'reaction'
|
||||
) {
|
||||
// Procesar el mensaje normalmente
|
||||
this.messageHandle(content, database, settings);
|
||||
} else {
|
||||
this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
|
||||
}
|
||||
} else if (content.statuses) {
|
||||
// Procesar actualizaciones de estado
|
||||
this.messageHandle(content, database, settings);
|
||||
} else {
|
||||
this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error en eventHandler:');
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
|
||||
try {
|
||||
let quoted: any;
|
||||
let webhookUrl: any;
|
||||
const linkPreview = options?.linkPreview != false ? undefined : false;
|
||||
if (options?.quoted) {
|
||||
const m = options?.quoted;
|
||||
|
||||
@@ -780,13 +987,15 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
to: number.replace(/\D/g, ''),
|
||||
text: {
|
||||
body: message['conversation'],
|
||||
preview_url: linkPreview,
|
||||
preview_url: Boolean(options?.linkPreview),
|
||||
},
|
||||
};
|
||||
quoted ? (content.context = { message_id: quoted.id }) : content;
|
||||
return await this.post(content, 'messages');
|
||||
}
|
||||
if (message['media']) {
|
||||
const isImage = message['mimetype']?.startsWith('image/');
|
||||
|
||||
content = {
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual',
|
||||
@@ -794,8 +1003,10 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
to: number.replace(/\D/g, ''),
|
||||
[message['mediaType']]: {
|
||||
[message['type']]: message['id'],
|
||||
preview_url: linkPreview,
|
||||
caption: message['caption'],
|
||||
...(message['mediaType'] !== 'audio' &&
|
||||
message['fileName'] &&
|
||||
!isImage && { filename: message['fileName'] }),
|
||||
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
|
||||
},
|
||||
};
|
||||
quoted ? (content.context = { message_id: quoted.id }) : content;
|
||||
@@ -893,13 +1104,13 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
})();
|
||||
|
||||
if (messageSent?.error_data) {
|
||||
if (messageSent?.error_data || messageSent.message) {
|
||||
this.logger.error(messageSent);
|
||||
return messageSent;
|
||||
}
|
||||
|
||||
const messageRaw: any = {
|
||||
key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: this.createJid(number) },
|
||||
key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: createJid(number) },
|
||||
message: this.convertMessageToRaw(message, content),
|
||||
messageType: this.renderMessageType(content.type),
|
||||
messageTimestamp: (messageSent?.messages[0]?.timestamp as number) || Math.round(new Date().getTime() / 1000),
|
||||
@@ -960,29 +1171,50 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
return res;
|
||||
}
|
||||
|
||||
private async getIdMedia(mediaMessage: any) {
|
||||
private async getIdMedia(mediaMessage: any, isFile = false) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
const fileStream = createReadStream(mediaMessage.media);
|
||||
if (isFile === false) {
|
||||
if (isURL(mediaMessage.media)) {
|
||||
const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' });
|
||||
const buffer = Buffer.from(response.data, 'base64');
|
||||
formData.append('file', buffer, {
|
||||
filename: mediaMessage.fileName || 'media',
|
||||
contentType: mediaMessage.mimetype,
|
||||
});
|
||||
} else {
|
||||
const buffer = Buffer.from(mediaMessage.media, 'base64');
|
||||
formData.append('file', buffer, {
|
||||
filename: mediaMessage.fileName || 'media',
|
||||
contentType: mediaMessage.mimetype,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
formData.append('file', mediaMessage.media.buffer, {
|
||||
filename: mediaMessage.media.originalname,
|
||||
contentType: mediaMessage.media.mimetype,
|
||||
});
|
||||
}
|
||||
|
||||
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
|
||||
formData.append('typeFile', mediaMessage.mimetype);
|
||||
const mimetype = mediaMessage.mimetype || mediaMessage.media.mimetype;
|
||||
|
||||
formData.append('typeFile', mimetype);
|
||||
formData.append('messaging_product', 'whatsapp');
|
||||
|
||||
// const fileBuffer = await fs.readFile(mediaMessage.media);
|
||||
const token = this.token;
|
||||
|
||||
// const fileBlob = new Blob([fileBuffer], { type: mediaMessage.mimetype });
|
||||
// formData.append('file', fileBlob);
|
||||
// formData.append('typeFile', mediaMessage.mimetype);
|
||||
// formData.append('messaging_product', 'whatsapp');
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
const url = `${this.configService.get<WaBusiness>('WA_BUSINESS').URL}/${
|
||||
this.configService.get<WaBusiness>('WA_BUSINESS').VERSION
|
||||
}/${this.number}/media`;
|
||||
|
||||
const headers = { Authorization: `Bearer ${this.token}` };
|
||||
const res = await axios.post(
|
||||
process.env.API_URL + '/' + process.env.VERSION + '/' + this.number + '/media',
|
||||
formData,
|
||||
{ headers },
|
||||
);
|
||||
const res = await axios.post(url, formData, { headers });
|
||||
return res.data.id;
|
||||
} catch (error) {
|
||||
this.logger.error(error.response.data);
|
||||
throw new InternalServerErrorException(error?.toString() || error);
|
||||
}
|
||||
}
|
||||
|
||||
protected async prepareMediaMessage(mediaMessage: MediaMessage) {
|
||||
@@ -1001,7 +1233,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
mediaMessage.fileName = 'video.mp4';
|
||||
}
|
||||
|
||||
let mimetype: string;
|
||||
let mimetype: string | false;
|
||||
|
||||
const prepareMedia: any = {
|
||||
caption: mediaMessage?.caption,
|
||||
@@ -1012,11 +1244,11 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
};
|
||||
|
||||
if (isURL(mediaMessage.media)) {
|
||||
mimetype = mime.getType(mediaMessage.media);
|
||||
mimetype = mimeTypes.lookup(mediaMessage.media);
|
||||
prepareMedia.id = mediaMessage.media;
|
||||
prepareMedia.type = 'link';
|
||||
} else {
|
||||
mimetype = mime.getType(mediaMessage.fileName);
|
||||
mimetype = mimeTypes.lookup(mediaMessage.fileName);
|
||||
const id = await this.getIdMedia(prepareMedia);
|
||||
prepareMedia.id = id;
|
||||
prepareMedia.type = 'id';
|
||||
@@ -1055,11 +1287,55 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
return mediaSent;
|
||||
}
|
||||
|
||||
public async processAudio(audio: string, number: string) {
|
||||
public async processAudio(audio: string, number: string, file: any) {
|
||||
number = number.replace(/\D/g, '');
|
||||
const hash = `${number}-${new Date().getTime()}`;
|
||||
|
||||
let mimetype: string;
|
||||
if (process.env.API_AUDIO_CONVERTER) {
|
||||
this.logger.verbose('Using audio converter API');
|
||||
const formData = new FormData();
|
||||
|
||||
if (file) {
|
||||
formData.append('file', file.buffer, {
|
||||
filename: file.originalname,
|
||||
contentType: file.mimetype,
|
||||
});
|
||||
} else if (isURL(audio)) {
|
||||
formData.append('url', audio);
|
||||
} else {
|
||||
formData.append('base64', audio);
|
||||
}
|
||||
|
||||
formData.append('format', 'mp3');
|
||||
|
||||
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
apikey: process.env.API_AUDIO_CONVERTER_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
const audioConverter = response?.data?.audio || response?.data?.url;
|
||||
|
||||
if (!audioConverter) {
|
||||
throw new InternalServerErrorException('Failed to convert audio');
|
||||
}
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp3`,
|
||||
mediaType: 'audio',
|
||||
media: audioConverter,
|
||||
mimetype: 'audio/mpeg',
|
||||
};
|
||||
|
||||
const id = await this.getIdMedia(prepareMedia);
|
||||
prepareMedia.id = id;
|
||||
prepareMedia.type = 'id';
|
||||
|
||||
this.logger.verbose('Audio converted');
|
||||
return prepareMedia;
|
||||
} else {
|
||||
let mimetype: string | false;
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp3`,
|
||||
@@ -1068,32 +1344,30 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
};
|
||||
|
||||
if (isURL(audio)) {
|
||||
mimetype = mime.getType(audio);
|
||||
mimetype = mimeTypes.lookup(audio);
|
||||
prepareMedia.id = audio;
|
||||
prepareMedia.type = 'link';
|
||||
} else {
|
||||
mimetype = mime.getType(prepareMedia.fileName);
|
||||
} else if (audio && !file) {
|
||||
mimetype = mimeTypes.lookup(prepareMedia.fileName);
|
||||
const id = await this.getIdMedia(prepareMedia);
|
||||
prepareMedia.id = id;
|
||||
prepareMedia.type = 'id';
|
||||
} else if (file) {
|
||||
prepareMedia.media = file;
|
||||
const id = await this.getIdMedia(prepareMedia, true);
|
||||
prepareMedia.id = id;
|
||||
prepareMedia.type = 'id';
|
||||
mimetype = file.mimetype;
|
||||
}
|
||||
|
||||
prepareMedia.mimetype = mimetype;
|
||||
|
||||
return prepareMedia;
|
||||
}
|
||||
|
||||
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
|
||||
const mediaData: SendAudioDto = { ...data };
|
||||
|
||||
if (file?.buffer) {
|
||||
mediaData.audio = file.buffer.toString('base64');
|
||||
} else {
|
||||
console.error('El archivo no tiene buffer o file es undefined');
|
||||
throw new Error('File or buffer is undefined');
|
||||
}
|
||||
|
||||
const message = await this.processAudio(mediaData.audio, data.number);
|
||||
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
|
||||
const message = await this.processAudio(data.audio, data.number, file);
|
||||
|
||||
const audioSent = await this.sendMessageWithTyping(
|
||||
data.number,
|
||||
@@ -1112,97 +1386,42 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
return audioSent;
|
||||
}
|
||||
|
||||
private toJSONString(button: Button): string {
|
||||
const toString = (obj: any) => JSON.stringify(obj);
|
||||
|
||||
const json = {
|
||||
call: () => toString({ display_text: button.displayText, phone_number: button.phoneNumber }),
|
||||
reply: () => toString({ display_text: button.displayText, id: button.id }),
|
||||
copy: () => toString({ display_text: button.displayText, copy_code: button.copyCode }),
|
||||
url: () =>
|
||||
toString({
|
||||
display_text: button.displayText,
|
||||
url: button.url,
|
||||
merchant_url: button.url,
|
||||
}),
|
||||
};
|
||||
|
||||
return json[button.type]?.() || '';
|
||||
}
|
||||
|
||||
private readonly mapType = new Map<TypeButton, string>([
|
||||
['reply', 'quick_reply'],
|
||||
['copy', 'cta_copy'],
|
||||
['url', 'cta_url'],
|
||||
['call', 'cta_call'],
|
||||
]);
|
||||
|
||||
public async buttonMessage(data: SendButtonsDto) {
|
||||
const generate = await (async () => {
|
||||
if (data?.thumbnailUrl) {
|
||||
return await this.prepareMediaMessage({
|
||||
mediatype: 'image',
|
||||
media: data.thumbnailUrl,
|
||||
});
|
||||
}
|
||||
})();
|
||||
const embeddedMedia: any = {};
|
||||
|
||||
const buttons = data.buttons.map((value) => {
|
||||
return {
|
||||
name: this.mapType.get(value.type),
|
||||
buttonParamsJson: this.toJSONString(value),
|
||||
const btnItems = {
|
||||
text: data.buttons.map((btn) => btn.displayText),
|
||||
ids: data.buttons.map((btn) => btn.id),
|
||||
};
|
||||
});
|
||||
|
||||
const message: proto.IMessage = {
|
||||
viewOnceMessage: {
|
||||
message: {
|
||||
messageContextInfo: {
|
||||
deviceListMetadata: {},
|
||||
deviceListMetadataVersion: 2,
|
||||
},
|
||||
interactiveMessage: {
|
||||
body: {
|
||||
text: (() => {
|
||||
let t = '*' + data.title + '*';
|
||||
if (data?.description) {
|
||||
t += '\n\n';
|
||||
t += data.description;
|
||||
t += '\n';
|
||||
if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) {
|
||||
throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.');
|
||||
}
|
||||
return t;
|
||||
})(),
|
||||
},
|
||||
footer: {
|
||||
text: data?.footer,
|
||||
},
|
||||
header: (() => {
|
||||
if (generate?.message?.imageMessage) {
|
||||
|
||||
return await this.sendMessageWithTyping(
|
||||
data.number,
|
||||
{
|
||||
text: !embeddedMedia?.mediaKey ? data.title : undefined,
|
||||
buttons: data.buttons.map((button) => {
|
||||
return {
|
||||
hasMediaAttachment: !!generate.message.imageMessage,
|
||||
imageMessage: generate.message.imageMessage,
|
||||
type: 'reply',
|
||||
reply: {
|
||||
title: button.displayText,
|
||||
id: button.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
})(),
|
||||
nativeFlowMessage: {
|
||||
buttons: buttons,
|
||||
messageParamsJson: JSON.stringify({
|
||||
from: 'api',
|
||||
templateId: v4(),
|
||||
}),
|
||||
[embeddedMedia?.mediaKey]: embeddedMedia?.message,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return await this.sendMessageWithTyping(data.number, message, {
|
||||
{
|
||||
delay: data?.delay,
|
||||
presence: 'composing',
|
||||
quoted: data?.quoted,
|
||||
linkPreview: data?.linkPreview,
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async locationMessage(data: SendLocationDto) {
|
||||
@@ -1310,7 +1529,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
if (!contact.wuid) {
|
||||
contact.wuid = this.createJid(contact.phoneNumber);
|
||||
contact.wuid = createJid(contact.phoneNumber);
|
||||
}
|
||||
|
||||
result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD';
|
||||
|
||||
60
src/api/integrations/channel/whatsapp/baileys.controller.ts
Normal file
60
src/api/integrations/channel/whatsapp/baileys.controller.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
|
||||
export class BaileysController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async onWhatsapp({ instanceName }: InstanceDto, body: any) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysOnWhatsapp(body?.jid);
|
||||
}
|
||||
|
||||
public async profilePictureUrl({ instanceName }: InstanceDto, body: any) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysProfilePictureUrl(body?.jid, body?.type, body?.timeoutMs);
|
||||
}
|
||||
|
||||
public async assertSessions({ instanceName }: InstanceDto, body: any) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysAssertSessions(body?.jids, body?.force);
|
||||
}
|
||||
|
||||
public async createParticipantNodes({ instanceName }: InstanceDto, body: any) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysCreateParticipantNodes(body?.jids, body?.message, body?.extraAttrs);
|
||||
}
|
||||
|
||||
public async getUSyncDevices({ instanceName }: InstanceDto, body: any) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysGetUSyncDevices(body?.jids, body?.useCache, body?.ignoreZeroDevices);
|
||||
}
|
||||
|
||||
public async generateMessageTag({ instanceName }: InstanceDto) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysGenerateMessageTag();
|
||||
}
|
||||
|
||||
public async sendNode({ instanceName }: InstanceDto, body: any) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysSendNode(body?.stanza);
|
||||
}
|
||||
|
||||
public async signalRepositoryDecryptMessage({ instanceName }: InstanceDto, body: any) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysSignalRepositoryDecryptMessage(body?.jid, body?.type, body?.ciphertext);
|
||||
}
|
||||
|
||||
public async getAuthState({ instanceName }: InstanceDto) {
|
||||
const instance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
return instance.baileysGetAuthState();
|
||||
}
|
||||
}
|
||||
105
src/api/integrations/channel/whatsapp/baileys.router.ts
Normal file
105
src/api/integrations/channel/whatsapp/baileys.router.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { baileysController } from '@api/server.module';
|
||||
import { instanceSchema } from '@validate/instance.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class BaileysRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('onWhatsapp'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.onWhatsapp(instance, req.body),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('profilePictureUrl'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.profilePictureUrl(instance, req.body),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('assertSessions'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.assertSessions(instance, req.body),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('createParticipantNodes'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.createParticipantNodes(instance, req.body),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('getUSyncDevices'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.getUSyncDevices(instance, req.body),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('generateMessageTag'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.generateMessageTag(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('sendNode'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.sendNode(instance, req.body),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('signalRepositoryDecryptMessage'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.signalRepositoryDecryptMessage(instance, req.body),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('getAuthState'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => baileysController.getAuthState(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BaileysEventMap, MessageUpsertType, proto } from 'baileys';
|
||||
import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs';
|
||||
|
||||
type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
|
||||
type MountProps = {
|
||||
onMessageReceive: (payload: MessageUpsertPayload, settings: any) => Promise<void>;
|
||||
};
|
||||
|
||||
export class BaileysMessageProcessor {
|
||||
private processorLogs = new Logger('BaileysMessageProcessor');
|
||||
private subscription?: Subscription;
|
||||
|
||||
protected messageSubject = new Subject<{
|
||||
messages: proto.IWebMessageInfo[];
|
||||
type: MessageUpsertType;
|
||||
requestId?: string;
|
||||
settings: any;
|
||||
}>();
|
||||
|
||||
mount({ onMessageReceive }: MountProps) {
|
||||
this.subscription = this.messageSubject
|
||||
.pipe(
|
||||
tap(({ messages }) => {
|
||||
this.processorLogs.log(`Processing batch of ${messages.length} messages`);
|
||||
}),
|
||||
concatMap(({ messages, type, requestId, settings }) =>
|
||||
from(onMessageReceive({ messages, type, requestId }, settings)).pipe(
|
||||
retryWhen((errors) =>
|
||||
errors.pipe(
|
||||
tap((error) => this.processorLogs.warn(`Retrying message batch due to error: ${error.message}`)),
|
||||
delay(1000), // 1 segundo de delay
|
||||
take(3), // Máximo 3 tentativas
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
catchError((error) => {
|
||||
this.processorLogs.error(`Error processing message batch: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
error: (error) => {
|
||||
this.processorLogs.error(`Message stream error: ${error}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
processMessage(payload: MessageUpsertPayload, settings: any) {
|
||||
const { messages, type, requestId } = payload;
|
||||
this.messageSubject.next({ messages, type, requestId, settings });
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.subscription?.unsubscribe();
|
||||
this.messageSubject.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys';
|
||||
|
||||
export interface ServerToClientEvents {
|
||||
withAck: (d: string, callback: (e: number) => void) => void;
|
||||
onWhatsApp: onWhatsAppType;
|
||||
profilePictureUrl: ProfilePictureUrlType;
|
||||
assertSessions: AssertSessionsType;
|
||||
createParticipantNodes: CreateParticipantNodesType;
|
||||
getUSyncDevices: GetUSyncDevicesType;
|
||||
generateMessageTag: GenerateMessageTagType;
|
||||
sendNode: SendNodeType;
|
||||
'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType;
|
||||
}
|
||||
|
||||
export interface ClientToServerEvents {
|
||||
init: (
|
||||
me: Contact | undefined,
|
||||
account: proto.IADVSignedDeviceIdentity | undefined,
|
||||
status: WAConnectionState,
|
||||
) => void;
|
||||
'CB:call': (packet: any) => void;
|
||||
'CB:ack,class:call': (packet: any) => void;
|
||||
'connection.update:status': (
|
||||
me: Contact | undefined,
|
||||
account: proto.IADVSignedDeviceIdentity | undefined,
|
||||
status: WAConnectionState,
|
||||
) => void;
|
||||
'connection.update:qr': (qr: string) => void;
|
||||
}
|
||||
|
||||
export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void;
|
||||
export type onWhatsAppCallback = (
|
||||
response: {
|
||||
exists: boolean;
|
||||
jid: string;
|
||||
}[],
|
||||
) => void;
|
||||
|
||||
export type ProfilePictureUrlType = (
|
||||
jid: string,
|
||||
type: 'image' | 'preview',
|
||||
timeoutMs: number | undefined,
|
||||
callback: ProfilePictureUrlCallback,
|
||||
) => void;
|
||||
export type ProfilePictureUrlCallback = (response: string | undefined) => void;
|
||||
|
||||
export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void;
|
||||
export type AssertSessionsCallback = (response: boolean) => void;
|
||||
|
||||
export type CreateParticipantNodesType = (
|
||||
jids: string[],
|
||||
message: any,
|
||||
extraAttrs: any,
|
||||
callback: CreateParticipantNodesCallback,
|
||||
) => void;
|
||||
export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void;
|
||||
|
||||
export type GetUSyncDevicesType = (
|
||||
jids: string[],
|
||||
useCache: boolean,
|
||||
ignoreZeroDevices: boolean,
|
||||
callback: GetUSyncDevicesTypeCallback,
|
||||
) => void;
|
||||
export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void;
|
||||
|
||||
export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void;
|
||||
export type GenerateMessageTagTypeCallback = (response: string) => void;
|
||||
|
||||
export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void;
|
||||
export type SendNodeTypeCallback = (response: boolean) => void;
|
||||
|
||||
export type SignalRepositoryDecryptMessageType = (
|
||||
jid: string,
|
||||
type: 'pkmsg' | 'msg',
|
||||
ciphertext: Buffer,
|
||||
callback: SignalRepositoryDecryptMessageCallback,
|
||||
) => void;
|
||||
export type SignalRepositoryDecryptMessageCallback = (response: any) => void;
|
||||
@@ -0,0 +1,181 @@
|
||||
import { ConnectionState, WAConnectionState, WASocket } from 'baileys';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
import { ClientToServerEvents, ServerToClientEvents } from './transport.type';
|
||||
|
||||
let baileys_connection_state: WAConnectionState = 'close';
|
||||
|
||||
export const useVoiceCallsBaileys = async (
|
||||
wavoip_token: string,
|
||||
baileys_sock: WASocket,
|
||||
status?: WAConnectionState,
|
||||
logger?: boolean,
|
||||
) => {
|
||||
baileys_connection_state = status ?? 'close';
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('https://devices.wavoip.com/baileys', {
|
||||
transports: ['websocket'],
|
||||
path: `/${wavoip_token}/websocket`,
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
if (logger) console.log('[*] - Wavoip connected', socket.id);
|
||||
|
||||
socket.emit(
|
||||
'init',
|
||||
baileys_sock.authState.creds.me,
|
||||
baileys_sock.authState.creds.account,
|
||||
baileys_connection_state,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
if (logger) console.log('[*] - Wavoip disconnect');
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
if (socket.active) {
|
||||
if (logger)
|
||||
console.log(
|
||||
'[*] - Wavoip connection error temporary failure, the socket will automatically try to reconnect',
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
if (logger) console.log('[*] - Wavoip connection error', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('onWhatsApp', async (jid, callback) => {
|
||||
try {
|
||||
const response: any = await baileys_sock.onWhatsApp(jid);
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call onWhatsApp function', response, jid);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call onWhatsApp function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('profilePictureUrl', async (jid, type, timeoutMs, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.profilePictureUrl(jid, type, timeoutMs);
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call profilePictureUrl function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call profilePictureUrl function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('assertSessions', async (jids, force, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.assertSessions(jids, force);
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call assertSessions function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call assertSessions function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('createParticipantNodes', async (jids, message, extraAttrs, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.createParticipantNodes(jids, message, extraAttrs);
|
||||
|
||||
callback(response, true);
|
||||
|
||||
if (logger) console.log('[*] Success on call createParticipantNodes function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call createParticipantNodes function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('getUSyncDevices', async (jids, useCache, ignoreZeroDevices, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.getUSyncDevices(jids, useCache, ignoreZeroDevices);
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call getUSyncDevices function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call getUSyncDevices function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('generateMessageTag', async (callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.generateMessageTag();
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call generateMessageTag function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call generateMessageTag function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('sendNode', async (stanza, callback) => {
|
||||
try {
|
||||
console.log('sendNode', JSON.stringify(stanza));
|
||||
const response = await baileys_sock.sendNode(stanza);
|
||||
|
||||
callback(true);
|
||||
|
||||
if (logger) console.log('[*] Success on call sendNode function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call sendNode function', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('signalRepository:decryptMessage', async (jid, type, ciphertext, callback) => {
|
||||
try {
|
||||
const response = await baileys_sock.signalRepository.decryptMessage({
|
||||
jid: jid,
|
||||
type: type,
|
||||
ciphertext: ciphertext,
|
||||
});
|
||||
|
||||
callback(response);
|
||||
|
||||
if (logger) console.log('[*] Success on call signalRepository:decryptMessage function', response);
|
||||
} catch (error) {
|
||||
if (logger) console.error('[*] Error on call signalRepository:decryptMessage function', error);
|
||||
}
|
||||
});
|
||||
|
||||
// we only use this connection data to inform the webphone that the device is connected and creeds account to generate e2e whatsapp key for make call packets
|
||||
baileys_sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
|
||||
const { connection } = update;
|
||||
|
||||
if (connection) {
|
||||
baileys_connection_state = connection;
|
||||
socket
|
||||
.timeout(1000)
|
||||
.emit(
|
||||
'connection.update:status',
|
||||
baileys_sock.authState.creds.me,
|
||||
baileys_sock.authState.creds.account,
|
||||
connection,
|
||||
);
|
||||
}
|
||||
|
||||
if (update.qr) {
|
||||
socket.timeout(1000).emit('connection.update:qr', update.qr);
|
||||
}
|
||||
});
|
||||
|
||||
baileys_sock.ws.on('CB:call', (packet) => {
|
||||
if (logger) console.log('[*] Signling received');
|
||||
socket.volatile.timeout(1000).emit('CB:call', packet);
|
||||
});
|
||||
|
||||
baileys_sock.ws.on('CB:ack,class:call', (packet) => {
|
||||
if (logger) console.log('[*] Signling ack received');
|
||||
socket.volatile.timeout(1000).emit('CB:ack,class:call', packet);
|
||||
});
|
||||
|
||||
return socket;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
935
src/api/integrations/chatbot/base-chatbot.controller.ts
Normal file
935
src/api/integrations/chatbot/base-chatbot.controller.ts
Normal file
@@ -0,0 +1,935 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
|
||||
import { BaseChatbotDto } from './base-chatbot.dto';
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from './chatbot.controller';
|
||||
|
||||
// Common settings interface for all chatbot integrations
|
||||
export interface ChatbotSettings {
|
||||
expire: number;
|
||||
keywordFinish: string;
|
||||
delayMessage: number;
|
||||
unknownMessage: string;
|
||||
listeningFromMe: boolean;
|
||||
stopBotFromMe: boolean;
|
||||
keepOpen: boolean;
|
||||
debounceTime: number;
|
||||
ignoreJids: string[];
|
||||
splitMessages: boolean;
|
||||
timePerChar: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Common bot properties for all chatbot integrations
|
||||
export interface BaseBotData {
|
||||
enabled?: boolean;
|
||||
description: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType: string | TriggerType;
|
||||
triggerOperator?: string | TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: string[];
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export abstract class BaseChatbotController<BotType = any, BotData extends BaseChatbotDto = BaseChatbotDto>
|
||||
extends ChatbotController
|
||||
implements ChatbotControllerInterface
|
||||
{
|
||||
public readonly logger: Logger;
|
||||
|
||||
integrationEnabled: boolean;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Name of the integration, to be set by the derived class
|
||||
protected abstract readonly integrationName: string;
|
||||
|
||||
// Method to process bot-specific logic
|
||||
protected abstract processBot(
|
||||
waInstance: any,
|
||||
remoteJid: string,
|
||||
bot: BotType,
|
||||
session: any,
|
||||
settings: ChatbotSettings,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
): Promise<void>;
|
||||
|
||||
// Method to get the fallback bot ID from settings
|
||||
protected abstract getFallbackBotId(settings: any): string | undefined;
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
// Base create bot implementation
|
||||
public async createBot(instance: InstanceDto, data: BotData) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// Set default settings if not provided
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck?.expire;
|
||||
if (data.keywordFinish === undefined || data.keywordFinish === null)
|
||||
data.keywordFinish = defaultSettingCheck?.keywordFinish;
|
||||
if (data.delayMessage === undefined || data.delayMessage === null)
|
||||
data.delayMessage = defaultSettingCheck?.delayMessage;
|
||||
if (data.unknownMessage === undefined || data.unknownMessage === null)
|
||||
data.unknownMessage = defaultSettingCheck?.unknownMessage;
|
||||
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
|
||||
data.listeningFromMe = defaultSettingCheck?.listeningFromMe;
|
||||
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
|
||||
data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe;
|
||||
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck?.keepOpen;
|
||||
if (data.debounceTime === undefined || data.debounceTime === null)
|
||||
data.debounceTime = defaultSettingCheck?.debounceTime;
|
||||
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck?.ignoreJids;
|
||||
if (data.splitMessages === undefined || data.splitMessages === null)
|
||||
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
|
||||
if (data.timePerChar === undefined || data.timePerChar === null)
|
||||
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error(
|
||||
`You already have a ${this.integrationName} with an "All" trigger, you cannot have more bots while it is active`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for trigger keyword duplicates
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for trigger advanced duplicates
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Derived classes should implement the specific duplicate checking before calling this method
|
||||
// and add bot-specific fields to the data object
|
||||
|
||||
try {
|
||||
const botData = {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
...this.getAdditionalBotData(data),
|
||||
};
|
||||
|
||||
const bot = await this.botRepository.create({
|
||||
data: botData,
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error(`Error creating ${this.integrationName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional fields needed for specific bot types
|
||||
protected abstract getAdditionalBotData(data: BotData): Record<string, any>;
|
||||
|
||||
// Common implementation for findBot
|
||||
public async findBot(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
try {
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return bots;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error(`Error finding ${this.integrationName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Common implementation for fetchBot
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.findUnique({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error(`Error fetching ${this.integrationName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Common implementation for settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const existingSettings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the name of the fallback field for this integration type
|
||||
const fallbackFieldName = this.getFallbackFieldName();
|
||||
|
||||
const settingsData = {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
[fallbackFieldName]: data.fallbackId, // Use the correct field name dynamically
|
||||
};
|
||||
|
||||
if (existingSettings) {
|
||||
const settings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: existingSettings.id,
|
||||
},
|
||||
data: settingsData,
|
||||
});
|
||||
|
||||
// Map the specific fallback field to a generic 'fallbackId' in the response
|
||||
return {
|
||||
...settings,
|
||||
fallbackId: settings[fallbackFieldName],
|
||||
};
|
||||
} else {
|
||||
const settings = await this.settingsRepository.create({
|
||||
data: {
|
||||
...settingsData,
|
||||
Instance: {
|
||||
connect: {
|
||||
id: instanceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Map the specific fallback field to a generic 'fallbackId' in the response
|
||||
return {
|
||||
...settings,
|
||||
fallbackId: settings[fallbackFieldName],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract method to get the field name for the fallback ID
|
||||
protected abstract getFallbackFieldName(): string;
|
||||
|
||||
// Abstract method to get the integration type (dify, n8n, evoai, etc.)
|
||||
protected abstract getIntegrationType(): string;
|
||||
|
||||
// Common implementation for fetchSettings
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the name of the fallback field for this integration type
|
||||
const fallbackFieldName = this.getFallbackFieldName();
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 300,
|
||||
keywordFinish: 'bye',
|
||||
delayMessage: 1000,
|
||||
unknownMessage: 'Sorry, I dont understand',
|
||||
listeningFromMe: true,
|
||||
stopBotFromMe: true,
|
||||
keepOpen: false,
|
||||
debounceTime: 1,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
fallbackId: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Return with standardized fallbackId field
|
||||
return {
|
||||
...settings,
|
||||
fallbackId: settings[fallbackFieldName],
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Common implementation for changeStatus
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error(`Error changing ${this.integrationName} status`);
|
||||
}
|
||||
}
|
||||
|
||||
// Common implementation for fetchSessions
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error(`${this.integrationName} not found`);
|
||||
}
|
||||
|
||||
// Get the integration type (dify, n8n, evoai, etc.)
|
||||
const integrationType = this.getIntegrationType();
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: integrationType,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
// Common implementation for ignoreJid
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Base implementation for updateBot
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: BotData) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error(`${this.integrationName} not found`);
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error(`${this.integrationName} not found`);
|
||||
}
|
||||
|
||||
// Check for "all" trigger type conflicts
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error(
|
||||
`You already have a ${this.integrationName} with an "All" trigger, you cannot have more bots while it is active`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Let subclasses check for integration-specific duplicates
|
||||
await this.validateNoDuplicatesOnUpdate(botId, instanceId, data);
|
||||
|
||||
// Check for keyword trigger duplicates
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for advanced trigger duplicates
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Combine common fields with bot-specific fields
|
||||
const updateData = {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
...this.getAdditionalUpdateFields(data),
|
||||
};
|
||||
|
||||
const updatedBot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return updatedBot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error(`Error updating ${this.integrationName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract method for validating bot-specific duplicates on update
|
||||
protected abstract validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: BotData): Promise<void>;
|
||||
|
||||
// Abstract method for getting additional fields for update
|
||||
protected abstract getAdditionalUpdateFields(data: BotData): Record<string, any>;
|
||||
|
||||
// Base implementation for deleteBot
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error(`${this.integrationName} not found`);
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error(`${this.integrationName} not found`);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error(`Error deleting ${this.integrationName} bot`);
|
||||
}
|
||||
}
|
||||
|
||||
// Base implementation for emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
if (!this.integrationEnabled) return;
|
||||
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
// Get integration type
|
||||
// const integrationType = this.getIntegrationType();
|
||||
|
||||
// Find a bot for this message
|
||||
let findBot: any = await this.findBotTrigger(this.botRepository, content, instance, session);
|
||||
|
||||
// If no bot is found, try to use fallback
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the fallback ID for this integration type
|
||||
const fallbackId = this.getFallbackBotId(fallback);
|
||||
|
||||
if (fallbackId) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallbackId,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have a bot, return
|
||||
if (!findBot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect settings with fallbacks to default settings
|
||||
let expire = findBot.expire;
|
||||
let keywordFinish = findBot.keywordFinish;
|
||||
let delayMessage = findBot.delayMessage;
|
||||
let unknownMessage = findBot.unknownMessage;
|
||||
let listeningFromMe = findBot.listeningFromMe;
|
||||
let stopBotFromMe = findBot.stopBotFromMe;
|
||||
let keepOpen = findBot.keepOpen;
|
||||
let debounceTime = findBot.debounceTime;
|
||||
let ignoreJids = findBot.ignoreJids;
|
||||
let splitMessages = findBot.splitMessages;
|
||||
let timePerChar = findBot.timePerChar;
|
||||
|
||||
if (expire === undefined || expire === null) expire = settings.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
// Handle stopping the bot if message is from me
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if not listening to messages from me
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if session exists but not awaiting user input
|
||||
if (session && session.status === 'closed') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if session exists and status is paused
|
||||
if (session && session.status === 'paused') {
|
||||
this.logger.warn(`Session for ${remoteJid} is paused, skipping message processing`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Merged settings
|
||||
const mergedSettings = {
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
};
|
||||
|
||||
// Process with debounce if needed
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
mergedSettings,
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
msg,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
mergedSettings,
|
||||
content,
|
||||
msg?.pushName,
|
||||
msg,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/api/integrations/chatbot/base-chatbot.dto.ts
Normal file
42
src/api/integrations/chatbot/base-chatbot.dto.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Base DTO for all chatbot integrations
|
||||
* Contains common properties shared by all chatbot types
|
||||
*/
|
||||
export class BaseChatbotDto {
|
||||
enabled?: boolean;
|
||||
description: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: string[];
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base settings DTO for all chatbot integrations
|
||||
*/
|
||||
export class BaseChatbotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
fallbackId?: string; // Unified fallback ID field for all integrations
|
||||
}
|
||||
412
src/api/integrations/chatbot/base-chatbot.service.ts
Normal file
412
src/api/integrations/chatbot/base-chatbot.service.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { IntegrationSession } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Base class for all chatbot service implementations
|
||||
* Contains common methods shared across different chatbot integrations
|
||||
*/
|
||||
export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
||||
protected readonly logger: Logger;
|
||||
protected readonly waMonitor: WAMonitoringService;
|
||||
protected readonly prismaRepository: PrismaRepository;
|
||||
protected readonly configService?: ConfigService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
loggerName: string,
|
||||
configService?: ConfigService,
|
||||
) {
|
||||
this.waMonitor = waMonitor;
|
||||
this.prismaRepository = prismaRepository;
|
||||
this.logger = new Logger(loggerName);
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message contains an image
|
||||
*/
|
||||
protected isImageMessage(content: string): boolean {
|
||||
return content.includes('imageMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message contains audio
|
||||
*/
|
||||
protected isAudioMessage(content: string): boolean {
|
||||
return content.includes('audioMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is valid JSON
|
||||
*/
|
||||
protected isJSON(str: string): boolean {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the media type from a URL based on its extension
|
||||
*/
|
||||
protected getMediaType(url: string): string | null {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new chatbot session
|
||||
*/
|
||||
public async createNewSession(instance: InstanceDto | any, data: any, type: string) {
|
||||
try {
|
||||
// Extract pushName safely - if data.pushName is an object with a pushName property, use that
|
||||
const pushNameValue =
|
||||
typeof data.pushName === 'object' && data.pushName?.pushName
|
||||
? data.pushName.pushName
|
||||
: typeof data.pushName === 'string'
|
||||
? data.pushName
|
||||
: null;
|
||||
|
||||
// Extract remoteJid safely
|
||||
const remoteJidValue =
|
||||
typeof data.remoteJid === 'object' && data.remoteJid?.remoteJid ? data.remoteJid.remoteJid : data.remoteJid;
|
||||
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: remoteJidValue,
|
||||
pushName: pushNameValue,
|
||||
sessionId: remoteJidValue,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard implementation for processing incoming messages
|
||||
* This handles the common workflow across all chatbot types:
|
||||
* 1. Check for existing session or create new one
|
||||
* 2. Handle message based on session state
|
||||
*/
|
||||
public async process(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: BotType,
|
||||
session: IntegrationSession,
|
||||
settings: SettingsType,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// For new sessions or sessions awaiting initialization
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// If session is paused, ignore the message
|
||||
if (session.status === 'paused') {
|
||||
return;
|
||||
}
|
||||
|
||||
// For existing sessions, keywords might indicate the conversation should end
|
||||
const keywordFinish = (settings as any)?.keywordFinish || '';
|
||||
const normalizedContent = content.toLowerCase().trim();
|
||||
if (keywordFinish.length > 0 && normalizedContent === keywordFinish.toLowerCase()) {
|
||||
// Update session to closed and return
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward the message to the chatbot API
|
||||
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
|
||||
|
||||
// Update session to indicate we're waiting for user response
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in process: ${error}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard implementation for sending messages to WhatsApp
|
||||
* This handles common patterns like markdown links and formatting
|
||||
*/
|
||||
protected async sendMessageWhatsApp(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
message: string,
|
||||
settings: SettingsType,
|
||||
): Promise<void> {
|
||||
if (!message) return;
|
||||
|
||||
const linkRegex = /!?\[(.*?)\]\((.*?)\)/g;
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const splitMessages = (settings as any)?.splitMessages ?? false;
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, altText, url] = match;
|
||||
const mediaType = this.getMediaType(url);
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
// Send accumulated text before sending media
|
||||
if (textBuffer.trim()) {
|
||||
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
// Handle sending the media
|
||||
try {
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: (settings as any)?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: (settings as any)?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
fileName: mediaType === 'document' ? altText || 'document' : undefined,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error sending media: ${error}`);
|
||||
// If media fails, at least send the alt text and URL
|
||||
textBuffer += `${altText}: ${url}`;
|
||||
}
|
||||
} else {
|
||||
// It's a regular link, keep it in the text
|
||||
textBuffer += fullMatch;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
// Add any remaining text after the last match
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
// Send any remaining text
|
||||
if (textBuffer.trim()) {
|
||||
await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to send formatted text with proper typing indicators and delays
|
||||
*/
|
||||
private async sendFormattedText(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
text: string,
|
||||
settings: any,
|
||||
splitMessages: boolean,
|
||||
): Promise<void> {
|
||||
const timePerChar = settings?.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (splitMessages) {
|
||||
const multipleMessages = text.split('\n\n');
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
if (!message.trim()) continue;
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const delay = Math.min(Math.max(text.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: text,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard implementation for initializing a new session
|
||||
* This method should be overridden if a subclass needs specific initialization
|
||||
*/
|
||||
protected async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: BotType,
|
||||
settings: SettingsType,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string | any,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
// Create a session if none exists
|
||||
if (!session) {
|
||||
// Extract pushName properly - if it's an object with pushName property, use that
|
||||
const pushNameValue =
|
||||
typeof pushName === 'object' && pushName?.pushName
|
||||
? pushName.pushName
|
||||
: typeof pushName === 'string'
|
||||
? pushName
|
||||
: null;
|
||||
|
||||
const sessionResult = await this.createNewSession(
|
||||
{
|
||||
instanceName: instance.instanceName,
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
{
|
||||
remoteJid,
|
||||
pushName: pushNameValue,
|
||||
botId: (bot as any).id,
|
||||
},
|
||||
this.getBotType(),
|
||||
);
|
||||
|
||||
if (!sessionResult || !sessionResult.session) {
|
||||
this.logger.error('Failed to create new session');
|
||||
return;
|
||||
}
|
||||
|
||||
session = sessionResult.session;
|
||||
}
|
||||
|
||||
// Update session status to opened
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Forward the message to the chatbot
|
||||
await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bot type identifier (e.g., 'dify', 'n8n', 'evoai')
|
||||
* This should match the type field used in the IntegrationSession
|
||||
*/
|
||||
protected abstract getBotType(): string;
|
||||
|
||||
/**
|
||||
* Send a message to the chatbot API
|
||||
* This is specific to each chatbot integration
|
||||
*/
|
||||
protected abstract sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: SettingsType,
|
||||
bot: BotType,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import {
|
||||
difyController,
|
||||
evoaiController,
|
||||
evolutionBotController,
|
||||
flowiseController,
|
||||
n8nController,
|
||||
openaiController,
|
||||
typebotController,
|
||||
} from '@api/server.module';
|
||||
@@ -97,6 +99,10 @@ export class ChatbotController {
|
||||
|
||||
await difyController.emit(emitData);
|
||||
|
||||
await n8nController.emit(emitData);
|
||||
|
||||
await evoaiController.emit(emitData);
|
||||
|
||||
await flowiseController.emit(emitData);
|
||||
}
|
||||
|
||||
@@ -173,7 +179,7 @@ export class ChatbotController {
|
||||
if (session) {
|
||||
if (session.status !== 'closed' && !session.botId) {
|
||||
this.logger.warn('Session is already opened in another integration');
|
||||
return;
|
||||
return null;
|
||||
} else if (!session.botId) {
|
||||
session = null;
|
||||
}
|
||||
@@ -184,18 +190,17 @@ export class ChatbotController {
|
||||
|
||||
public async findBotTrigger(
|
||||
botRepository: any,
|
||||
settingsRepository: any,
|
||||
content: string,
|
||||
instance: InstanceDto,
|
||||
session?: IntegrationSession,
|
||||
) {
|
||||
let findBot: null;
|
||||
let findBot: any = null;
|
||||
|
||||
if (!session) {
|
||||
findBot = await findBotByTrigger(botRepository, settingsRepository, content, instance.instanceId);
|
||||
findBot = await findBotByTrigger(botRepository, content, instance.instanceId);
|
||||
|
||||
if (!findBot) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
findBot = await botRepository.findFirst({
|
||||
|
||||
@@ -4,8 +4,10 @@ import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.rou
|
||||
import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router';
|
||||
import { Router } from 'express';
|
||||
|
||||
import { EvoaiRouter } from './evoai/routes/evoai.router';
|
||||
import { EvolutionBotRouter } from './evolutionBot/routes/evolutionBot.router';
|
||||
import { FlowiseRouter } from './flowise/routes/flowise.router';
|
||||
import { N8nRouter } from './n8n/routes/n8n.router';
|
||||
|
||||
export class ChatbotRouter {
|
||||
public readonly router: Router;
|
||||
@@ -19,5 +21,7 @@ export class ChatbotRouter {
|
||||
this.router.use('/openai', new OpenaiRouter(...guards).router);
|
||||
this.router.use('/dify', new DifyRouter(...guards).router);
|
||||
this.router.use('/flowise', new FlowiseRouter(...guards).router);
|
||||
this.router.use('/n8n', new N8nRouter(...guards).router);
|
||||
this.router.use('/evoai', new EvoaiRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema';
|
||||
export * from '@api/integrations/chatbot/dify/validate/dify.schema';
|
||||
export * from '@api/integrations/chatbot/evoai/validate/evoai.schema';
|
||||
export * from '@api/integrations/chatbot/evolutionBot/validate/evolutionBot.schema';
|
||||
export * from '@api/integrations/chatbot/flowise/validate/flowise.schema';
|
||||
export * from '@api/integrations/chatbot/n8n/validate/n8n.schema';
|
||||
export * from '@api/integrations/chatbot/openai/validate/openai.schema';
|
||||
export * from '@api/integrations/chatbot/typebot/validate/typebot.schema';
|
||||
|
||||
@@ -26,9 +26,9 @@ import axios from 'axios';
|
||||
import { proto } from 'baileys';
|
||||
import dayjs from 'dayjs';
|
||||
import FormData from 'form-data';
|
||||
import Jimp from 'jimp';
|
||||
import { Jimp, JimpMime } from 'jimp';
|
||||
import Long from 'long';
|
||||
import mime from 'mime';
|
||||
import mimeTypes from 'mime-types';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
@@ -295,6 +295,7 @@ export class ChatwootService {
|
||||
avatar_url?: string,
|
||||
jid?: string,
|
||||
) {
|
||||
try {
|
||||
const client = await this.clientCw(instance);
|
||||
|
||||
if (!client) {
|
||||
@@ -340,6 +341,11 @@ export class ChatwootService {
|
||||
await this.addLabelToContact(this.provider.nameInbox, contactId);
|
||||
|
||||
return contact;
|
||||
} catch (error) {
|
||||
this.logger.error('Error creating contact');
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async updateContact(instance: InstanceDto, id: number, data: any) {
|
||||
@@ -401,7 +407,6 @@ export class ChatwootService {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -452,6 +457,24 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
|
||||
private async mergeContacts(baseId: number, mergeId: number) {
|
||||
try {
|
||||
const contact = await chatwootRequest(this.getClientCwConfig(), {
|
||||
method: 'POST',
|
||||
url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
|
||||
body: {
|
||||
base_contact_id: baseId,
|
||||
mergee_contact_id: mergeId,
|
||||
},
|
||||
});
|
||||
|
||||
return contact;
|
||||
} catch {
|
||||
this.logger.error('Error merging contacts');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async mergeBrazilianContacts(contacts: any[]) {
|
||||
try {
|
||||
const contact = await chatwootRequest(this.getClientCwConfig(), {
|
||||
@@ -544,64 +567,95 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
public async createConversation(instance: InstanceDto, body: any) {
|
||||
try {
|
||||
this.logger.verbose('--- Start createConversation ---');
|
||||
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
|
||||
|
||||
const client = await this.clientCw(instance);
|
||||
|
||||
if (!client) {
|
||||
this.logger.warn(`Client not found for instance: ${JSON.stringify(instance)}`);
|
||||
if (!body?.key) {
|
||||
this.logger.warn(
|
||||
`body.key is null or undefined in createConversation. Full body object: ${JSON.stringify(body)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`;
|
||||
this.logger.verbose(`Cache key: ${cacheKey}`);
|
||||
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
|
||||
const remoteJid = body.key.remoteJid;
|
||||
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||||
const maxWaitTime = 5000; // 5 secounds
|
||||
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
this.logger.verbose(`Cache hit for key: ${cacheKey}`);
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Cached conversation ID: ${conversationId}`);
|
||||
let conversationExists: conversation | boolean;
|
||||
try {
|
||||
conversationExists = await client.conversations.get({
|
||||
accountId: this.provider.accountId,
|
||||
conversationId: conversationId,
|
||||
// Processa atualização de contatos já criados @lid
|
||||
if (isLid && body.key.senderPn !== body.key.previousRemoteJid) {
|
||||
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
|
||||
if (contact && contact.identifier !== body.key.senderPn) {
|
||||
this.logger.verbose(
|
||||
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn}`,
|
||||
);
|
||||
const updateContact = await this.updateContact(instance, contact.id, {
|
||||
identifier: body.key.senderPn,
|
||||
phone_number: `+${body.key.senderPn.split('@')[0]}`,
|
||||
});
|
||||
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting conversation: ${error}`);
|
||||
conversationExists = false;
|
||||
}
|
||||
if (!conversationExists) {
|
||||
this.logger.verbose('Conversation does not exist, re-calling createConversation');
|
||||
this.cache.delete(cacheKey);
|
||||
return await this.createConversation(instance, body);
|
||||
}
|
||||
|
||||
if (updateContact === null) {
|
||||
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
|
||||
if (baseContact) {
|
||||
await this.mergeContacts(baseContact.id, contact.id);
|
||||
this.logger.verbose(
|
||||
`Merge contacts: (${baseContact.id}) ${baseContact.phone_number} and (${contact.id}) ${contact.phone_number}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.logger.verbose(`--- Start createConversation ---`);
|
||||
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
|
||||
|
||||
// If it already exists in the cache, return conversationId
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
const isGroup = body.key.remoteJid.includes('@g.us');
|
||||
this.logger.verbose(`Is group: ${isGroup}`);
|
||||
|
||||
const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0];
|
||||
this.logger.verbose(`Chat ID: ${chatId}`);
|
||||
|
||||
let nameContact: string;
|
||||
|
||||
nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||
this.logger.verbose(`Name contact: ${nameContact}`);
|
||||
|
||||
const filterInbox = await this.getInbox(instance);
|
||||
|
||||
if (!filterInbox) {
|
||||
this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`);
|
||||
return null;
|
||||
// If lock already exists, wait until release or timeout
|
||||
if (await this.cache.has(lockKey)) {
|
||||
this.logger.verbose(`Operação de criação já em andamento para ${remoteJid}, aguardando resultado...`);
|
||||
const start = Date.now();
|
||||
while (await this.cache.has(lockKey)) {
|
||||
if (Date.now() - start > maxWaitTime) {
|
||||
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
||||
break;
|
||||
}
|
||||
await new Promise((res) => setTimeout(res, 300));
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||
return conversationId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adquire lock
|
||||
await this.cache.set(lockKey, true, 30);
|
||||
this.logger.verbose(`Bloqueio adquirido para: ${lockKey}`);
|
||||
|
||||
try {
|
||||
/*
|
||||
Double check after lock
|
||||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
||||
*/
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
return (await this.cache.get(cacheKey)) as number;
|
||||
}
|
||||
|
||||
const client = await this.clientCw(instance);
|
||||
if (!client) return null;
|
||||
|
||||
const isGroup = remoteJid.includes('@g.us');
|
||||
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
|
||||
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||
const filterInbox = await this.getInbox(instance);
|
||||
if (!filterInbox) return null;
|
||||
|
||||
if (isGroup) {
|
||||
this.logger.verbose('Processing group conversation');
|
||||
this.logger.verbose(`Processing group conversation`);
|
||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||
|
||||
@@ -639,9 +693,9 @@ export class ChatwootService {
|
||||
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
let contact = await this.findContact(instance, chatId);
|
||||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
||||
|
||||
if (contact) {
|
||||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
||||
if (!body.key.fromMe) {
|
||||
const waProfilePictureFile =
|
||||
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
|
||||
@@ -655,10 +709,8 @@ export class ChatwootService {
|
||||
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
|
||||
)
|
||||
: false);
|
||||
|
||||
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
|
||||
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
|
||||
|
||||
if (pictureNeedsUpdate || nameNeedsUpdate) {
|
||||
contact = await this.updateContact(instance, contact.id, {
|
||||
...(nameNeedsUpdate && { name: nameContact }),
|
||||
@@ -668,7 +720,6 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const jid = body.key.remoteJid;
|
||||
contact = await this.createContact(
|
||||
instance,
|
||||
chatId,
|
||||
@@ -676,12 +727,12 @@ export class ChatwootService {
|
||||
isGroup,
|
||||
nameContact,
|
||||
picture_url.profilePictureUrl || null,
|
||||
jid,
|
||||
remoteJid,
|
||||
);
|
||||
}
|
||||
|
||||
if (!contact) {
|
||||
this.logger.warn('Contact not created or found');
|
||||
this.logger.warn(`Contact not created or found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -695,38 +746,37 @@ export class ChatwootService {
|
||||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
||||
|
||||
if (!contactConversations || !contactConversations.payload) {
|
||||
this.logger.error('No conversations found or payload is undefined');
|
||||
this.logger.error(`No conversations found or payload is undefined`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contactConversations.payload.length) {
|
||||
let conversation: any;
|
||||
let inboxConversation = contactConversations.payload.find(
|
||||
(conversation) => conversation.inbox_id == filterInbox.id,
|
||||
);
|
||||
if (inboxConversation) {
|
||||
if (this.provider.reopenConversation) {
|
||||
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
|
||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
|
||||
|
||||
if (this.provider.conversationPending) {
|
||||
if (conversation) {
|
||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
|
||||
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
|
||||
await client.conversations.toggleStatus({
|
||||
accountId: this.provider.accountId,
|
||||
conversationId: conversation.id,
|
||||
conversationId: inboxConversation.id,
|
||||
data: {
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conversation = contactConversations.payload.find(
|
||||
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
||||
inboxConversation = contactConversations.payload.find(
|
||||
(conversation) =>
|
||||
conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
||||
);
|
||||
this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`);
|
||||
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
|
||||
}
|
||||
|
||||
if (conversation) {
|
||||
this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`);
|
||||
this.cache.set(cacheKey, conversation.id);
|
||||
return conversation.id;
|
||||
if (inboxConversation) {
|
||||
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
||||
this.cache.set(cacheKey, inboxConversation.id);
|
||||
return inboxConversation.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,21 +789,34 @@ export class ChatwootService {
|
||||
data['status'] = 'pending';
|
||||
}
|
||||
|
||||
/*
|
||||
Triple check after lock
|
||||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
||||
*/
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
return (await this.cache.get(cacheKey)) as number;
|
||||
}
|
||||
|
||||
const conversation = await client.conversations.create({
|
||||
accountId: this.provider.accountId,
|
||||
data,
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
this.logger.warn('Conversation not created or found');
|
||||
this.logger.warn(`Conversation not created or found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.verbose(`New conversation created with ID: ${conversation.id}`);
|
||||
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
|
||||
this.cache.set(cacheKey, conversation.id);
|
||||
return conversation.id;
|
||||
} finally {
|
||||
await this.cache.delete(lockKey);
|
||||
this.logger.verbose(`Block released for: ${lockKey}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in createConversation: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,12 +995,14 @@ export class ChatwootService {
|
||||
quotedMsg?: MessageModel,
|
||||
) {
|
||||
if (sourceId && this.isImportHistoryAvailable()) {
|
||||
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]);
|
||||
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
|
||||
if (messageAlreadySaved) {
|
||||
if (messageAlreadySaved.size > 0) {
|
||||
this.logger.warn('Message already saved on chatwoot');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
const data = new FormData();
|
||||
|
||||
if (content) {
|
||||
@@ -1065,7 +1130,7 @@ export class ChatwootService {
|
||||
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
||||
try {
|
||||
const parsedMedia = path.parse(decodeURIComponent(media));
|
||||
let mimeType = mime.getType(parsedMedia?.ext) || '';
|
||||
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
|
||||
let fileName = parsedMedia?.name + parsedMedia?.ext;
|
||||
|
||||
if (!mimeType) {
|
||||
@@ -1105,12 +1170,13 @@ export class ChatwootService {
|
||||
|
||||
sendTelemetry('/message/sendWhatsAppAudio');
|
||||
|
||||
const messageSent = await waInstance?.audioWhatsapp(data, true);
|
||||
const messageSent = await waInstance?.audioWhatsapp(data, null, true);
|
||||
|
||||
return messageSent;
|
||||
}
|
||||
|
||||
if (type === 'image' && parsedMedia && parsedMedia?.ext === '.gif') {
|
||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
||||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||||
type = 'document';
|
||||
}
|
||||
|
||||
@@ -1652,7 +1718,7 @@ export class ChatwootService {
|
||||
stickerMessage: undefined,
|
||||
documentMessage: msg.documentMessage?.caption,
|
||||
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
|
||||
audioMessage: msg.audioMessage?.caption,
|
||||
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
|
||||
contactMessage: msg.contactMessage?.vcard,
|
||||
contactsArrayMessage: msg.contactsArrayMessage,
|
||||
locationMessage: msg.locationMessage,
|
||||
@@ -1834,6 +1900,12 @@ export class ChatwootService {
|
||||
|
||||
public async eventWhatsapp(event: string, instance: InstanceDto, body: any) {
|
||||
try {
|
||||
// Ignore events that are not messages (like EPHEMERAL_SYNC_RESPONSE)
|
||||
if (body?.type && body.type !== 'message' && body.type !== 'conversation') {
|
||||
this.logger.verbose(`Ignoring non-message event type: ${body.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||
|
||||
if (!waInstance) {
|
||||
@@ -1879,6 +1951,11 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
if (event === 'messages.upsert' || event === 'send.message') {
|
||||
if (!body?.key) {
|
||||
this.logger.warn(`body.key is null or undefined. Full body object: ${JSON.stringify(body)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.key.remoteJid === 'status@broadcast') {
|
||||
return;
|
||||
}
|
||||
@@ -1897,7 +1974,7 @@ export class ChatwootService {
|
||||
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
|
||||
: originalMessage;
|
||||
|
||||
if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) {
|
||||
if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1957,7 +2034,7 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
if (!nameFile) {
|
||||
nameFile = `${Math.random().toString(36).substring(7)}.${mime.getExtension(downloadBase64.mimetype) || ''}`;
|
||||
nameFile = `${Math.random().toString(36).substring(7)}.${mimeTypes.extension(downloadBase64.mimetype) || ''}`;
|
||||
}
|
||||
|
||||
const fileData = Buffer.from(downloadBase64.base64, 'base64');
|
||||
@@ -1969,11 +2046,21 @@ export class ChatwootService {
|
||||
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||
|
||||
let formattedPhoneNumber: string;
|
||||
|
||||
if (phoneMatch) {
|
||||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
||||
} else {
|
||||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
||||
}
|
||||
|
||||
let content: string;
|
||||
|
||||
if (!body.key.fromMe) {
|
||||
content = `**${participantName}:**\n\n${bodyMessage}`;
|
||||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
||||
} else {
|
||||
content = `${bodyMessage}`;
|
||||
}
|
||||
@@ -2046,8 +2133,8 @@ export class ChatwootService {
|
||||
if (isAdsMessage) {
|
||||
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
|
||||
|
||||
const extension = mime.getExtension(imgBuffer.headers['content-type']);
|
||||
const mimeType = extension && mime.getType(extension);
|
||||
const extension = mimeTypes.extension(imgBuffer.headers['content-type']);
|
||||
const mimeType = extension && mimeTypes.lookup(extension);
|
||||
|
||||
if (!mimeType) {
|
||||
this.logger.warn('mimetype of Ads message not found');
|
||||
@@ -2055,13 +2142,15 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
const random = Math.random().toString(36).substring(7);
|
||||
const nameFile = `${random}.${mime.getExtension(mimeType)}`;
|
||||
const nameFile = `${random}.${mimeTypes.extension(mimeType)}`;
|
||||
const fileData = Buffer.from(imgBuffer.data, 'binary');
|
||||
|
||||
const img = await Jimp.read(fileData);
|
||||
await img.cover(320, 180);
|
||||
|
||||
const processedBuffer = await img.getBufferAsync(Jimp.MIME_PNG);
|
||||
await img.cover({
|
||||
w: 320,
|
||||
h: 180,
|
||||
});
|
||||
const processedBuffer = await img.getBuffer(JimpMime.png);
|
||||
|
||||
const fileStream = new Readable();
|
||||
fileStream._read = () => {}; // _read is required but you can noop it
|
||||
@@ -2098,11 +2187,21 @@ export class ChatwootService {
|
||||
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||
|
||||
let formattedPhoneNumber: string;
|
||||
|
||||
if (phoneMatch) {
|
||||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
||||
} else {
|
||||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
||||
}
|
||||
|
||||
let content: string;
|
||||
|
||||
if (!body.key.fromMe) {
|
||||
content = `**${participantName}**\n\n${bodyMessage}`;
|
||||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
||||
} else {
|
||||
content = `${bodyMessage}`;
|
||||
}
|
||||
@@ -2178,11 +2277,24 @@ export class ChatwootService {
|
||||
}
|
||||
}
|
||||
|
||||
if (event === 'messages.edit') {
|
||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||
// Ignore events that are not messages (like EPHEMERAL_SYNC_RESPONSE)
|
||||
if (body?.type && body.type !== 'message') {
|
||||
this.logger.verbose(`Ignoring non-message event type: ${body.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body?.key?.id) {
|
||||
this.logger.warn(
|
||||
`body.key.id is null or undefined in messages.edit. Full body object: ${JSON.stringify(body)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const editedText = `${
|
||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
||||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
||||
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||||
const message = await this.getMessageByKeyId(instance, body.key.id);
|
||||
const key = message.key as {
|
||||
id: string;
|
||||
fromMe: boolean;
|
||||
@@ -2442,6 +2554,7 @@ export class ChatwootService {
|
||||
chatwootConfig: ChatwootDto,
|
||||
prepareMessage: (message: any) => any,
|
||||
) {
|
||||
try {
|
||||
if (!this.isImportHistoryAvailable()) {
|
||||
return;
|
||||
}
|
||||
@@ -2494,5 +2607,8 @@ export class ChatwootService {
|
||||
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
|
||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||
waInstance.clearCacheChatwoot();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,23 +169,34 @@ class ChatwootImport {
|
||||
}
|
||||
}
|
||||
|
||||
public async getExistingSourceIds(sourceIds: string[]): Promise<Set<string>> {
|
||||
public async getExistingSourceIds(sourceIds: string[], conversationId?: number): Promise<Set<string>> {
|
||||
try {
|
||||
const existingSourceIdsSet = new Set<string>();
|
||||
|
||||
if (sourceIds.length === 0) {
|
||||
return existingSourceIdsSet;
|
||||
}
|
||||
|
||||
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890
|
||||
const query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
|
||||
// Ensure all sourceIds are consistently prefixed with 'WAID:' as required by downstream systems and database queries.
|
||||
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`);
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
const result = await pgClient.query(query, [formattedSourceIds]);
|
||||
|
||||
const params = conversationId ? [formattedSourceIds, conversationId] : [formattedSourceIds];
|
||||
|
||||
const query = conversationId
|
||||
? 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2'
|
||||
: 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
|
||||
|
||||
const result = await pgClient.query(query, params);
|
||||
for (const row of result.rows) {
|
||||
existingSourceIdsSet.add(row.source_id);
|
||||
}
|
||||
|
||||
return existingSourceIdsSet;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on getExistingSourceIds: ${error.toString()}`);
|
||||
return new Set<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public async importHistoryMessages(
|
||||
@@ -495,25 +506,30 @@ class ChatwootImport {
|
||||
stickerMessage: msg.message.stickerMessage,
|
||||
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
|
||||
};
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
|
||||
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null);
|
||||
switch (typeKey) {
|
||||
case 'documentMessage':
|
||||
return `_<File: ${msg.message.documentMessage.fileName}${
|
||||
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
|
||||
}>_`;
|
||||
case 'documentMessage': {
|
||||
const doc = msg.message.documentMessage;
|
||||
const fileName = doc?.fileName || 'document';
|
||||
const caption = doc?.caption ? ` ${doc.caption}` : '';
|
||||
return `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
|
||||
case 'documentWithCaptionMessage':
|
||||
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${
|
||||
msg.message.documentWithCaptionMessage.message.documentMessage.caption
|
||||
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}`
|
||||
: ''
|
||||
}>_`;
|
||||
case 'documentWithCaptionMessage': {
|
||||
const doc = msg.message.documentWithCaptionMessage?.message?.documentMessage;
|
||||
const fileName = doc?.fileName || 'document';
|
||||
const caption = doc?.caption ? ` ${doc.caption}` : '';
|
||||
return `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
|
||||
case 'templateMessage':
|
||||
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText
|
||||
? `*${msg.message.templateMessage.hydratedTemplate.hydratedTitleText}*\\n`
|
||||
: '' + msg.message.templateMessage.hydratedTemplate.hydratedContentText;
|
||||
case 'templateMessage': {
|
||||
const template = msg.message.templateMessage?.hydratedTemplate;
|
||||
return (
|
||||
(template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
|
||||
(template?.hydratedContentText || '')
|
||||
);
|
||||
}
|
||||
|
||||
case 'imageMessage':
|
||||
return '_<Image Message>_';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { DifyDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
|
||||
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
|
||||
@@ -7,12 +6,11 @@ import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Dify } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Dify as DifyModel } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import { Dify as DifyModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class DifyController extends ChatbotController implements ChatbotControllerInterface {
|
||||
export class DifyController extends BaseChatbotController<DifyModel, DifyDto> {
|
||||
constructor(
|
||||
private readonly difyService: DifyService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@@ -26,6 +24,7 @@ export class DifyController extends ChatbotController implements ChatbotControll
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('DifyController');
|
||||
protected readonly integrationName = 'Dify';
|
||||
|
||||
integrationEnabled = configService.get<Dify>('DIFY').ENABLED;
|
||||
botRepository: any;
|
||||
@@ -33,7 +32,55 @@ export class DifyController extends ChatbotController implements ChatbotControll
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Bots
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.fallbackId;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'difyIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'dify';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: DifyDto): Record<string, any> {
|
||||
return {
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: DifyDto): Record<string, any> {
|
||||
return {
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: DifyDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Override createBot to add Dify-specific validation
|
||||
public async createBot(instance: InstanceDto, data: DifyDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
@@ -45,842 +92,35 @@ export class DifyController extends ChatbotController implements ChatbotControll
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
// Dify-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Process Dify-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: DifyModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
|
||||
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
|
||||
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
|
||||
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
|
||||
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
|
||||
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
|
||||
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
|
||||
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
|
||||
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
|
||||
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
|
||||
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating dify');
|
||||
}
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: DifyDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
botType: data.botType,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating dify');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting dify bot');
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
difyIdFallback: data.difyIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
difyIdFallback: updateSettings.difyIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
difyIdFallback: data.difyIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
difyIdFallback: newSetttings.difyIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
difyIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
difyIdFallback: settings.difyIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'dify',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
if (!this.integrationEnabled) return;
|
||||
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(
|
||||
this.botRepository,
|
||||
this.settingsRepository,
|
||||
content,
|
||||
instance,
|
||||
session,
|
||||
)) as DifyModel;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.difyIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.difyIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (!expire) expire = settings.expire;
|
||||
if (!keywordFinish) keywordFinish = settings.keywordFinish;
|
||||
if (!delayMessage) delayMessage = settings.delayMessage;
|
||||
if (!unknownMessage) unknownMessage = settings.unknownMessage;
|
||||
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
|
||||
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (!keepOpen) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (!ignoreJids) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.difyService.processDify(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.difyService.processDify(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
await this.difyService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,13 @@
|
||||
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { $Enums } from '@prisma/client';
|
||||
|
||||
export class DifyDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class DifyDto extends BaseChatbotDto {
|
||||
botType?: $Enums.DifyBotType;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class DifySettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
export class DifySettingDto extends BaseChatbotSettingDto {
|
||||
difyIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
@@ -1,60 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Dify, DifySetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export class DifyService {
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class DifyService extends BaseChatbotService<Dify, DifySetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('DifyService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'dify',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'DifyService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
/**
|
||||
* Return the bot type for Dify
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'dify';
|
||||
}
|
||||
|
||||
private isJSON(str: string): boolean {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageToBot(
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: DifySetting,
|
||||
@@ -62,10 +36,30 @@ export class DifyService {
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
) {
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
let endpoint: string = dify.apiUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Dify endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle audio messages - transcribe using OpenAI Whisper
|
||||
let processedContent = content;
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[Dify] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
processedContent = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Dify] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dify.botType === 'chatBot') {
|
||||
endpoint += '/chat-messages';
|
||||
const payload: any = {
|
||||
@@ -74,17 +68,17 @@ export class DifyService {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
query: content,
|
||||
query: processedContent,
|
||||
response_mode: 'blocking',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@@ -112,7 +106,9 @@ export class DifyService {
|
||||
const message = response?.data?.answer;
|
||||
const conversationId = response?.data?.conversation_id;
|
||||
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@@ -130,21 +126,21 @@ export class DifyService {
|
||||
endpoint += '/completion-messages';
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
query: content,
|
||||
query: processedContent,
|
||||
pushName: pushName,
|
||||
remoteJid: remoteJid,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
response_mode: 'blocking',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@@ -172,7 +168,9 @@ export class DifyService {
|
||||
const message = response?.data?.answer;
|
||||
const conversationId = response?.data?.conversation_id;
|
||||
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@@ -194,17 +192,17 @@ export class DifyService {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
query: content,
|
||||
query: processedContent,
|
||||
response_mode: 'streaming',
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
// Handle image messages
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
@@ -224,23 +222,12 @@ export class DifyService {
|
||||
headers: {
|
||||
Authorization: `Bearer ${dify.apiKey}`,
|
||||
},
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
let conversationId;
|
||||
let answer = '';
|
||||
|
||||
const stream = response.data;
|
||||
const reader = new Readable().wrap(stream);
|
||||
|
||||
reader.on('data', (chunk) => {
|
||||
const data = chunk.toString().replace(/data:\s*/g, '');
|
||||
|
||||
if (data.trim() === '' || !data.startsWith('{')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data.replaceAll('data: ', '');
|
||||
const events = data.split('\n').filter((line) => line.trim() !== '');
|
||||
|
||||
for (const eventString of events) {
|
||||
@@ -254,18 +241,13 @@ export class DifyService {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing stream data:', error);
|
||||
}
|
||||
});
|
||||
|
||||
reader.on('end', async () => {
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = answer;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
if (answer) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, answer, settings);
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
@@ -274,374 +256,13 @@ export class DifyService {
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
sessionId: conversationId,
|
||||
sessionId: session.sessionId === remoteJid ? conversationId : session.sessionId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
reader.on('error', (error) => {
|
||||
console.error('Error reading stream:', error);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dify.botType === 'workflow') {
|
||||
endpoint += '/workflows/run';
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
query: content,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
},
|
||||
response_mode: 'blocking',
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
payload.files = [
|
||||
{
|
||||
type: 'image',
|
||||
transfer_method: 'remote_url',
|
||||
url: contentSplit[1].split('?')[0],
|
||||
},
|
||||
];
|
||||
payload.inputs.query = contentSplit[2] || content;
|
||||
}
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${dify.apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = response?.data?.data.outputs.text;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error.response?.data || error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessageWhatsApp(instance: any, remoteJid: string, message: string, settings: DifySetting) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
textBuffer = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
dify: Dify,
|
||||
settings: DifySetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: dify.id,
|
||||
});
|
||||
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async processDify(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
dify: Dify,
|
||||
session: IntegrationSession,
|
||||
settings: DifySetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && settings.expire && settings.expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
|
||||
|
||||
const diff = now - sessionUpdatedAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > settings.expire) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: dify.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, dify, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
if (settings.unknownMessage) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings.delayMessage || 1000,
|
||||
text: settings.unknownMessage,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: dify.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendMessageToBot(instance, session, settings, dify, remoteJid, pushName, content);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { EvoaiDto } from '@api/integrations/chatbot/evoai/dto/evoai.dto';
|
||||
import { EvoaiService } from '@api/integrations/chatbot/evoai/services/evoai.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Evoai } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Evoai as EvoaiModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class EvoaiController extends BaseChatbotController<EvoaiModel, EvoaiDto> {
|
||||
constructor(
|
||||
private readonly evoaiService: EvoaiService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.evoai;
|
||||
this.settingsRepository = this.prismaRepository.evoaiSetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('EvoaiController');
|
||||
protected readonly integrationName = 'Evoai';
|
||||
|
||||
integrationEnabled = configService.get<Evoai>('EVOAI').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.evoaiIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'evoaiIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'evoai';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: EvoaiDto): Record<string, any> {
|
||||
return {
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: EvoaiDto): Record<string, any> {
|
||||
return {
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: EvoaiDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Evoai already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Override createBot to add EvoAI-specific validation
|
||||
public async createBot(instance: InstanceDto, data: EvoaiDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Evoai is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// EvoAI-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
agentUrl: data.agentUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Evoai already exists');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Process Evoai-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvoaiModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.evoaiService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
||||
10
src/api/integrations/chatbot/evoai/dto/evoai.dto.ts
Normal file
10
src/api/integrations/chatbot/evoai/dto/evoai.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class EvoaiDto extends BaseChatbotDto {
|
||||
agentUrl?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export class EvoaiSettingDto extends BaseChatbotSettingDto {
|
||||
evoaiIdFallback?: string;
|
||||
}
|
||||
124
src/api/integrations/chatbot/evoai/routes/evoai.router.ts
Normal file
124
src/api/integrations/chatbot/evoai/routes/evoai.router.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { evoaiController } from '@api/server.module';
|
||||
import {
|
||||
evoaiIgnoreJidSchema,
|
||||
evoaiSchema,
|
||||
evoaiSettingSchema,
|
||||
evoaiStatusSchema,
|
||||
instanceSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { EvoaiDto, EvoaiSettingDto } from '../dto/evoai.dto';
|
||||
|
||||
export class EvoaiRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiDto>({
|
||||
request: req,
|
||||
schema: evoaiSchema,
|
||||
ClassRef: EvoaiDto,
|
||||
execute: (instance, data) => evoaiController.createBot(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.findBot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchBot(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiDto>({
|
||||
request: req,
|
||||
schema: evoaiSchema,
|
||||
ClassRef: EvoaiDto,
|
||||
execute: (instance, data) => evoaiController.updateBot(instance, req.params.evoaiId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.deleteBot(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<EvoaiSettingDto>({
|
||||
request: req,
|
||||
schema: evoaiSettingSchema,
|
||||
ClassRef: EvoaiSettingDto,
|
||||
execute: (instance, data) => evoaiController.settings(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchSettings(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: evoaiStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => evoaiController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:evoaiId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => evoaiController.fetchSessions(instance, req.params.evoaiId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: evoaiIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => evoaiController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
186
src/api/integrations/chatbot/evoai/services/evoai.service.ts
Normal file
186
src/api/integrations/chatbot/evoai/services/evoai.service.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Evoai, EvoaiSetting, IntegrationSession } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import { downloadMediaMessage } from 'baileys';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'EvoaiService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bot type for EvoAI
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'evoai';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the abstract method to send message to EvoAI API
|
||||
* Handles audio transcription, image processing, and complex JSON-RPC payload
|
||||
*/
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: EvoaiSetting,
|
||||
evoai: Evoai,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.debug(`[EvoAI] Sending message to bot with content: ${content}`);
|
||||
|
||||
let processedContent = content;
|
||||
|
||||
// Handle audio messages - transcribe using OpenAI Whisper
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[EvoAI] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
processedContent = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[EvoAI] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint: string = evoai.agentUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No EvoAI endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
const callId = `req-${uuidv4().substring(0, 8)}`;
|
||||
const messageId = remoteJid.split('@')[0] || uuidv4(); // Use phone number as messageId
|
||||
|
||||
// Prepare message parts
|
||||
const parts = [
|
||||
{
|
||||
type: 'text',
|
||||
text: processedContent,
|
||||
},
|
||||
];
|
||||
|
||||
// Handle image message if present
|
||||
if (this.isImageMessage(content) && msg) {
|
||||
const contentSplit = content.split('|');
|
||||
parts[0].text = contentSplit[2] || content;
|
||||
|
||||
try {
|
||||
// Download the image
|
||||
const mediaBuffer = await downloadMediaMessage(msg, 'buffer', {});
|
||||
const fileContent = Buffer.from(mediaBuffer).toString('base64');
|
||||
const fileName = contentSplit[2] || `${msg.key?.id || 'image'}.jpg`;
|
||||
|
||||
parts.push({
|
||||
type: 'file',
|
||||
file: {
|
||||
name: fileName,
|
||||
mimeType: 'image/jpeg',
|
||||
bytes: fileContent,
|
||||
},
|
||||
} as any);
|
||||
} catch (fileErr) {
|
||||
this.logger.error(`[EvoAI] Failed to process image: ${fileErr}`);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
jsonrpc: '2.0',
|
||||
id: callId,
|
||||
method: 'message/send',
|
||||
params: {
|
||||
contextId: session.sessionId,
|
||||
message: {
|
||||
role: 'user',
|
||||
parts,
|
||||
messageId: messageId,
|
||||
metadata: {
|
||||
messageKey: msg?.key,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
fromMe: msg?.key?.fromMe,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.logger.debug(`[EvoAI] Sending request to: ${endpoint}`);
|
||||
// Redact base64 file bytes from payload log
|
||||
const redactedPayload = JSON.parse(JSON.stringify(payload));
|
||||
if (redactedPayload?.params?.message?.parts) {
|
||||
redactedPayload.params.message.parts = redactedPayload.params.message.parts.map((part) => {
|
||||
if (part.type === 'file' && part.file && part.file.bytes) {
|
||||
return { ...part, file: { ...part.file, bytes: '[base64 omitted]' } };
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
this.logger.debug(`[EvoAI] Payload: ${JSON.stringify(redactedPayload)}`);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers: {
|
||||
'x-api-key': evoai.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.debug(`[EvoAI] Response: ${JSON.stringify(response.data)}`);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
let message = undefined;
|
||||
const result = response?.data?.result;
|
||||
|
||||
// Extract message from artifacts array
|
||||
if (result?.artifacts && Array.isArray(result.artifacts) && result.artifacts.length > 0) {
|
||||
const artifact = result.artifacts[0];
|
||||
if (artifact?.parts && Array.isArray(artifact.parts)) {
|
||||
const textPart = artifact.parts.find((p) => p.type === 'text' && p.text);
|
||||
if (textPart) message = textPart.text;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(`[EvoAI] Extracted message to send: ${message}`);
|
||||
|
||||
if (message) {
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[EvoAI] Error sending message: ${error?.response?.data ? JSON.stringify(error.response.data) : error}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/api/integrations/chatbot/evoai/validate/evoai.schema.ts
Normal file
115
src/api/integrations/chatbot/evoai/validate/evoai.schema.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
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 evoaiSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
agentUrl: { type: 'string' },
|
||||
apiKey: { type: 'string' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'agentUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'agentUrl', 'triggerType'),
|
||||
};
|
||||
|
||||
export const evoaiStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const evoaiSettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
botIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
),
|
||||
};
|
||||
|
||||
export const evoaiIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
@@ -1,16 +1,13 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { EvolutionBot } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import { EvolutionBot, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { EvolutionBotDto } from '../dto/evolutionBot.dto';
|
||||
import { EvolutionBotService } from '../services/evolutionBot.service';
|
||||
|
||||
export class EvolutionBotController extends ChatbotController implements ChatbotControllerInterface {
|
||||
export class EvolutionBotController extends BaseChatbotController<EvolutionBot, EvolutionBotDto> {
|
||||
constructor(
|
||||
private readonly evolutionBotService: EvolutionBotService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@@ -24,835 +21,76 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('EvolutionBotController');
|
||||
protected readonly integrationName = 'EvolutionBot';
|
||||
|
||||
integrationEnabled: boolean;
|
||||
integrationEnabled = true; // Set to true by default or use config value if available
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: EvolutionBotDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
// Implementation of abstract methods required by BaseChatbotController
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.botIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'botIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'evolution';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: EvolutionBotDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: EvolutionBotDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(
|
||||
botId: string,
|
||||
instanceId: string,
|
||||
data: EvolutionBotDto,
|
||||
): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Evolution Bot already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Process bot-specific logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvolutionBot,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
|
||||
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
|
||||
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
|
||||
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
|
||||
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
|
||||
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
|
||||
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
|
||||
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
|
||||
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
|
||||
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
|
||||
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a dify with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Dify already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating bot');
|
||||
}
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: EvolutionBotDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Bot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating bot');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting bot');
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
botIdFallback: data.botIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
botIdFallback: updateSettings.botIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
botIdFallback: data.botIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
botIdFallback: newSetttings.botIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
botIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
botIdFallback: settings.botIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'evolution',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(
|
||||
this.botRepository,
|
||||
this.settingsRepository,
|
||||
content,
|
||||
instance,
|
||||
session,
|
||||
)) as EvolutionBot;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.botIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.botIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (!expire) expire = settings.expire;
|
||||
if (!keywordFinish) keywordFinish = settings.keywordFinish;
|
||||
if (!delayMessage) delayMessage = settings.delayMessage;
|
||||
if (!unknownMessage) unknownMessage = settings.unknownMessage;
|
||||
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
|
||||
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (!keepOpen) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (!ignoreJids) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.evolutionBotService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.evolutionBotService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
await this.evolutionBotService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,10 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class EvolutionBotDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
export class EvolutionBotDto extends BaseChatbotDto {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export class EvolutionBotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
export class EvolutionBotSettingDto extends BaseChatbotSettingDto {
|
||||
botIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
@@ -1,71 +1,76 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { EvolutionBot, EvolutionBotSetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
|
||||
export class EvolutionBotService {
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class EvolutionBotService extends BaseChatbotService<EvolutionBot, EvolutionBotSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('EvolutionBotService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'evolution',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'EvolutionBotService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
/**
|
||||
* Get the bot type identifier
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'evolution';
|
||||
}
|
||||
|
||||
private async sendMessageToBot(
|
||||
/**
|
||||
* Send a message to the Evolution Bot API
|
||||
*/
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: EvolutionBotSetting,
|
||||
bot: EvolutionBot,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
) {
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const payload: any = {
|
||||
inputs: {
|
||||
sessionId: session.id,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
fromMe: msg?.key?.fromMe,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
query: content,
|
||||
conversation_id: session.sessionId === remoteJid ? undefined : session.sessionId,
|
||||
user: remoteJid,
|
||||
};
|
||||
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[EvolutionBot] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.query = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[EvolutionBot] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
@@ -83,6 +88,13 @@ export class EvolutionBotService {
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
const endpoint = bot.apiUrl;
|
||||
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Evolution Bot endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
let headers: any = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
@@ -94,335 +106,33 @@ export class EvolutionBotService {
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios.post(bot.apiUrl, payload, {
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
|
||||
const message = response?.data?.message;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private async sendMessageWhatsApp(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
session: IntegrationSession,
|
||||
settings: EvolutionBotSetting,
|
||||
message: string,
|
||||
) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
textBuffer = '';
|
||||
|
||||
let message = response?.data?.message;
|
||||
|
||||
if (message && typeof message === 'string' && message.startsWith("'") && message.endsWith("'")) {
|
||||
const innerContent = message.slice(1, -1);
|
||||
if (!innerContent.includes("'")) {
|
||||
message = innerContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
textBuffer = '';
|
||||
}
|
||||
if (message) {
|
||||
// Use the base class method to send the message to WhatsApp
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
|
||||
// Send telemetry
|
||||
sendTelemetry('/message/sendText');
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvolutionBot,
|
||||
settings: EvolutionBotSetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: bot.id,
|
||||
});
|
||||
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: EvolutionBot,
|
||||
session: IntegrationSession,
|
||||
settings: EvolutionBotSetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && settings.expire && settings.expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
|
||||
|
||||
const diff = now - sessionUpdatedAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > settings.expire) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in sendMessageToBot: ${error.message || JSON.stringify(error)}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
if (settings.unknownMessage) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings.delayMessage || 1000,
|
||||
text: settings.unknownMessage,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, session, bot, remoteJid, pushName, content);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService, Flowise } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Flowise } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||
|
||||
import { ChatbotController, ChatbotControllerInterface, EmitData } from '../../chatbot.controller';
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
import { FlowiseDto } from '../dto/flowise.dto';
|
||||
import { FlowiseService } from '../services/flowise.service';
|
||||
|
||||
export class FlowiseController extends ChatbotController implements ChatbotControllerInterface {
|
||||
export class FlowiseController extends BaseChatbotController<FlowiseModel, FlowiseDto> {
|
||||
constructor(
|
||||
private readonly flowiseService: FlowiseService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@@ -24,15 +24,73 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('FlowiseController');
|
||||
protected readonly integrationName = 'Flowise';
|
||||
|
||||
integrationEnabled: boolean;
|
||||
integrationEnabled = configService.get<Flowise>('FLOWISE').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Bots
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.flowiseIdFallback;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'flowiseIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'flowise';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: FlowiseDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
protected getAdditionalUpdateFields(data: FlowiseDto): Record<string, any> {
|
||||
return {
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: FlowiseDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Flowise already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Process Flowise-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: FlowiseModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.flowiseService.processBot(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
|
||||
// Override createBot to add module availability check and Flowise-specific validation
|
||||
public async createBot(instance: InstanceDto, data: FlowiseDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Flowise is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
@@ -41,66 +99,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids ||
|
||||
!data.splitMessages ||
|
||||
!data.timePerChar
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
|
||||
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
|
||||
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
|
||||
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
|
||||
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
|
||||
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
|
||||
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
|
||||
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
|
||||
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
|
||||
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
|
||||
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a Flowise with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
|
||||
// Flowise-specific duplicate check
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
@@ -113,746 +112,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
|
||||
throw new Error('Flowise already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating bot');
|
||||
}
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: FlowiseDto) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error('You already have a bot with an "All" trigger, you cannot have more bots while it is active');
|
||||
}
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Bot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating bot');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting bot');
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
flowiseIdFallback: data.flowiseIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
flowiseIdFallback: updateSettings.flowiseIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
splitMessages: updateSettings.splitMessages,
|
||||
timePerChar: updateSettings.timePerChar,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
flowiseIdFallback: data.flowiseIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
splitMessages: data.splitMessages,
|
||||
timePerChar: data.timePerChar,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
flowiseIdFallback: newSetttings.flowiseIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
splitMessages: newSetttings.splitMessages,
|
||||
timePerChar: newSetttings.timePerChar,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
splitMessages: false,
|
||||
timePerChar: 0,
|
||||
flowiseIdFallback: '',
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
splitMessages: settings.splitMessages,
|
||||
timePerChar: settings.timePerChar,
|
||||
flowiseIdFallback: settings.flowiseIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { bot: { remoteJid: remoteJid, status: status } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
|
||||
} else {
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const botData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
return { bot: { ...instance, bot: botData } };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (bot && bot.instanceId !== instanceId) {
|
||||
throw new Error('Dify not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: bot ? botId : { not: null },
|
||||
type: 'flowise',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
public async emit({ instance, remoteJid, msg }: EmitData) {
|
||||
try {
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(
|
||||
this.botRepository,
|
||||
this.settingsRepository,
|
||||
content,
|
||||
instance,
|
||||
session,
|
||||
)) as Flowise;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.flowiseIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.flowiseIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
let splitMessages = findBot?.splitMessages;
|
||||
let timePerChar = findBot?.timePerChar;
|
||||
|
||||
if (!expire) expire = settings.expire;
|
||||
if (!keywordFinish) keywordFinish = settings.keywordFinish;
|
||||
if (!delayMessage) delayMessage = settings.delayMessage;
|
||||
if (!unknownMessage) unknownMessage = settings.unknownMessage;
|
||||
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
|
||||
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (!keepOpen) keepOpen = settings.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
|
||||
if (!ignoreJids) ignoreJids = settings.ignoreJids;
|
||||
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
|
||||
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.flowiseService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
debouncedContent,
|
||||
msg?.pushName,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.flowiseService.processBot(
|
||||
this.waMonitor.waInstances[instance.instanceName],
|
||||
remoteJid,
|
||||
findBot,
|
||||
session,
|
||||
{
|
||||
...settings,
|
||||
expire,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debounceTime,
|
||||
ignoreJids,
|
||||
splitMessages,
|
||||
timePerChar,
|
||||
},
|
||||
content,
|
||||
msg?.pushName,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
// Let the base class handle the rest
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,10 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class FlowiseDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
export class FlowiseDto extends BaseChatbotDto {
|
||||
apiUrl: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class FlowiseSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
export class FlowiseSettingDto extends BaseChatbotSettingDto {
|
||||
flowiseIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { Integration } from '@api/types/wa.types';
|
||||
import { Auth, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Flowise, FlowiseSetting, IntegrationSession } from '@prisma/client';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { Flowise as FlowiseModel, IntegrationSession } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
export class FlowiseService {
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class FlowiseService extends BaseChatbotService<FlowiseModel> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('FlowiseService');
|
||||
|
||||
public async createNewSession(instance: InstanceDto, data: any) {
|
||||
try {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data.pushName,
|
||||
sessionId: data.remoteJid,
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
botId: data.botId,
|
||||
instanceId: instance.instanceId,
|
||||
type: 'flowise',
|
||||
},
|
||||
});
|
||||
|
||||
return { session };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'FlowiseService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
private isImageMessage(content: string) {
|
||||
return content.includes('imageMessage');
|
||||
// Return the bot type for Flowise
|
||||
protected getBotType(): string {
|
||||
return 'flowise';
|
||||
}
|
||||
|
||||
private async sendMessageToBot(instance: any, bot: Flowise, remoteJid: string, pushName: string, content: string) {
|
||||
// Process Flowise-specific bot logic
|
||||
public async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: FlowiseModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
await this.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
|
||||
// Implement the abstract method to send message to Flowise API
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
bot: FlowiseModel,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
): Promise<void> {
|
||||
const payload: any = {
|
||||
question: content,
|
||||
overrideConfig: {
|
||||
@@ -54,11 +61,24 @@ export class FlowiseService {
|
||||
pushName: pushName,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: this.configService.get<Auth>('AUTHENTICATION').API_KEY.KEY,
|
||||
apiKey: instance.token,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Handle audio messages
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[Flowise] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.question = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[Flowise] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isImageMessage(content)) {
|
||||
const contentSplit = content.split('|');
|
||||
|
||||
@@ -91,335 +111,26 @@ export class FlowiseService {
|
||||
|
||||
const endpoint = bot.apiUrl;
|
||||
|
||||
if (!endpoint) return null;
|
||||
if (!endpoint) {
|
||||
this.logger.error('No Flowise endpoint defined');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS)
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
|
||||
const message = response?.data?.text;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private async sendMessageWhatsApp(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
session: IntegrationSession,
|
||||
settings: FlowiseSetting,
|
||||
message: string,
|
||||
) {
|
||||
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
let textBuffer = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getMediaType = (url: string): string | null => {
|
||||
const extension = url.split('.').pop()?.toLowerCase();
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
|
||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
|
||||
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
||||
|
||||
if (imageExtensions.includes(extension || '')) return 'image';
|
||||
if (audioExtensions.includes(extension || '')) return 'audio';
|
||||
if (videoExtensions.includes(extension || '')) return 'video';
|
||||
if (documentExtensions.includes(extension || '')) return 'document';
|
||||
return null;
|
||||
};
|
||||
|
||||
while ((match = linkRegex.exec(message)) !== null) {
|
||||
const [fullMatch, exclMark, altText, url] = match;
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
const beforeText = message.slice(lastIndex, match.index);
|
||||
if (beforeText) {
|
||||
textBuffer += beforeText;
|
||||
}
|
||||
|
||||
if (mediaType) {
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
textBuffer = '';
|
||||
if (message) {
|
||||
// Use the base class method to send the message to WhatsApp
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
await instance.audioWhatsapp({
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
audio: url,
|
||||
caption: altText,
|
||||
});
|
||||
} else {
|
||||
await instance.mediaMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
mediatype: mediaType,
|
||||
media: url,
|
||||
caption: altText,
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < message.length) {
|
||||
const remainingText = message.slice(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
textBuffer += remainingText;
|
||||
}
|
||||
}
|
||||
|
||||
const splitMessages = settings.splitMessages ?? false;
|
||||
const timePerChar = settings.timePerChar ?? 0;
|
||||
const minDelay = 1000;
|
||||
const maxDelay = 20000;
|
||||
|
||||
if (textBuffer.trim()) {
|
||||
if (splitMessages) {
|
||||
const multipleMessages = textBuffer.trim().split('\n\n');
|
||||
|
||||
for (let index = 0; index < multipleMessages.length; index++) {
|
||||
const message = multipleMessages[index];
|
||||
|
||||
const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.presenceSubscribe(remoteJid);
|
||||
await instance.client.sendPresenceUpdate('composing', remoteJid);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: message,
|
||||
},
|
||||
false,
|
||||
);
|
||||
resolve();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
if (instance.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
await instance.client.sendPresenceUpdate('paused', remoteJid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await instance.textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings?.delayMessage || 1000,
|
||||
text: textBuffer.trim(),
|
||||
},
|
||||
false,
|
||||
);
|
||||
textBuffer = '';
|
||||
}
|
||||
}
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private async initNewSession(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: Flowise,
|
||||
settings: FlowiseSetting,
|
||||
session: IntegrationSession,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
const data = await this.createNewSession(instance, {
|
||||
remoteJid,
|
||||
pushName,
|
||||
botId: bot.id,
|
||||
});
|
||||
|
||||
if (data.session) {
|
||||
session = data.session;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content);
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: Flowise,
|
||||
session: IntegrationSession,
|
||||
settings: FlowiseSetting,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
) {
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && settings.expire && settings.expire > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
const sessionUpdatedAt = new Date(session.updatedAt).getTime();
|
||||
|
||||
const diff = now - sessionUpdatedAt;
|
||||
|
||||
const diffInMinutes = Math.floor(diff / 1000 / 60);
|
||||
|
||||
if (diffInMinutes > settings.expire) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!content) {
|
||||
if (settings.unknownMessage) {
|
||||
this.waMonitor.waInstances[instance.instanceName].textMessage(
|
||||
{
|
||||
number: remoteJid.split('@')[0],
|
||||
delay: settings.delayMessage || 1000,
|
||||
text: settings.unknownMessage,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.keywordFinish && content.toLowerCase() === settings.keywordFinish.toLowerCase()) {
|
||||
if (settings.keepOpen) {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'closed',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: bot.id,
|
||||
remoteJid: remoteJid,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.sendMessageToBot(instance, bot, remoteJid, pushName, content);
|
||||
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, session, settings, message);
|
||||
|
||||
return;
|
||||
}
|
||||
// The service is now complete with just the abstract method implementations
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ export const flowiseSchema: JSONSchema7 = {
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'apiUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
|
||||
@@ -69,7 +71,9 @@ export const flowiseSettingSchema: JSONSchema7 = {
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
botIdFallback: { type: 'string' },
|
||||
flowiseIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
|
||||
127
src/api/integrations/chatbot/n8n/controllers/n8n.controller.ts
Normal file
127
src/api/integrations/chatbot/n8n/controllers/n8n.controller.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { N8nDto } from '@api/integrations/chatbot/n8n/dto/n8n.dto';
|
||||
import { N8nService } from '@api/integrations/chatbot/n8n/services/n8n.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { configService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { IntegrationSession, N8n as N8nModel } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class N8nController extends BaseChatbotController<N8nModel, N8nDto> {
|
||||
constructor(
|
||||
private readonly n8nService: N8nService,
|
||||
prismaRepository: PrismaRepository,
|
||||
waMonitor: WAMonitoringService,
|
||||
) {
|
||||
super(prismaRepository, waMonitor);
|
||||
|
||||
this.botRepository = this.prismaRepository.n8n;
|
||||
this.settingsRepository = this.prismaRepository.n8nSetting;
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('N8nController');
|
||||
protected readonly integrationName = 'N8n';
|
||||
|
||||
integrationEnabled = configService.get('N8N').ENABLED;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.fallbackId;
|
||||
}
|
||||
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'n8nIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'n8n';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: N8nDto): Record<string, any> {
|
||||
return {
|
||||
webhookUrl: data.webhookUrl,
|
||||
basicAuthUser: data.basicAuthUser,
|
||||
basicAuthPass: data.basicAuthPass,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: N8nDto): Record<string, any> {
|
||||
return {
|
||||
webhookUrl: data.webhookUrl,
|
||||
basicAuthUser: data.basicAuthUser,
|
||||
basicAuthPass: data.basicAuthPass,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: N8nDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
webhookUrl: data.webhookUrl,
|
||||
basicAuthUser: data.basicAuthUser,
|
||||
basicAuthPass: data.basicAuthPass,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('N8n already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: N8nDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('N8n is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
// Check for N8n-specific duplicate
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
webhookUrl: data.webhookUrl,
|
||||
basicAuthUser: data.basicAuthUser,
|
||||
basicAuthPass: data.basicAuthPass,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('N8n already exists');
|
||||
}
|
||||
|
||||
// Let the base class handle the rest of the bot creation process
|
||||
return super.createBot(instance, data);
|
||||
}
|
||||
|
||||
// Process N8n-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: N8nModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
// Use the base class pattern instead of calling n8nService.process directly
|
||||
await this.n8nService.process(instance, remoteJid, bot, session, settings, content, pushName, msg);
|
||||
}
|
||||
}
|
||||
17
src/api/integrations/chatbot/n8n/dto/n8n.dto.ts
Normal file
17
src/api/integrations/chatbot/n8n/dto/n8n.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class N8nDto extends BaseChatbotDto {
|
||||
// N8n specific fields
|
||||
webhookUrl?: string;
|
||||
basicAuthUser?: string;
|
||||
basicAuthPass?: string;
|
||||
}
|
||||
|
||||
export class N8nSettingDto extends BaseChatbotSettingDto {
|
||||
// N8n has no specific fields
|
||||
}
|
||||
|
||||
export class N8nMessageDto {
|
||||
chatInput: string;
|
||||
sessionId: string;
|
||||
}
|
||||
114
src/api/integrations/chatbot/n8n/routes/n8n.router.ts
Normal file
114
src/api/integrations/chatbot/n8n/routes/n8n.router.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { n8nController } from '@api/server.module';
|
||||
import {
|
||||
instanceSchema,
|
||||
n8nIgnoreJidSchema,
|
||||
n8nSchema,
|
||||
n8nSettingSchema,
|
||||
n8nStatusSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
import { N8nDto, N8nSettingDto } from '../dto/n8n.dto';
|
||||
|
||||
export class N8nRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<N8nDto>({
|
||||
request: req,
|
||||
schema: n8nSchema,
|
||||
ClassRef: N8nDto,
|
||||
execute: (instance, data) => n8nController.createBot(instance, data),
|
||||
});
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('find'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.findBot(instance),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:n8nId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.fetchBot(instance, req.params.n8nId),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:n8nId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<N8nDto>({
|
||||
request: req,
|
||||
schema: n8nSchema,
|
||||
ClassRef: N8nDto,
|
||||
execute: (instance, data) => n8nController.updateBot(instance, req.params.n8nId, data),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:n8nId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.deleteBot(instance, req.params.n8nId),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<N8nSettingDto>({
|
||||
request: req,
|
||||
schema: n8nSettingSchema,
|
||||
ClassRef: N8nSettingDto,
|
||||
execute: (instance, data) => n8nController.settings(instance, data),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.fetchSettings(instance),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: n8nStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => n8nController.changeStatus(instance, data),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:n8nId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => n8nController.fetchSessions(instance, req.params.n8nId),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<IgnoreJidDto>({
|
||||
request: req,
|
||||
schema: n8nIgnoreJidSchema,
|
||||
ClassRef: IgnoreJidDto,
|
||||
execute: (instance, data) => n8nController.ignoreJid(instance, data),
|
||||
});
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
97
src/api/integrations/chatbot/n8n/services/n8n.service.ts
Normal file
97
src/api/integrations/chatbot/n8n/services/n8n.service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
import { ConfigService, HttpServer } from '@config/env.config';
|
||||
import { IntegrationSession, N8n, N8nSetting } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
import { BaseChatbotService } from '../../base-chatbot.service';
|
||||
import { OpenaiService } from '../../openai/services/openai.service';
|
||||
|
||||
export class N8nService extends BaseChatbotService<N8n, N8nSetting> {
|
||||
private openaiService: OpenaiService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'N8nService', configService);
|
||||
this.openaiService = openaiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bot type for N8n
|
||||
*/
|
||||
protected getBotType(): string {
|
||||
return 'n8n';
|
||||
}
|
||||
|
||||
protected async sendMessageToBot(
|
||||
instance: any,
|
||||
session: IntegrationSession,
|
||||
settings: N8nSetting,
|
||||
n8n: N8n,
|
||||
remoteJid: string,
|
||||
pushName: string,
|
||||
content: string,
|
||||
msg?: any,
|
||||
) {
|
||||
try {
|
||||
if (!session) {
|
||||
this.logger.error('Session is null in sendMessageToBot');
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint: string = n8n.webhookUrl;
|
||||
const payload: any = {
|
||||
chatInput: content,
|
||||
sessionId: session.sessionId,
|
||||
remoteJid: remoteJid,
|
||||
pushName: pushName,
|
||||
keyId: msg?.key?.id,
|
||||
fromMe: msg?.key?.fromMe,
|
||||
instanceName: instance.instanceName,
|
||||
serverUrl: this.configService.get<HttpServer>('SERVER').URL,
|
||||
apiKey: instance.token,
|
||||
};
|
||||
|
||||
// Handle audio messages
|
||||
if (this.isAudioMessage(content) && msg) {
|
||||
try {
|
||||
this.logger.debug(`[N8n] Downloading audio for Whisper transcription`);
|
||||
const transcription = await this.openaiService.speechToText(msg, instance);
|
||||
if (transcription) {
|
||||
payload.chatInput = `[audio] ${transcription}`;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[N8n] Failed to transcribe audio: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (n8n.basicAuthUser && n8n.basicAuthPass) {
|
||||
const auth = Buffer.from(`${n8n.basicAuthUser}:${n8n.basicAuthPass}`).toString('base64');
|
||||
headers['Authorization'] = `Basic ${auth}`;
|
||||
}
|
||||
const response = await axios.post(endpoint, payload, { headers });
|
||||
const message = response?.data?.output || response?.data?.answer;
|
||||
|
||||
// Use base class method instead of custom implementation
|
||||
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
|
||||
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'opened',
|
||||
awaitUser: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error.response?.data || error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/api/integrations/chatbot/n8n/validate/n8n.schema.ts
Normal file
116
src/api/integrations/chatbot/n8n/validate/n8n.schema.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
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 n8nSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
webhookUrl: { type: 'string' },
|
||||
basicAuthUser: { type: 'string' },
|
||||
basicAuthPassword: { type: 'string' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'webhookUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'webhookUrl', 'triggerType'),
|
||||
};
|
||||
|
||||
export const n8nStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const n8nSettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
botIdFallback: { type: 'string' },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
'splitMessages',
|
||||
'timePerChar',
|
||||
),
|
||||
};
|
||||
|
||||
export const n8nIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,13 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class OpenaiCredsDto {
|
||||
name: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export class OpenaiDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
export class OpenaiDto extends BaseChatbotDto {
|
||||
openaiCredsId: string;
|
||||
botType?: string;
|
||||
botType: string;
|
||||
assistantId?: string;
|
||||
functionUrl?: string;
|
||||
model?: string;
|
||||
@@ -17,35 +15,10 @@ export class OpenaiDto {
|
||||
assistantMessages?: string[];
|
||||
userMessages?: string[];
|
||||
maxTokens?: number;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
export class OpenaiSettingDto {
|
||||
export class OpenaiSettingDto extends BaseChatbotSettingDto {
|
||||
openaiCredsId?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
openaiIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
speechToText?: boolean;
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export class OpenaiRouter extends RouterBroker {
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.getModels(instance),
|
||||
execute: (instance) => openaiController.getModels(instance, req.query.openaiCredsId as string),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
import { IgnoreJidDto } from '@api/dto/chatbot.dto';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { TypebotDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
|
||||
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service';
|
||||
@@ -8,13 +7,12 @@ import { Events } from '@api/types/wa.types';
|
||||
import { configService, Typebot } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { Typebot as TypebotModel } from '@prisma/client';
|
||||
import { getConversationMessage } from '@utils/getConversationMessage';
|
||||
import { IntegrationSession, Typebot as TypebotModel } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
|
||||
import { ChatbotController, ChatbotControllerInterface } from '../../chatbot.controller';
|
||||
import { BaseChatbotController } from '../../base-chatbot.controller';
|
||||
|
||||
export class TypebotController extends ChatbotController implements ChatbotControllerInterface {
|
||||
export class TypebotController extends BaseChatbotController<TypebotModel, TypebotDto> {
|
||||
constructor(
|
||||
private readonly typebotService: TypebotService,
|
||||
prismaRepository: PrismaRepository,
|
||||
@@ -28,6 +26,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('TypebotController');
|
||||
protected readonly integrationName = 'Typebot';
|
||||
|
||||
integrationEnabled = configService.get<Typebot>('TYPEBOT').ENABLED;
|
||||
botRepository: any;
|
||||
@@ -35,231 +34,39 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Bots
|
||||
public async createBot(instance: InstanceDto, data: TypebotDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
if (
|
||||
!data.expire ||
|
||||
!data.keywordFinish ||
|
||||
!data.delayMessage ||
|
||||
!data.unknownMessage ||
|
||||
!data.listeningFromMe ||
|
||||
!data.stopBotFromMe ||
|
||||
!data.keepOpen ||
|
||||
!data.debounceTime ||
|
||||
!data.ignoreJids
|
||||
) {
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
|
||||
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR';
|
||||
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
|
||||
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi';
|
||||
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
|
||||
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
|
||||
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
|
||||
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
|
||||
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
ignoreJids: data.ignoreJids,
|
||||
});
|
||||
}
|
||||
protected getFallbackBotId(settings: any): string | undefined {
|
||||
return settings?.typebotIdFallback;
|
||||
}
|
||||
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll && data.triggerType === 'all') {
|
||||
throw new Error('You already have a typebot with an "All" trigger, you cannot have more bots while it is active');
|
||||
protected getFallbackFieldName(): string {
|
||||
return 'typebotIdFallback';
|
||||
}
|
||||
|
||||
protected getIntegrationType(): string {
|
||||
return 'typebot';
|
||||
}
|
||||
|
||||
protected getAdditionalBotData(data: TypebotDto): Record<string, any> {
|
||||
return {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific updates
|
||||
protected getAdditionalUpdateFields(data: TypebotDto): Record<string, any> {
|
||||
return {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation for bot-specific duplicate validation on update
|
||||
protected async validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: TypebotDto): Promise<void> {
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Typebot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
instanceId: instanceId,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error creating typebot');
|
||||
}
|
||||
}
|
||||
|
||||
public async findBot(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bots = await this.botRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bots.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
|
||||
public async fetchBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const bot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (bot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
public async updateBot(instance: InstanceDto, botId: string, data: TypebotDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const typebot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!typebot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (typebot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'all') {
|
||||
const checkTriggerAll = await this.botRepository.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
triggerType: 'all',
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
@@ -267,284 +74,44 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
},
|
||||
});
|
||||
|
||||
if (checkTriggerAll) {
|
||||
throw new Error(
|
||||
'You already have a typebot with an "All" trigger, you cannot have more bots while it is active',
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Typebot already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// Process Typebot-specific bot logic
|
||||
protected async processBot(
|
||||
instance: any,
|
||||
remoteJid: string,
|
||||
bot: TypebotModel,
|
||||
session: IntegrationSession,
|
||||
settings: any,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
) {
|
||||
// Map to the original processTypebot method signature
|
||||
await this.typebotService.processTypebot(
|
||||
instance,
|
||||
remoteJid,
|
||||
msg,
|
||||
session,
|
||||
bot,
|
||||
bot.url,
|
||||
settings.expire,
|
||||
bot.typebot,
|
||||
settings.keywordFinish,
|
||||
settings.delayMessage,
|
||||
settings.unknownMessage,
|
||||
settings.listeningFromMe,
|
||||
settings.stopBotFromMe,
|
||||
settings.keepOpen,
|
||||
content,
|
||||
{}, // prefilledVariables (optional)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Typebot already exists');
|
||||
}
|
||||
|
||||
if (data.triggerType === 'keyword') {
|
||||
if (!data.triggerOperator || !data.triggerValue) {
|
||||
throw new Error('Trigger operator and value are required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
id: {
|
||||
not: botId,
|
||||
},
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.triggerType === 'advanced') {
|
||||
if (!data.triggerValue) {
|
||||
throw new Error('Trigger value is required');
|
||||
}
|
||||
|
||||
const checkDuplicate = await this.botRepository.findFirst({
|
||||
where: {
|
||||
triggerValue: data.triggerValue,
|
||||
id: { not: botId },
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
throw new Error('Trigger already exists');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const bot = await this.botRepository.update({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
data: {
|
||||
enabled: data?.enabled,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
typebot: data.typebot,
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
triggerType: data.triggerType,
|
||||
triggerOperator: data.triggerOperator,
|
||||
triggerValue: data.triggerValue,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error updating typebot');
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteBot(instance: InstanceDto, botId: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const typebot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!typebot) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
if (typebot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
try {
|
||||
await this.prismaRepository.integrationSession.deleteMany({
|
||||
where: {
|
||||
botId: botId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.botRepository.delete({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
return { typebot: { id: botId } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error deleting typebot');
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (settings) {
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
typebotIdFallback: data.typebotIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: updateSettings.expire,
|
||||
keywordFinish: updateSettings.keywordFinish,
|
||||
delayMessage: updateSettings.delayMessage,
|
||||
unknownMessage: updateSettings.unknownMessage,
|
||||
listeningFromMe: updateSettings.listeningFromMe,
|
||||
stopBotFromMe: updateSettings.stopBotFromMe,
|
||||
keepOpen: updateSettings.keepOpen,
|
||||
debounceTime: updateSettings.debounceTime,
|
||||
typebotIdFallback: updateSettings.typebotIdFallback,
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
}
|
||||
|
||||
const newSetttings = await this.settingsRepository.create({
|
||||
data: {
|
||||
expire: data.expire,
|
||||
keywordFinish: data.keywordFinish,
|
||||
delayMessage: data.delayMessage,
|
||||
unknownMessage: data.unknownMessage,
|
||||
listeningFromMe: data.listeningFromMe,
|
||||
stopBotFromMe: data.stopBotFromMe,
|
||||
keepOpen: data.keepOpen,
|
||||
debounceTime: data.debounceTime,
|
||||
typebotIdFallback: data.typebotIdFallback,
|
||||
ignoreJids: data.ignoreJids,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
expire: newSetttings.expire,
|
||||
keywordFinish: newSetttings.keywordFinish,
|
||||
delayMessage: newSetttings.delayMessage,
|
||||
unknownMessage: newSetttings.unknownMessage,
|
||||
listeningFromMe: newSetttings.listeningFromMe,
|
||||
stopBotFromMe: newSetttings.stopBotFromMe,
|
||||
keepOpen: newSetttings.keepOpen,
|
||||
debounceTime: newSetttings.debounceTime,
|
||||
typebotIdFallback: newSetttings.typebotIdFallback,
|
||||
ignoreJids: newSetttings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
include: {
|
||||
Fallback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
expire: 0,
|
||||
keywordFinish: '',
|
||||
delayMessage: 0,
|
||||
unknownMessage: '',
|
||||
listeningFromMe: false,
|
||||
stopBotFromMe: false,
|
||||
keepOpen: false,
|
||||
ignoreJids: [],
|
||||
typebotIdFallback: null,
|
||||
fallback: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
expire: settings.expire,
|
||||
keywordFinish: settings.keywordFinish,
|
||||
delayMessage: settings.delayMessage,
|
||||
unknownMessage: settings.unknownMessage,
|
||||
listeningFromMe: settings.listeningFromMe,
|
||||
stopBotFromMe: settings.stopBotFromMe,
|
||||
keepOpen: settings.keepOpen,
|
||||
ignoreJids: settings.ignoreJids,
|
||||
typebotIdFallback: settings.typebotIdFallback,
|
||||
fallback: settings.Fallback,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching default settings');
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions
|
||||
// TypeBot specific method for starting a bot from API
|
||||
public async startBot(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
@@ -570,6 +137,8 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
let listeningFromMe = data?.typebot?.listeningFromMe;
|
||||
let stopBotFromMe = data?.typebot?.stopBotFromMe;
|
||||
let keepOpen = data?.typebot?.keepOpen;
|
||||
let debounceTime = data?.typebot?.debounceTime;
|
||||
let ignoreJids = data?.typebot?.ignoreJids;
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
@@ -586,15 +155,20 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
!unknownMessage ||
|
||||
!listeningFromMe ||
|
||||
!stopBotFromMe ||
|
||||
!keepOpen
|
||||
!keepOpen ||
|
||||
!debounceTime ||
|
||||
!ignoreJids
|
||||
) {
|
||||
if (!expire) expire = defaultSettingCheck?.expire || 0;
|
||||
if (!keywordFinish) keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR';
|
||||
if (!delayMessage) delayMessage = defaultSettingCheck?.delayMessage || 1000;
|
||||
if (!unknownMessage) unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi';
|
||||
if (!listeningFromMe) listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
|
||||
if (!stopBotFromMe) stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
|
||||
if (!keepOpen) keepOpen = defaultSettingCheck?.keepOpen || false;
|
||||
if (expire === undefined || expire === null) expire = defaultSettingCheck.expire;
|
||||
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = defaultSettingCheck.keywordFinish;
|
||||
if (delayMessage === undefined || delayMessage === null) delayMessage = defaultSettingCheck.delayMessage;
|
||||
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = defaultSettingCheck.unknownMessage;
|
||||
if (listeningFromMe === undefined || listeningFromMe === null)
|
||||
listeningFromMe = defaultSettingCheck.listeningFromMe;
|
||||
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = defaultSettingCheck.stopBotFromMe;
|
||||
if (keepOpen === undefined || keepOpen === null) keepOpen = defaultSettingCheck.keepOpen;
|
||||
if (debounceTime === undefined || debounceTime === null) debounceTime = defaultSettingCheck.debounceTime;
|
||||
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = defaultSettingCheck.ignoreJids;
|
||||
|
||||
if (!defaultSettingCheck) {
|
||||
await this.settings(instance, {
|
||||
@@ -605,6 +179,8 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
listeningFromMe: listeningFromMe,
|
||||
stopBotFromMe: stopBotFromMe,
|
||||
keepOpen: keepOpen,
|
||||
debounceTime: debounceTime,
|
||||
ignoreJids: ignoreJids,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -652,11 +228,12 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
},
|
||||
});
|
||||
|
||||
// Use the original processTypebot method with all parameters
|
||||
await this.typebotService.processTypebot(
|
||||
instanceData,
|
||||
this.waMonitor.waInstances[instanceData.name],
|
||||
remoteJid,
|
||||
null,
|
||||
null,
|
||||
null, // msg
|
||||
null, // session
|
||||
findBot,
|
||||
url,
|
||||
expire,
|
||||
@@ -738,331 +315,4 @@ export class TypebotController extends ChatbotController implements ChatbotContr
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const remoteJid = data.remoteJid;
|
||||
const status = data.status;
|
||||
|
||||
const defaultSettingCheck = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (status === 'delete') {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
instanceId: instanceId,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } };
|
||||
}
|
||||
|
||||
if (status === 'closed') {
|
||||
if (defaultSettingCheck?.keepOpen) {
|
||||
await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.sessionRepository.deleteMany({
|
||||
where: {
|
||||
remoteJid: remoteJid,
|
||||
instanceId: instanceId,
|
||||
botId: { not: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { typebot: { ...instance, typebot: { remoteJid: remoteJid, status: status } } };
|
||||
}
|
||||
|
||||
const session = await this.sessionRepository.updateMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid: remoteJid,
|
||||
botId: { not: null },
|
||||
},
|
||||
data: {
|
||||
status: status,
|
||||
},
|
||||
});
|
||||
|
||||
const typebotData = {
|
||||
remoteJid: remoteJid,
|
||||
status: status,
|
||||
session,
|
||||
};
|
||||
|
||||
this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData);
|
||||
|
||||
return { typebot: { ...instance, typebot: typebotData } };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error changing status');
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const typebot = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: botId,
|
||||
},
|
||||
});
|
||||
|
||||
if (typebot && typebot.instanceId !== instanceId) {
|
||||
throw new Error('Typebot not found');
|
||||
}
|
||||
|
||||
return await this.sessionRepository.findMany({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
remoteJid,
|
||||
botId: botId ?? { not: null },
|
||||
type: 'typebot',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error fetching sessions');
|
||||
}
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
|
||||
if (!this.integrationEnabled) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
try {
|
||||
const instanceId = await this.prismaRepository.instance
|
||||
.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
})
|
||||
.then((instance) => instance.id);
|
||||
|
||||
const settings = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
throw new Error('Settings not found');
|
||||
}
|
||||
|
||||
let ignoreJids: any = settings?.ignoreJids || [];
|
||||
|
||||
if (data.action === 'add') {
|
||||
if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
|
||||
|
||||
ignoreJids.push(data.remoteJid);
|
||||
} else {
|
||||
ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
|
||||
}
|
||||
|
||||
const updateSettings = await this.settingsRepository.update({
|
||||
where: {
|
||||
id: settings.id,
|
||||
},
|
||||
data: {
|
||||
ignoreJids: ignoreJids,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ignoreJids: updateSettings.ignoreJids,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new Error('Error setting default settings');
|
||||
}
|
||||
}
|
||||
|
||||
public async emit({
|
||||
instance,
|
||||
remoteJid,
|
||||
msg,
|
||||
}: {
|
||||
instance: InstanceDto;
|
||||
remoteJid: string;
|
||||
msg: any;
|
||||
pushName?: string;
|
||||
}) {
|
||||
if (!this.integrationEnabled) return;
|
||||
|
||||
try {
|
||||
const instanceData = await this.prismaRepository.instance.findFirst({
|
||||
where: {
|
||||
name: instance.instanceName,
|
||||
},
|
||||
});
|
||||
|
||||
if (!instanceData) throw new Error('Instance not found');
|
||||
|
||||
const session = await this.getSession(remoteJid, instance);
|
||||
|
||||
const content = getConversationMessage(msg);
|
||||
|
||||
let findBot = (await this.findBotTrigger(
|
||||
this.botRepository,
|
||||
this.settingsRepository,
|
||||
content,
|
||||
instance,
|
||||
session,
|
||||
)) as TypebotModel;
|
||||
|
||||
if (!findBot) {
|
||||
const fallback = await this.settingsRepository.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallback?.typebotIdFallback) {
|
||||
const findFallback = await this.botRepository.findFirst({
|
||||
where: {
|
||||
id: fallback.typebotIdFallback,
|
||||
},
|
||||
});
|
||||
|
||||
findBot = findFallback;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await this.prismaRepository.typebotSetting.findFirst({
|
||||
where: {
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
const url = findBot?.url;
|
||||
const typebot = findBot?.typebot;
|
||||
let expire = findBot?.expire;
|
||||
let keywordFinish = findBot?.keywordFinish;
|
||||
let delayMessage = findBot?.delayMessage;
|
||||
let unknownMessage = findBot?.unknownMessage;
|
||||
let listeningFromMe = findBot?.listeningFromMe;
|
||||
let stopBotFromMe = findBot?.stopBotFromMe;
|
||||
let keepOpen = findBot?.keepOpen;
|
||||
let debounceTime = findBot?.debounceTime;
|
||||
let ignoreJids = findBot?.ignoreJids;
|
||||
|
||||
if (!expire) expire = settings.expire;
|
||||
if (!keywordFinish) keywordFinish = settings.keywordFinish;
|
||||
if (!delayMessage) delayMessage = settings.delayMessage;
|
||||
if (!unknownMessage) unknownMessage = settings.unknownMessage;
|
||||
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
|
||||
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
|
||||
if (!keepOpen) keepOpen = settings.keepOpen;
|
||||
if (!debounceTime) debounceTime = settings.debounceTime;
|
||||
if (!ignoreJids) ignoreJids = settings.ignoreJids;
|
||||
|
||||
if (this.checkIgnoreJids(ignoreJids, remoteJid)) return;
|
||||
|
||||
const key = msg.key as {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
participant: string;
|
||||
};
|
||||
|
||||
if (stopBotFromMe && key.fromMe && session) {
|
||||
await this.sessionRepository.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
status: 'paused',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listeningFromMe && key.fromMe) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTime && debounceTime > 0) {
|
||||
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
|
||||
await this.typebotService.processTypebot(
|
||||
instanceData,
|
||||
remoteJid,
|
||||
msg,
|
||||
session,
|
||||
findBot,
|
||||
url,
|
||||
expire,
|
||||
typebot,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
debouncedContent,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
await this.typebotService.processTypebot(
|
||||
instanceData,
|
||||
remoteJid,
|
||||
msg,
|
||||
session,
|
||||
findBot,
|
||||
url,
|
||||
expire,
|
||||
typebot,
|
||||
keywordFinish,
|
||||
delayMessage,
|
||||
unknownMessage,
|
||||
listeningFromMe,
|
||||
stopBotFromMe,
|
||||
keepOpen,
|
||||
content,
|
||||
);
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) return;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user