Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03e9588581 | ||
|
|
393f5ef7da | ||
|
|
f778678514 | ||
|
|
e4f88771a8 | ||
|
|
e885ea8e95 | ||
|
|
f8a9ed7d06 | ||
|
|
d89e9105d0 | ||
|
|
3ba92dec04 | ||
|
|
441ebbd525 | ||
|
|
690c43c245 | ||
|
|
26d8591a6c | ||
|
|
ac63c097ec | ||
|
|
d29e4f8351 | ||
|
|
615ccda4c3 | ||
|
|
d52eb71c76 | ||
|
|
8edd81a765 | ||
|
|
5830cee384 | ||
|
|
20f15b4370 | ||
|
|
8432cd4f8d | ||
|
|
55870b88bf | ||
|
|
6acf4373f4 | ||
|
|
2381f0843f | ||
|
|
b4e6f5e722 | ||
|
|
415bee3e8e | ||
|
|
aad924be79 | ||
|
|
81083e7f82 | ||
|
|
f8e52959a4 | ||
|
|
8eb542f49e | ||
|
|
45ae263ab9 | ||
|
|
f066f982bf | ||
|
|
0888640741 | ||
|
|
bde3722f5c | ||
|
|
0cd3df117a | ||
|
|
4c9525cc9f | ||
|
|
c14fe1e67d | ||
|
|
d1e3b209e7 | ||
|
|
f37bf62a30 | ||
|
|
538bfe0e24 | ||
|
|
93cd5307d9 | ||
|
|
b697f37b1e | ||
|
|
96a9cd8cbb | ||
|
|
a21b9a11a9 | ||
|
|
213b51e598 | ||
|
|
faf3c77508 | ||
|
|
e6fdfb88ca | ||
|
|
99746df626 | ||
|
|
43aeeb55e8 | ||
|
|
4f89f49d44 | ||
|
|
be583e7e40 | ||
|
|
e8c8f89733 | ||
|
|
13b6d3ddf7 | ||
|
|
5e97fb74b1 | ||
|
|
cc47321cad | ||
|
|
607dff8f7a | ||
|
|
0a74f8dbcd | ||
|
|
796befcf87 | ||
|
|
2c5610dd01 | ||
|
|
43d110fa31 | ||
|
|
bf52dd4874 | ||
|
|
f8268180bf | ||
|
|
04cf9bc8c0 | ||
|
|
ff38cf7660 | ||
|
|
4c0b44c4b5 | ||
|
|
e75c29d57f | ||
|
|
f8329c29b1 | ||
|
|
52222a6b98 | ||
|
|
99a0a19d8c | ||
|
|
48ed33bc94 | ||
|
|
b234e134bf | ||
|
|
f6004fd294 | ||
|
|
91d541387b | ||
|
|
9dbd45da6e | ||
|
|
adc1c3960c | ||
|
|
ff742d2e84 | ||
|
|
10bd4d6271 | ||
|
|
874cafe114 | ||
|
|
0ce2c4b9fa | ||
|
|
a8a7988bfa | ||
|
|
2940d93b81 | ||
|
|
88707cf72b | ||
|
|
b0f883c6a8 | ||
|
|
4cddb766c2 | ||
|
|
6675492901 | ||
|
|
a9d030af2a | ||
|
|
2782a188a9 | ||
|
|
256688497e | ||
|
|
4def193c57 | ||
|
|
7f3a2e6aa5 | ||
|
|
60e4f13b67 | ||
|
|
2d6cfeb0fa | ||
|
|
03e85434df | ||
|
|
0ce4c01c48 | ||
|
|
0e95890ca4 | ||
|
|
072f4b2ab0 | ||
|
|
47a91ef669 | ||
|
|
5143bd1ef8 | ||
|
|
0efc0751c9 | ||
|
|
51dda739cb | ||
|
|
3b28826679 | ||
|
|
81646bbaa2 | ||
|
|
d8d84107fb | ||
|
|
58d917d374 | ||
|
|
f8f0063975 | ||
|
|
73faf330e3 | ||
|
|
5a1ebb186d | ||
|
|
54d6d715b0 | ||
|
|
5d9d6e29e2 | ||
|
|
cf3385f624 | ||
|
|
0346c5e7b9 | ||
|
|
443652b0a7 | ||
|
|
1e73c3dc6a | ||
|
|
afa9d4208f | ||
|
|
64eda25b5d | ||
|
|
cfc6c931a3 | ||
|
|
35bc56c948 | ||
|
|
371bde4c3e | ||
|
|
9497e2900e | ||
|
|
51bbc9998a | ||
|
|
057f7bce56 | ||
|
|
52412fb4cc | ||
|
|
a57f3443de | ||
|
|
9b2537e7c7 | ||
|
|
a378ad4c3d | ||
|
|
8f867e0b5d | ||
|
|
d4f6ec18fd | ||
|
|
ac05dd83e9 | ||
|
|
17c22f0cd4 | ||
|
|
971979c13f | ||
|
|
845e203637 | ||
|
|
7a2e1cd731 | ||
|
|
84ba011a4c | ||
|
|
50399f1192 | ||
|
|
a2517b248d | ||
|
|
f02d95331a | ||
|
|
cb1faff88c | ||
|
|
3ff1c06eea | ||
|
|
c91d2c59eb | ||
|
|
4cf5a74d23 | ||
|
|
13de42dbe0 | ||
|
|
f99b86ed18 | ||
|
|
07f6c78372 | ||
|
|
83678553e3 | ||
|
|
9d1bbd780e | ||
|
|
899f9f2e84 | ||
|
|
1d1a083464 | ||
|
|
fabc6753e6 | ||
|
|
a16f3b67fe | ||
|
|
c0c3381a36 | ||
|
|
212a27d9df | ||
|
|
a18c1d82c1 | ||
|
|
67349e4509 | ||
|
|
293267536b | ||
|
|
10d103a09a | ||
|
|
1a6ff4dbde | ||
|
|
321310934d | ||
|
|
22312d775a | ||
|
|
b71d83ce48 | ||
|
|
1a91e7d722 | ||
|
|
7a54b6b599 | ||
|
|
821c9f7b00 | ||
|
|
e7f300b4bc | ||
|
|
a5bff80b3a | ||
|
|
89dd3aa9e1 | ||
|
|
8fc5658e9d | ||
|
|
eb3879bf65 | ||
|
|
daf56433a3 | ||
|
|
e4cb5d5652 | ||
|
|
a2e0119e61 | ||
|
|
f8bcaebf4a | ||
|
|
935a2b515f | ||
|
|
2313915ade | ||
|
|
dfc1c6dde4 | ||
|
|
62d1b3dbf6 | ||
|
|
7272f0e669 | ||
|
|
96da96281d | ||
|
|
50e09c3680 | ||
|
|
1da6a16640 | ||
|
|
10f80132f8 | ||
|
|
750b2b86b5 | ||
|
|
2fc2caed85 | ||
|
|
1431e8c0f1 | ||
|
|
01c9c2ca56 | ||
|
|
f5d216df6f | ||
|
|
7614822d2b | ||
|
|
d2eb463d32 | ||
|
|
07b1e50534 | ||
|
|
41b6b3459e | ||
|
|
0f5fcf60f6 | ||
|
|
18d8330ab3 | ||
|
|
7de5f461aa | ||
|
|
caa055abed | ||
|
|
49af88dea3 | ||
|
|
05d856d8ae | ||
|
|
c5c99136fa | ||
|
|
a8b520012f | ||
|
|
449df53c19 | ||
|
|
69ac19b5c0 | ||
|
|
163b11aa36 | ||
|
|
f4171a9086 | ||
|
|
dfbae9c71e | ||
|
|
91caa7ba25 | ||
|
|
2ac57bb6c3 | ||
|
|
e01647eb48 | ||
|
|
90bc05ecf3 | ||
|
|
21998cbfae | ||
|
|
87c49a8766 | ||
|
|
4e78d559a8 | ||
|
|
18ce902c60 | ||
|
|
5784db8055 | ||
|
|
6862063654 | ||
|
|
4aea282356 | ||
|
|
82ff15ae56 | ||
|
|
6cbf8cded2 | ||
|
|
35e9418988 | ||
|
|
a58be84e56 | ||
|
|
2994e0a54d | ||
|
|
f16100e1d9 | ||
|
|
e57be58d48 | ||
|
|
7909910dc6 | ||
|
|
7f1778ceb0 | ||
|
|
aba90574d8 | ||
|
|
33c945bf95 | ||
|
|
d40e82eb02 | ||
|
|
6e82d6f40b | ||
|
|
e193eb5696 | ||
|
|
8c63c4b097 | ||
|
|
4127d609cf | ||
|
|
57f6db4252 | ||
|
|
68a91cb55c | ||
|
|
869a343e43 | ||
|
|
fd2b22b339 | ||
|
|
90d3a5b1ad | ||
|
|
b4a2ad6b96 | ||
|
|
27ff7e7a21 | ||
|
|
94ae624662 | ||
|
|
52e018aa93 | ||
|
|
8bd13a233e | ||
|
|
87f79c1dc5 | ||
|
|
31533b5d19 | ||
|
|
27cc9f4764 | ||
|
|
4fdfce3944 | ||
|
|
1e12a83cdf | ||
|
|
1c41b8be65 | ||
|
|
f72ee7c1f8 | ||
|
|
e75912435b | ||
|
|
da294a58a6 | ||
|
|
7689199e11 | ||
|
|
324ab11f09 | ||
|
|
b1f2e17e3d | ||
|
|
90822cd582 | ||
|
|
9eec72b641 | ||
|
|
050c652480 | ||
|
|
c44680c987 | ||
|
|
9394509a07 | ||
|
|
63890dc499 | ||
|
|
e2330901e4 | ||
|
|
58672fa1e1 | ||
|
|
3890071f77 | ||
|
|
d60dfba90a | ||
|
|
4e58713e7e | ||
|
|
6f0af3c7cc | ||
|
|
23bac2e7b1 | ||
|
|
e839203dbd | ||
|
|
304b0037ec | ||
|
|
482dbbeb0e | ||
|
|
8f4c79baf4 | ||
|
|
31038a9318 | ||
|
|
86fde1f670 | ||
|
|
984ef0c6a1 | ||
|
|
744ee1abb1 | ||
|
|
47f4f2670d | ||
|
|
f16eaed935 | ||
|
|
dfe9454a72 | ||
|
|
4aef38ff04 | ||
|
|
ab8de51022 | ||
|
|
c001d7fed6 | ||
|
|
cc056dca9c | ||
|
|
47f60a1626 | ||
|
|
98721051da | ||
|
|
5d7a6f4ff7 | ||
|
|
dcb7bf033f | ||
|
|
7d29be2d69 | ||
|
|
41b669a35c | ||
|
|
7b5ca3bc57 | ||
|
|
bb0c7fd22b | ||
|
|
c79958541e | ||
|
|
94ee8863d6 | ||
|
|
011477262d | ||
|
|
2e8e4cefb0 | ||
|
|
5e2802c098 | ||
|
|
9a6250a055 | ||
|
|
f8cb56525a | ||
|
|
b5b491cb7b | ||
|
|
7eb6be70f9 | ||
|
|
0de8a5fabc | ||
|
|
9fbc18fb70 | ||
|
|
e7ce3368b0 | ||
|
|
729600e9c6 | ||
|
|
54886f385b | ||
|
|
81854ad7ec | ||
|
|
5261d4f281 | ||
|
|
72d9d94342 | ||
|
|
b782ad27a2 | ||
|
|
d7dad5e6c2 | ||
|
|
8765bb4412 | ||
|
|
984be25d51 | ||
|
|
70620207dd | ||
|
|
a96200f981 | ||
|
|
4978abf3d8 | ||
|
|
a67d84f21e | ||
|
|
91f59b38fd | ||
|
|
d0ec351910 | ||
|
|
a7b12874fb | ||
|
|
cd2c7bdd86 | ||
|
|
045077143f | ||
|
|
bcd2199687 | ||
|
|
af108eede4 | ||
|
|
ee1cf838b8 | ||
|
|
0fc74895da | ||
|
|
394937c154 | ||
|
|
71f9adeff4 | ||
|
|
e13ccdab9c | ||
|
|
2074748a7a | ||
|
|
283d63394e | ||
|
|
5cfb53c367 | ||
|
|
9466407b8a | ||
|
|
4b65f0c462 | ||
|
|
23504b35c8 | ||
|
|
dd6ec4ca3a | ||
|
|
8235e5a111 | ||
|
|
55499a6144 | ||
|
|
9dbc4a6085 | ||
|
|
f7c6ce847c | ||
|
|
84eed00415 | ||
|
|
51aa0e3154 | ||
|
|
77893a0eb0 | ||
|
|
4a866d8a08 | ||
|
|
f912353a5c | ||
|
|
a6a1d8dc28 | ||
|
|
150e73aa74 | ||
|
|
a9300b3134 | ||
|
|
bbe3859085 | ||
|
|
b420548dd9 | ||
|
|
741483eec7 | ||
|
|
2acf7e4e17 | ||
|
|
9bca6d904d | ||
|
|
857c586df6 | ||
|
|
df8cb7cecb | ||
|
|
9d0050adb8 | ||
|
|
df927ab04a | ||
|
|
02e5934e8f | ||
|
|
825a4152db | ||
|
|
81b10099af | ||
|
|
3be5f328b0 | ||
|
|
5caa3b8b81 | ||
|
|
8231bf7d8e | ||
|
|
22d94808fb | ||
|
|
57daefa680 | ||
|
|
5cf8704973 | ||
|
|
28da9c34bb | ||
|
|
51ba5e3266 | ||
|
|
f696794ee1 | ||
|
|
c6eca9510b | ||
|
|
9a2fd2fccd | ||
|
|
c0a2d02f0d | ||
|
|
6493d16fd2 | ||
|
|
9f96c678dc | ||
|
|
a81f2ab553 | ||
|
|
8fbf7fa0e5 | ||
|
|
af0bba313a | ||
|
|
76307dec12 | ||
|
|
0b441054ae | ||
|
|
2e8b78aebc | ||
|
|
d01b221a24 | ||
|
|
61bb111768 | ||
|
|
de47d6e445 | ||
|
|
59fe4b30f0 | ||
|
|
166dae8e27 | ||
|
|
5ce64fd30d | ||
|
|
1a4513f437 | ||
|
|
59be4a780f | ||
|
|
74001c2b49 | ||
|
|
a9ce0fb2d9 | ||
|
|
4c3fd1c214 | ||
|
|
a9459fe746 | ||
|
|
5a3228f05b | ||
|
|
befc238db9 | ||
|
|
ae75a1f8d4 | ||
|
|
d1c50bfd73 | ||
|
|
6f540366f4 | ||
|
|
151fc760fe | ||
|
|
be4391a4b6 | ||
|
|
76ae3e0f6b | ||
|
|
f37b6edaa6 | ||
|
|
7095f77f93 | ||
|
|
5853b778f2 | ||
|
|
6405725ce6 | ||
|
|
111d6141aa | ||
|
|
5560b3c8e6 | ||
|
|
d528d541ea | ||
|
|
ab6b51cc1f | ||
|
|
35aa4f2658 | ||
|
|
36db3c37e8 | ||
|
|
b0c9232153 | ||
|
|
0070fb9e5c | ||
|
|
aa33ac4b1d | ||
|
|
1c2bc5b395 | ||
|
|
f2fe18a4c5 | ||
|
|
87fdcbebf6 | ||
|
|
68a4f651ea | ||
|
|
243caa9626 | ||
|
|
863684b916 | ||
|
|
2d543f3728 | ||
|
|
47088cfbee | ||
|
|
66c75cbf79 | ||
|
|
b2c68d88ae | ||
|
|
675e998cba | ||
|
|
db1d20ae36 | ||
|
|
59b40dda4e | ||
|
|
317fab4dfe | ||
|
|
6c83ab68b4 | ||
|
|
2194465757 | ||
|
|
a0e70f88ad | ||
|
|
9102a3d852 | ||
|
|
b05cba896e | ||
|
|
6716a31b6c | ||
|
|
2e42d0e7eb | ||
|
|
dee9864157 | ||
|
|
0293fc8c6f | ||
|
|
725fdc03a6 | ||
|
|
4557cb695f | ||
|
|
172403a061 | ||
|
|
d03c3c185e | ||
|
|
50a111343c | ||
|
|
f01693adc3 | ||
|
|
d2bbbb4448 | ||
|
|
38c0aadff2 | ||
|
|
79d593b155 | ||
|
|
ddc84cfecc | ||
|
|
b620383d94 | ||
|
|
2319dba68d | ||
|
|
0c388ee3a7 | ||
|
|
fb5b3e2964 | ||
|
|
d44eaa5b41 | ||
|
|
2eaf05df27 | ||
|
|
9e5431e205 | ||
|
|
26c3102af8 | ||
|
|
104f06ef2b | ||
|
|
93c8654e84 | ||
|
|
a7261790bd | ||
|
|
d8d6e521f6 | ||
|
|
02497bcd40 | ||
|
|
b498e15620 | ||
|
|
1288d312c0 | ||
|
|
84cd350c42 | ||
|
|
100bf89074 | ||
|
|
d66c80f95a | ||
|
|
26f04bbd94 | ||
|
|
6582c03823 | ||
|
|
abacaf2e02 | ||
|
|
c65d8b9856 | ||
|
|
8f94cf06de | ||
|
|
94c1c16de8 | ||
|
|
e69d992725 | ||
|
|
e6c5dd6e22 | ||
|
|
5486d4874a | ||
|
|
9d9d8e03fe | ||
|
|
6ee50243a2 | ||
|
|
3ec078f9c1 | ||
|
|
4c3209be6d | ||
|
|
7fe8f9a8dc | ||
|
|
7fa2b29418 | ||
|
|
dd4edd5a85 | ||
|
|
b85df5a0d3 | ||
|
|
1a7bec3d4e | ||
|
|
727c4b454f | ||
|
|
4ba86b34a9 | ||
|
|
277a35aed9 | ||
|
|
28c1ffa09f | ||
|
|
10595680a9 | ||
|
|
577298dbc5 | ||
|
|
49ca4dfbf7 | ||
|
|
89ec47deec | ||
|
|
a043a46457 | ||
|
|
eb8e3a0fd1 | ||
|
|
fc25df8104 | ||
|
|
d8ffd3e543 | ||
|
|
ea4ec9310d | ||
|
|
479ec85e0d | ||
|
|
3e4b450260 | ||
|
|
f6a81949d0 | ||
|
|
6896962606 | ||
|
|
9b6d6ab4b0 | ||
|
|
d5f399f7d5 | ||
|
|
dbd3bd024c | ||
|
|
9ba7e19da3 | ||
|
|
5f6d1412cf | ||
|
|
0b5af3145e | ||
|
|
ad10ea4e96 | ||
|
|
9de53a972a | ||
|
|
4043efbb9a | ||
|
|
42019644f0 | ||
|
|
bcc25ef42f | ||
|
|
602bbf3b60 | ||
|
|
585ac6074f | ||
|
|
5bd1a43df6 | ||
|
|
c9279a4b59 | ||
|
|
9ff8eed6ba | ||
|
|
96ffdd251f | ||
|
|
35b8b7d01c | ||
|
|
4e2e2b2b3c | ||
|
|
bfff0c8cf4 | ||
|
|
e253f445b9 | ||
|
|
73ed663c9c | ||
|
|
2236150b83 | ||
|
|
da24feb390 | ||
|
|
44a5ada162 | ||
|
|
e57cc2587c | ||
|
|
38bfb9baa5 | ||
|
|
540636d59b | ||
|
|
3cc784353e | ||
|
|
44946ee784 | ||
|
|
a9d77487c6 | ||
|
|
70d4f27abd | ||
|
|
9fca09661d | ||
|
|
2f0efd0223 | ||
|
|
5c31ed53df | ||
|
|
87640ea238 | ||
|
|
eb8f771b06 | ||
|
|
b080b5862e | ||
|
|
df50e7527e | ||
|
|
e2b43575c0 | ||
|
|
2059793562 | ||
|
|
e5df21e165 | ||
|
|
792029b13f | ||
|
|
0ef49d7116 | ||
|
|
600d3291eb | ||
|
|
b78413190c | ||
|
|
c9729af139 | ||
|
|
697f267fc3 | ||
|
|
ac49ce3cc5 | ||
|
|
555daf6888 | ||
|
|
8f0c2994a6 | ||
|
|
873e66db19 | ||
|
|
38b542def4 | ||
|
|
a646e5efc0 | ||
|
|
31590c9229 | ||
|
|
98c2172908 | ||
|
|
314b02daf1 | ||
|
|
d2ab78b65c | ||
|
|
db2c352fd1 | ||
|
|
6e79ad2793 | ||
|
|
77eaa287fe | ||
|
|
a64820048c | ||
|
|
4ff4a5a893 | ||
|
|
5ea5681d2b | ||
|
|
2bb327b747 | ||
|
|
4dcdf60309 | ||
|
|
009fd5b2ec | ||
|
|
091e7e5bc1 | ||
|
|
ea1202bb65 | ||
|
|
cb413febaa | ||
|
|
364e55d477 | ||
|
|
680a35a26d | ||
|
|
08ff59e463 | ||
|
|
9db4c8c333 | ||
|
|
2489c2da46 | ||
|
|
fbc58b0707 | ||
|
|
d5a525789e | ||
|
|
34066921e1 | ||
|
|
901c594311 | ||
|
|
5f619e608b | ||
|
|
49295090ad | ||
|
|
a6e43adb0a | ||
|
|
1f165bb6cb | ||
|
|
8fc9d88d2e | ||
|
|
d7e5a5ce05 | ||
|
|
09d446fdeb | ||
|
|
c0bbd60e15 | ||
|
|
af3fc137be | ||
|
|
0ee5e1bd88 | ||
|
|
f92ea18651 | ||
|
|
3cce2bb35f | ||
|
|
e080ce42ee | ||
|
|
5dbe41da78 | ||
|
|
eef3957d4f | ||
|
|
680f4596b9 | ||
|
|
8de344f220 | ||
|
|
8005846955 | ||
|
|
3edd60448b | ||
|
|
4a2fa5c076 | ||
|
|
96e9c2cc4e | ||
|
|
2089a1bb35 | ||
|
|
023c2718e2 | ||
|
|
f7aacb193a | ||
|
|
e710040904 | ||
|
|
fb12826801 | ||
|
|
833a70c6e0 | ||
|
|
c807f9021c | ||
|
|
46b66bcdf0 | ||
|
|
2ebade817e | ||
|
|
080342e619 | ||
|
|
75b06d3ba1 | ||
|
|
3b6fa3343d | ||
|
|
469cdc9b1d | ||
|
|
8a179e7194 | ||
|
|
c133795753 | ||
|
|
941ace7461 | ||
|
|
8589420f5d | ||
|
|
ece24e182f | ||
|
|
38e5bb7bdf | ||
|
|
d09e931827 | ||
|
|
3f3f1e1c20 | ||
|
|
32f2468ba8 | ||
|
|
1d6540e0d9 | ||
|
|
31da4bfca1 | ||
|
|
41d49894e2 | ||
|
|
6dcdd15b1c | ||
|
|
c2b275fd90 | ||
|
|
e4838ef3ef | ||
|
|
55ca4660d7 | ||
|
|
c16c903ec4 | ||
|
|
937cf38ff6 | ||
|
|
07f76e854d | ||
|
|
cde1d084c6 | ||
|
|
9362c5d6ae | ||
|
|
117a9108c0 | ||
|
|
5214e461f2 | ||
|
|
9bdb178b7d | ||
|
|
9c526ffdba | ||
|
|
f80a74edd1 | ||
|
|
7d8c6c0a62 | ||
|
|
17b26b869b | ||
|
|
dfbe54e92f | ||
|
|
2321de409b | ||
|
|
502287a258 | ||
|
|
052ff0ea6e | ||
|
|
2b76789cdf | ||
|
|
7bbf5df231 | ||
|
|
794a04e7d3 | ||
|
|
645467f13a | ||
|
|
7e87637341 | ||
|
|
b7abeed54a | ||
|
|
d53646eef2 | ||
|
|
c54743de6c | ||
|
|
e1fdc41132 | ||
|
|
060aa82294 | ||
|
|
2df08eac3f | ||
|
|
0ecdb9d3d0 | ||
|
|
6a7fe1bd1f | ||
|
|
87c69cb3d0 | ||
|
|
c654855c22 | ||
|
|
11bb006801 | ||
|
|
e5551f3f64 | ||
|
|
0de148005f | ||
|
|
27e4ce628e | ||
|
|
19b752bf15 | ||
|
|
6528e5bba9 | ||
|
|
37e1183e87 | ||
|
|
824512cf8e | ||
|
|
82f296b17d | ||
|
|
8302530954 | ||
|
|
6c271829aa | ||
|
|
3b30d339ee | ||
|
|
c5a268bdbd | ||
|
|
8457ea8e47 | ||
|
|
72d69eeab5 | ||
|
|
31885e41af | ||
|
|
e63726d18b | ||
|
|
3d2ab45d25 | ||
|
|
246478e1d8 | ||
|
|
9418de3b23 | ||
|
|
c6b68cad37 | ||
|
|
ef36e4d3ac | ||
|
|
ba2ddd62ad | ||
|
|
6a03e1488c | ||
|
|
cabd2ed562 | ||
|
|
4a9296276d | ||
|
|
1fd84d824c | ||
|
|
faa55ffe2d | ||
|
|
d45af4191f | ||
|
|
9f8dead8b8 | ||
|
|
564037cd53 | ||
|
|
434de6292c | ||
|
|
a12e6990e5 | ||
|
|
ce0c4a094b | ||
|
|
e36728ec08 | ||
|
|
2c1f3090a0 | ||
|
|
0c6d4b3581 | ||
|
|
1d5b9eb241 | ||
|
|
b0567dafe3 | ||
|
|
518f78f067 | ||
|
|
47a2c21824 | ||
|
|
aca5547965 | ||
|
|
0e07876630 | ||
|
|
a43901b4a4 | ||
|
|
6515fac16d | ||
|
|
fdaa7e9f48 | ||
|
|
cd779850de | ||
|
|
ef939f4357 | ||
|
|
bfb9dd7009 | ||
|
|
f282f14eff | ||
|
|
992b289500 | ||
|
|
9dcdebcab2 | ||
|
|
c0b8bacab0 | ||
|
|
f96e6420fc | ||
|
|
d7f97480cc | ||
|
|
26df41b571 | ||
|
|
4ca336017d | ||
|
|
94252ea54c | ||
|
|
f8220fec1f | ||
|
|
a5ded396c9 | ||
|
|
c457cf8064 | ||
|
|
3079f90118 | ||
|
|
4fbb331015 | ||
|
|
640c0d305c | ||
|
|
0b044c67bb | ||
|
|
7a37ba8145 | ||
|
|
b447863d9f | ||
|
|
6dd7e14f6f | ||
|
|
b09ef98dce | ||
|
|
ff1cf8a288 | ||
|
|
603793662a | ||
|
|
4cdab0dbc7 | ||
|
|
d745279fec | ||
|
|
df34c9f2fd | ||
|
|
37da9aabe3 | ||
|
|
3a2205147c | ||
|
|
5c6acaf10a | ||
|
|
d916f2dea5 | ||
|
|
254a1aedcb | ||
|
|
dce3175388 | ||
|
|
e2b6b45418 | ||
|
|
e210d1e9fc | ||
|
|
df0f449e13 | ||
|
|
895105c900 | ||
|
|
209538709b | ||
|
|
ef7c35286b | ||
|
|
70af0828cb | ||
|
|
ef18e311b1 | ||
|
|
68de84973a | ||
|
|
ef6d4f9998 | ||
|
|
9f5413963b | ||
|
|
ec7c0a5cb7 | ||
|
|
f82bd9eadf | ||
|
|
843c064ebe | ||
|
|
8a52e1d1d0 | ||
|
|
8ec220dc9a | ||
|
|
9c4b9f4433 | ||
|
|
6059a29660 | ||
|
|
e65e6764fa | ||
|
|
1e77cb234a | ||
|
|
4a357087d1 | ||
|
|
6a3823cdff | ||
|
|
c2372ce96e | ||
|
|
0f5e79b163 | ||
|
|
fa4026c83f | ||
|
|
3e4d66ca27 | ||
|
|
2ec94dc13c | ||
|
|
6b11e08f3d | ||
|
|
e884e56b47 | ||
|
|
2d74affed4 | ||
|
|
3572b17049 | ||
|
|
4ee7091cd1 | ||
|
|
ff5e89d97c | ||
|
|
c4e82afc0c | ||
|
|
52c5d85487 | ||
|
|
fd0438f80b | ||
|
|
cb5781ca65 | ||
|
|
4d8aad9dfe | ||
|
|
39429a2e44 | ||
|
|
0a3514d81e | ||
|
|
4e5e0a7c34 | ||
|
|
f6cc6a9bef | ||
|
|
2e545e9fb9 | ||
|
|
d49ef64e7e | ||
|
|
b1e6eaef90 | ||
|
|
3420d0199b | ||
|
|
671db35571 | ||
|
|
d724f87726 | ||
|
|
5ab8565205 | ||
|
|
660c567d1b | ||
|
|
2c68d13c56 | ||
|
|
b9fc155b4f | ||
|
|
8dd5aefc81 | ||
|
|
f9aaf5dcef | ||
|
|
34a6e773df | ||
|
|
f0beb03344 | ||
|
|
1ff9a81ab3 | ||
|
|
9b1771c693 | ||
|
|
823e604737 | ||
|
|
97a31bdf17 | ||
|
|
ee7a198ae6 | ||
|
|
f143a51010 | ||
|
|
e483185b2c | ||
|
|
40e1c75746 | ||
|
|
456bd4693c | ||
|
|
5ac9c0f760 | ||
|
|
f621d84b83 | ||
|
|
0dd001e95a | ||
|
|
69b5235db9 | ||
|
|
a673d63b33 | ||
|
|
36485e10f2 | ||
|
|
8a1a062127 | ||
|
|
5961e0de08 | ||
|
|
60bda521a0 | ||
|
|
49f63c0f0d | ||
|
|
33910aedb8 | ||
|
|
3359a81a14 | ||
|
|
e27f850e68 | ||
|
|
07340b8f53 | ||
|
|
2f9dd7faa8 | ||
|
|
3f81fd4a87 | ||
|
|
4dd961fc66 | ||
|
|
0bd70bfb32 | ||
|
|
3ab845d2d6 | ||
|
|
443f09d5a2 | ||
|
|
3500952bb7 | ||
|
|
e98182c4b5 | ||
|
|
2337827744 | ||
|
|
556dd66af3 | ||
|
|
8bdad3f4f9 | ||
|
|
6346d4143d | ||
|
|
fd8dc88760 | ||
|
|
b2810f390e | ||
|
|
23d3cd0f4b | ||
|
|
4a3c76773d | ||
|
|
d23e7604f0 | ||
|
|
0b3f6c6eff | ||
|
|
f2869c892b | ||
|
|
3755a9529b | ||
|
|
890f0593ee | ||
|
|
e6d666b5b0 | ||
|
|
f440acf145 | ||
|
|
1ad0e0c6dd | ||
|
|
bb8691ddc0 | ||
|
|
cf3041d07b | ||
|
|
313b3d5d6b | ||
|
|
122e7336be | ||
|
|
e954453f6b | ||
|
|
55ef3d09f3 | ||
|
|
88b9cc5ac9 | ||
|
|
f07cb7470d | ||
|
|
bd874a0232 | ||
|
|
7cc4cccb9c | ||
|
|
e85a3bacb7 | ||
|
|
530dfa758f | ||
|
|
8bc3489060 | ||
|
|
6914cb171a | ||
|
|
176c2bbd4c | ||
|
|
7771954455 | ||
|
|
a66743bb20 | ||
|
|
851c7b75f9 | ||
|
|
ed0b406027 | ||
|
|
71985972a2 | ||
|
|
2dbeef9290 | ||
|
|
a79453270d | ||
|
|
ae71808c61 | ||
|
|
5c195ba37b | ||
|
|
607ee17a86 | ||
|
|
a15aacc5b3 | ||
|
|
78fb4b1a65 | ||
|
|
8e566cba72 | ||
|
|
36184288de | ||
|
|
6fc5bf38a7 | ||
|
|
e146ecde70 | ||
|
|
72b796019a | ||
|
|
0f66d15ba8 | ||
|
|
4695e933b8 | ||
|
|
5e1da6cd6f | ||
|
|
0c3bbc0980 | ||
|
|
d2bb5bea63 | ||
|
|
c76e191d4d | ||
|
|
171594d0ae | ||
|
|
4e5f499b48 | ||
|
|
bb75ad8b8e | ||
|
|
482cc7028b | ||
|
|
735bdb3854 | ||
|
|
1ee1bd78af | ||
|
|
00463c729d | ||
|
|
dac636b3bb | ||
|
|
ad87121506 | ||
|
|
55d802f005 | ||
|
|
f17164a3d9 | ||
|
|
0b9f9b2335 | ||
|
|
71d3e24241 | ||
|
|
ccd4512e8f | ||
|
|
fe6b77d67d | ||
|
|
58d12f4e70 | ||
|
|
17303039b8 | ||
|
|
f70d5203a3 | ||
|
|
efd6b1db65 | ||
|
|
dcca0fa921 | ||
|
|
e5bc447807 | ||
|
|
cd6b4b5ab1 | ||
|
|
fb79ae6138 | ||
|
|
17801b0b13 | ||
|
|
9010388de0 | ||
|
|
2da901f994 | ||
|
|
7832cc7a5d | ||
|
|
13ab2e8d0e | ||
|
|
868d23a278 | ||
|
|
eaf2bca44e | ||
|
|
af8f537f58 | ||
|
|
bc7ff472be | ||
|
|
9b9a2b5203 | ||
|
|
347259394b | ||
|
|
ead61b5e37 | ||
|
|
c9f7814f5d | ||
|
|
3ee3355e81 | ||
|
|
ce8f8b0ce9 | ||
|
|
cae35cf3b9 | ||
|
|
900e3c23bc | ||
|
|
aaeffeb983 | ||
|
|
15600324be | ||
|
|
da4913edb5 | ||
|
|
e8cc1e4906 | ||
|
|
801fc3f9fd | ||
|
|
703b0b4cde | ||
|
|
d55895389a | ||
|
|
56009e482b | ||
|
|
767d1bb3e3 | ||
|
|
05132ac0f5 | ||
|
|
fb372f8c28 | ||
|
|
a2dc863b7a | ||
|
|
15671c6781 | ||
|
|
6296115adb | ||
|
|
0a099a91d0 | ||
|
|
5f060fe022 | ||
|
|
a4bbe1cf7b | ||
|
|
dce74ae7d5 | ||
|
|
871c509493 | ||
|
|
9333470e3b | ||
|
|
0c108f5232 | ||
|
|
39d298c867 | ||
|
|
376e804a28 | ||
|
|
79e5c0589d | ||
|
|
f6679b7368 | ||
|
|
a3c0b86b67 | ||
|
|
c8f48dd62b | ||
|
|
aed157697a | ||
|
|
913b8606d9 | ||
|
|
7cadbad36e | ||
|
|
abb5bdffcc | ||
|
|
6a44c15dd3 | ||
|
|
e669dfe533 | ||
|
|
be888f195d | ||
|
|
d87cdef311 | ||
|
|
ecf07cb520 | ||
|
|
10829d53b9 | ||
|
|
71093f2f31 | ||
|
|
e88192002a | ||
|
|
3e14282603 | ||
|
|
5f16f4ca77 | ||
|
|
7b4fa4331f | ||
|
|
3ad0a463b5 | ||
|
|
d28d640f77 | ||
|
|
594c0a6122 | ||
|
|
843a826437 | ||
|
|
c4660bc824 | ||
|
|
6053d38e4d | ||
|
|
3865254a1c | ||
|
|
65b61c09cb | ||
|
|
cce1618117 | ||
|
|
8b4b49a34b | ||
|
|
048c5dc2ec | ||
|
|
eb218da8fb | ||
|
|
65942a4ed4 | ||
|
|
1444155c09 | ||
|
|
84ac1d8cfc | ||
|
|
440e1e879a | ||
|
|
bcecf793d4 | ||
|
|
16c0d827e1 | ||
|
|
94f16a9583 | ||
|
|
4bdac3bbc2 | ||
|
|
2bc4a59aaf | ||
|
|
cf886de5ce | ||
|
|
9969966ac6 | ||
|
|
fb809338e3 | ||
|
|
c6284eb994 | ||
|
|
454bda4c82 | ||
|
|
26e21441a0 | ||
|
|
2edef0166e | ||
|
|
05cb89779b | ||
|
|
c0b2524952 | ||
|
|
f1bb445593 | ||
|
|
7f777f338f | ||
|
|
4449dfdbd6 | ||
|
|
6226e65e97 | ||
|
|
be1783c029 | ||
|
|
83a002ae5b | ||
|
|
f0fcb2b329 | ||
|
|
5ff695a5b3 | ||
|
|
43fe1f076e | ||
|
|
1afd7e042c | ||
|
|
ba6474f97e | ||
|
|
d8eed114aa | ||
|
|
e61ad6f79c | ||
|
|
9d27616003 | ||
|
|
0f2641909f | ||
|
|
9ca96acda2 | ||
|
|
77db6c5e98 | ||
|
|
c4aa9804d2 | ||
|
|
eb7bfe3a62 | ||
|
|
01fd7a8d56 | ||
|
|
81fb93e5d6 | ||
|
|
64716ec6e6 | ||
|
|
e0f787be32 | ||
|
|
138da79599 | ||
|
|
b8b290c2a5 | ||
|
|
f35dcc0ec6 | ||
|
|
62704e261a | ||
|
|
f16ace6169 | ||
|
|
35628aa93b | ||
|
|
64bf42dc1c | ||
|
|
55fc6a4282 | ||
|
|
1a59300cd7 | ||
|
|
9c7d6d835e | ||
|
|
2a20242654 | ||
|
|
171f9de073 | ||
|
|
3b4d896bcd | ||
|
|
4b602333bc | ||
|
|
347cd193dd | ||
|
|
964d8dde9b | ||
|
|
bf794ce67f | ||
|
|
51dc9ea8c1 | ||
|
|
791a1fd475 | ||
|
|
244eb5f692 | ||
|
|
003bc33c25 | ||
|
|
b488b9f1df | ||
|
|
ff4f0a59af | ||
|
|
10e2c4bbd4 | ||
|
|
18536330ce | ||
|
|
e894069d4e | ||
|
|
5be2ca0deb | ||
|
|
83e2405151 | ||
|
|
220c0dfb01 | ||
|
|
5474f1ec0e | ||
|
|
dbd01abde8 | ||
|
|
83bbab0969 | ||
|
|
cc6808a6cd | ||
|
|
191fd89cfb | ||
|
|
42e5b46365 | ||
|
|
f444ce86f9 | ||
|
|
ce05b3d087 | ||
|
|
ab4dd28c51 | ||
|
|
9e23b287ff | ||
|
|
135c1ab10b | ||
|
|
de680c93f6 | ||
|
|
57ac57824f | ||
|
|
38df64c191 | ||
|
|
575969f938 | ||
|
|
42c388e5c9 | ||
|
|
6346433d45 | ||
|
|
44e63e00c3 | ||
|
|
8648b44583 | ||
|
|
086d540ee2 | ||
|
|
adf6836ee5 | ||
|
|
62f4406c51 | ||
|
|
e4c201e105 | ||
|
|
b6b0b1e7b3 | ||
|
|
9d857d015f | ||
|
|
2055c595bb | ||
|
|
00e33ceac7 | ||
|
|
77ef1ecebf | ||
|
|
57672a2a9c | ||
|
|
0ae594b198 | ||
|
|
14a7e1c72d | ||
|
|
e570cbef12 | ||
|
|
c6d732a723 | ||
|
|
9cb0131a37 | ||
|
|
d72b665cfe | ||
|
|
d6b87585f7 | ||
|
|
2e01692e92 | ||
|
|
78ad79c08f | ||
|
|
725446a022 | ||
|
|
3bd7594b7f | ||
|
|
2bd314732a | ||
|
|
f4d7776bca | ||
|
|
80e13beb2b | ||
|
|
5c37826710 | ||
|
|
693a288839 | ||
|
|
ecc0e1b60d | ||
|
|
1d60a4ca42 | ||
|
|
7bf343d071 | ||
|
|
0430b0ed53 | ||
|
|
9147dd9925 | ||
|
|
b6e7fac619 | ||
|
|
c1d15e570d | ||
|
|
72b067c8a7 | ||
|
|
94df9a681d | ||
|
|
1d30bb7e8e | ||
|
|
0779e326d7 | ||
|
|
3ad5b67fef | ||
|
|
d747bf72fd | ||
|
|
8474cb5789 | ||
|
|
83650c7dc9 | ||
|
|
74bbbc4b1d | ||
|
|
743e4f3e31 | ||
|
|
d7a699c714 | ||
|
|
366ab8355a | ||
|
|
e721270343 | ||
|
|
2ba7d1a425 | ||
|
|
b1a5dd3b3c | ||
|
|
cbc2d1ef9b | ||
|
|
95f58df3c2 | ||
|
|
01da700458 | ||
|
|
62f9a000f2 | ||
|
|
90f32638be | ||
|
|
6aa141bb33 | ||
|
|
6d46ad44a1 | ||
|
|
afea71cfeb | ||
|
|
866b3c1f65 | ||
|
|
858f89a7b9 | ||
|
|
98d68d4f6e | ||
|
|
ea69e725eb | ||
|
|
7a27078532 | ||
|
|
2ef48bf6ce | ||
|
|
588a209a6e | ||
|
|
8103c4f50a | ||
|
|
cb74c63474 | ||
|
|
1e9711d58b | ||
|
|
48876d2726 | ||
|
|
56da84052c | ||
|
|
e2e5bd54a1 | ||
|
|
c071776215 | ||
|
|
a06361f246 | ||
|
|
3a7b68430c | ||
|
|
d85e2498fd | ||
|
|
507382b6d2 | ||
|
|
f343cf8549 | ||
|
|
832d5baf35 | ||
|
|
0253f32574 | ||
|
|
6173180272 | ||
|
|
2d21931fb1 | ||
|
|
bdaa7187fd | ||
|
|
e7ad6e06ec | ||
|
|
7c14483852 | ||
|
|
8b4b1f934e | ||
|
|
713eb3f169 | ||
|
|
48297c80d1 | ||
|
|
315ffdac92 | ||
|
|
7ddc17e6e5 | ||
|
|
e3b8073c82 | ||
|
|
80517e416c | ||
|
|
4b71aade9d | ||
|
|
1b52698c54 | ||
|
|
b8f372d83f | ||
|
|
7bc5f55fb2 |
No files matched your search
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(yarn build:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm show:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(yarn test:unit:*)",
|
||||
"Bash(npx tsc:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
tmp/*
|
||||
Dockerfile
|
||||
docker-compose
|
||||
src/core/engines/gows/proto
|
||||
.*sessions
|
||||
sessions
|
||||
files
|
||||
venv
|
||||
.wwebjs_auth
|
||||
@@ -7,6 +11,8 @@ docs
|
||||
examples
|
||||
.github
|
||||
.git
|
||||
.envs/*
|
||||
.env
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
# WAHA - WhatsApp HTTP API
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
#
|
||||
|
||||
# ====================
|
||||
# ===== SECURITY =====
|
||||
# ====================
|
||||
WAHA_API_KEY=00000000000000000000000000000000
|
||||
WAHA_DASHBOARD_USERNAME=admin
|
||||
WAHA_DASHBOARD_PASSWORD=11111111111111111111111111111111
|
||||
WHATSAPP_SWAGGER_USERNAME=admin
|
||||
WHATSAPP_SWAGGER_PASSWORD=11111111111111111111111111111111
|
||||
|
||||
# Disable Dashboard or Swagger
|
||||
WAHA_DASHBOARD_ENABLED=True
|
||||
WHATSAPP_SWAGGER_ENABLED=True
|
||||
|
||||
# ==================
|
||||
# ===== COMMON =====
|
||||
# ==================
|
||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
# So you don't have to create sessions/apps when switching between engines
|
||||
WAHA_NAMESPACE=all
|
||||
|
||||
# "Firefox (YourApp)" in Linked Devices
|
||||
# WAHA_CLIENT_DEVICE_NAME=YourApp
|
||||
|
||||
# Base URL for the API (used for webhooks, file URLs, etc.)
|
||||
WAHA_BASE_URL=http://localhost:3000
|
||||
# WAHA_PUBLIC_URL=http://localhost:3000
|
||||
|
||||
# Server configuration (if you need to customize hostname/port)
|
||||
# WHATSAPP_API_SCHEMA=http
|
||||
# WHATSAPP_API_PORT=3000
|
||||
# WHATSAPP_API_HOSTNAME=localhost
|
||||
|
||||
|
||||
# ===================
|
||||
# ====== GOWS =======
|
||||
# ===================
|
||||
# By default, GOWS uses cgo (libc) DNS resolver
|
||||
# You can ask to use golang one, but it's less reliable
|
||||
# GODEBUG=netdns=go
|
||||
|
||||
# ===================
|
||||
# ===== LOGGING =====
|
||||
# ===================
|
||||
# Log format: JSON (for log management systems) or PRETTY (for development)
|
||||
WAHA_LOG_FORMAT=JSON
|
||||
|
||||
# Log level: info, debug, error, warn
|
||||
WAHA_LOG_LEVEL=info
|
||||
|
||||
# Don't print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
|
||||
# =========================
|
||||
# ===== MEDIA STORAGE =====
|
||||
# =========================
|
||||
# Local storage (default)
|
||||
WAHA_MEDIA_STORAGE=LOCAL
|
||||
WHATSAPP_FILES_LIFETIME=0
|
||||
WHATSAPP_FILES_FOLDER=/app/.media
|
||||
|
||||
# Media download settings
|
||||
# WHATSAPP_DOWNLOAD_MEDIA=true
|
||||
# WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
|
||||
# S3 storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=S3
|
||||
# WAHA_S3_REGION=eu-west-2
|
||||
# WAHA_S3_BUCKET=waha
|
||||
# WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
# WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
# WAHA_S3_ENDPOINT=http://minio:9000
|
||||
# WAHA_S3_FORCE_PATH_STYLE=True
|
||||
# WAHA_S3_PROXY_FILES=True
|
||||
|
||||
# PostgreSQL storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=POSTGRESQL
|
||||
# WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# ===========================
|
||||
# ===== SESSION STORAGE =====
|
||||
# ===========================
|
||||
# PostgreSQL for sessions (uncomment to use)
|
||||
# WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# MongoDB for sessions (uncomment to use)
|
||||
# WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
|
||||
|
||||
# =================
|
||||
# ===== APPS ======
|
||||
# =================
|
||||
# WAHA_APPS_ENABLED=True
|
||||
# REDIS_URL=redis://:redis@redis:6379
|
||||
|
||||
# Apps Jobs configuration - *_AGE in seconds
|
||||
# WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_AGE=
|
||||
# WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_COUNT=
|
||||
# WAHA_APPS_JOBS_REMOVE_ON_FAIL_AGE=
|
||||
# WAHA_APPS_JOBS_REMOVE_ON_FAIL_COUNT
|
||||
|
||||
|
||||
# ==================================
|
||||
# ===== ADVANCED CONFIGURATION =====
|
||||
# ==================================
|
||||
|
||||
# Timezone for screenshots and logs
|
||||
# TZ=Europe/Dublin
|
||||
|
||||
# Session management
|
||||
# WHATSAPP_START_SESSION=session1,session2
|
||||
# WHATSAPP_RESTART_ALL_SESSIONS=False
|
||||
|
||||
# Webhooks
|
||||
# WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# WHATSAPP_HOOK_CUSTOM_HEADERS=custom_header_key:custom_header_value
|
||||
# WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
|
||||
# Proxy configuration
|
||||
# WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
# WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
# WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
|
||||
# HTTPS configuration
|
||||
# !DEPRECATED!
|
||||
# Setup nginx reverse proxy to handle TLS connection
|
||||
# using Let's encrypt or self-issued certificate
|
||||
# WAHA_HTTPS_ENABLED=true
|
||||
# WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
# WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem
|
||||
# WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
@@ -0,0 +1,3 @@
|
||||
src/core/engines/gows/grpc/*
|
||||
src/core/engines/gows/proto/*
|
||||
src/core/engines/webjs/_lodash.js
|
||||
@@ -1,36 +0,0 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'simple-import-sort'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
'prettier/@typescript-eslint',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'lines-between-class-members': [
|
||||
'error',
|
||||
'always',
|
||||
{ exceptAfterSingleLine: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,30 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: '[BUG][WEBJS] - '
|
||||
title: '[WEBJS] - '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
type: 'BUG'
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
### Describe the bug
|
||||
|
||||
> A clear and concise description of what the bug is. Feel free to remove
|
||||
> sections that you don't feel to make the text shorter!
|
||||
A clear and concise description of what the bug is. Feel free to remove sections
|
||||
that you don't feel to make the text shorter!
|
||||
|
||||
**Version**
|
||||
### Version
|
||||
|
||||
Get the WAHA version by calling `GET /api/version`
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2023.11.1",
|
||||
"engine": "NOWEB",
|
||||
"tier": "PLUS",
|
||||
"browser": "/usr/bin/google-chrome-stable"
|
||||
"version": "YYYY.MM.DD",
|
||||
"engine": "ENGINE",
|
||||
"tier": "TIER"
|
||||
}
|
||||
```
|
||||
|
||||
> You can get the WAHA version by calling `GET /api/version` Try to update to
|
||||
> [the latest version](https://github.com/devlikeapro/waha/releases) before
|
||||
> creating an issue!
|
||||
Try to update to
|
||||
[the latest version](https://github.com/devlikeapro/waha/releases) before
|
||||
creating an issue!
|
||||
|
||||
## Steps
|
||||
|
||||
**To Reproduce** Steps to reproduce the behavior:
|
||||
|
||||
@@ -33,29 +37,23 @@ assignees: ''
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
### Expected behavior
|
||||
|
||||
> A clear and concise description of what you expected to happen.
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Engine** I'm using **WEBJS** engine.
|
||||
### Requests - Responses
|
||||
|
||||
> If you set `WHATSAPP_DEFAULT_ENGINE` explicitly to
|
||||
> [one of the engine](https://waha.devlike.pro/docs/how-to/engines/) - please
|
||||
> provide that information here and in the issue title.
|
||||
Please attach all sent requests, responses that you sent.
|
||||
|
||||
**Requests - Responses**
|
||||
### Docker Logs
|
||||
|
||||
> Please attach all sent requests, responses that you sent.
|
||||
Collect and attach related docker logs if you have any
|
||||
https://waha.devlike.pro/docs/how-to/deploy/#viewing-the-logs
|
||||
|
||||
**Docker Logs**
|
||||
### Screenshots
|
||||
|
||||
> Collect and attach related docker logs if you have any
|
||||
> https://waha.devlike.pro/docs/how-to/deploy/#viewing-the-logs
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Screenshots**
|
||||
### Additional context
|
||||
|
||||
> If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
|
||||
> Add any other context about the problem here.
|
||||
Add any other context about the problem here.
|
||||
@@ -1,24 +1,25 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: '[Feature Request]'
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: 'Feature'
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.** I'm always
|
||||
frustrated when [...]
|
||||
|
||||
> A clear and concise description of what the problem is. Ex.
|
||||
A clear and concise description of what the problem is. Ex.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
> A clear and concise description of what you want to happen.
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
> A clear and concise description of any alternative solutions or features
|
||||
> you've considered.
|
||||
A clear and concise description of any alternative solutions or features you've
|
||||
considered.
|
||||
|
||||
**Additional context** Add any other context or screenshots about the feature
|
||||
request here.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Question
|
||||
about: I have a question about the project or feature
|
||||
title: '[Question] '
|
||||
labels: 'question'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
👉 Kindly use
|
||||
[GitHub Discussions](https://github.com/devlikeapro/waha/discussions) to ask
|
||||
questions!
|
||||
+134
-15
@@ -8,38 +8,75 @@ on:
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name: ${{ matrix.platform }} - ${{ matrix.browser }} - ${{ matrix.tag }}
|
||||
name:
|
||||
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
||||
${{ matrix.tag }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Chromium
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
# Chromium - x86
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'latest'
|
||||
platform: 'amd64'
|
||||
browser: 'chromium'
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chromium - ARM
|
||||
- runner: 'ubuntu-24.04-arm'
|
||||
tag: 'arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'chromium'
|
||||
# Chrome
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
# Chrome - x86
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'chrome'
|
||||
platform: 'amd64'
|
||||
browser: 'chrome'
|
||||
# Chrome is not supported on arm64
|
||||
# - runner: "buildjet-4vcpu-ubuntu-2204-arm"
|
||||
# tag: "chrome-arm"
|
||||
# platform: "linux/arm64"
|
||||
# browser: "chrome"
|
||||
# No browser
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chrome - ARM (Chrome is not available for ARM)
|
||||
#- runner: "ubuntu-24.04-arm"
|
||||
# tag: "chrome-arm"
|
||||
# platform: "linux/arm64"
|
||||
# browser: "chrome"
|
||||
# goss: 'goss-linux-arm'
|
||||
|
||||
# NOWEB - x86
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'noweb'
|
||||
platform: 'amd64'
|
||||
browser: 'none'
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
engine: 'NOWEB'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# NOWEB - ARM
|
||||
- runner: 'ubuntu-24.04-arm'
|
||||
tag: 'noweb-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'none'
|
||||
engine: 'NOWEB'
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
# GOWS - x86
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'gows'
|
||||
platform: 'amd64'
|
||||
browser: 'none'
|
||||
engine: 'GOWS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# No browser - ARM
|
||||
- runner: 'ubuntu-24.04-arm'
|
||||
tag: 'gows-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'none'
|
||||
engine: 'GOWS'
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -64,7 +101,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
- name: Build (attempt 1)
|
||||
id: build1
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
@@ -72,6 +111,86 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Wait to retry build
|
||||
if: steps.build1.outcome == 'failure'
|
||||
run: |
|
||||
sleep 10
|
||||
|
||||
- name: Build (attempt 2)
|
||||
id: build2
|
||||
if: steps.build1.outcome == 'failure'
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Wait to retry build
|
||||
if: steps.build2.outcome == 'failure'
|
||||
run: |
|
||||
sleep 10
|
||||
|
||||
- name: Build (attempt 3)
|
||||
if: steps.build2.outcome == 'failure'
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Install goss
|
||||
env:
|
||||
GOSS_DOWNLOAD: 'https://github.com/goss-org/goss/releases/download'
|
||||
GOSS_VERSION: 'v0.4.8'
|
||||
run: |
|
||||
curl -L https://github.com/goss-org/goss/releases/download/${{ env.GOSS_VERSION }}/${{ matrix.goss }} -o /usr/local/bin/goss
|
||||
chmod +rx /usr/local/bin/goss
|
||||
|
||||
- name: Run smoke tests
|
||||
working-directory: tests/smoke
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
docker run -d \
|
||||
--name smoke \
|
||||
--rm -p3000:3000 \
|
||||
-e WAHA_API_KEY=test \
|
||||
${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
|
||||
|
||||
sleep 3
|
||||
docker logs smoke
|
||||
goss validate --retry-timeout 30s --sleep 1s
|
||||
docker rm --force smoke
|
||||
|
||||
- name: Push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,218 @@
|
||||
name: Dev
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# unit-tests:
|
||||
# if: github.repository == 'devlikeapro/waha-plus'
|
||||
# runs-on: ubuntu-22.04
|
||||
# name: Unit tests
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
#
|
||||
# - name: Set up Node
|
||||
# uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: 22
|
||||
#
|
||||
# - name: Enable Corepack
|
||||
# run: |
|
||||
# corepack enable
|
||||
# corepack prepare yarn@4.9.2 --activate
|
||||
#
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# unset GIT_CONFIG_PARAMETERS
|
||||
# yarn install --immutable
|
||||
#
|
||||
# - name: Run unit tests
|
||||
# run: yarn test:unit
|
||||
|
||||
docker-build:
|
||||
if: github.repository == 'devlikeapro/waha-plus'
|
||||
# needs: unit-tests
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name:
|
||||
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
||||
${{ matrix.tag }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Chromium - x86
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'dev'
|
||||
platform: 'amd64'
|
||||
browser: 'chromium'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chrome - x86
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'dev-chrome'
|
||||
platform: 'amd64'
|
||||
browser: 'chrome'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chromium - ARM
|
||||
- runner: 'ubuntu-24.04-arm'
|
||||
tag: 'dev-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'chromium'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check recent changes
|
||||
id: recent
|
||||
run: |
|
||||
count=$(git rev-list --after="24 hours ago" --count HEAD || true)
|
||||
if [ "$count" -gt 0 ]; then
|
||||
echo "has_recent=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_recent=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: No recent changes summary
|
||||
if: ${{ steps.recent.outputs.has_recent != 'true' }}
|
||||
run:
|
||||
echo "No commits in the last 24 hours; skipping build." >>
|
||||
"$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ steps.recent.outputs.has_recent == 'true' }}
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Docker meta
|
||||
if: ${{ steps.recent.outputs.has_recent == 'true' }}
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ vars.DOCKER_IMAGE }}
|
||||
flavor: latest=false
|
||||
tags: |
|
||||
type=raw,value=${{ matrix.tag }}
|
||||
|
||||
- name: Login to image repository
|
||||
if: ${{ steps.recent.outputs.has_recent == 'true' }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build (attempt 1)
|
||||
id: build1
|
||||
if: ${{ steps.recent.outputs.has_recent == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Wait to retry build
|
||||
if:
|
||||
${{ steps.recent.outputs.has_recent == 'true' && steps.build1.outcome
|
||||
== 'failure' }}
|
||||
run: |
|
||||
sleep 10
|
||||
|
||||
- name: Build (attempt 2)
|
||||
id: build2
|
||||
if:
|
||||
${{ steps.recent.outputs.has_recent == 'true' && steps.build1.outcome
|
||||
== 'failure' }}
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Wait to retry build
|
||||
if:
|
||||
${{ steps.recent.outputs.has_recent == 'true' && steps.build2.outcome
|
||||
== 'failure' }}
|
||||
run: |
|
||||
sleep 10
|
||||
|
||||
- name: Build (attempt 3)
|
||||
if:
|
||||
${{ steps.recent.outputs.has_recent == 'true' && steps.build2.outcome
|
||||
== 'failure' }}
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Install goss
|
||||
if: ${{ steps.recent.outputs.has_recent == 'true' }}
|
||||
env:
|
||||
GOSS_DOWNLOAD: 'https://github.com/goss-org/goss/releases/download'
|
||||
GOSS_VERSION: 'v0.4.8'
|
||||
run: |
|
||||
curl -L https://github.com/goss-org/goss/releases/download/${{ env.GOSS_VERSION }}/${{ matrix.goss }} -o /usr/local/bin/goss
|
||||
chmod +rx /usr/local/bin/goss
|
||||
|
||||
- name: Run smoke tests
|
||||
if: ${{ steps.recent.outputs.has_recent == 'true' }}
|
||||
working-directory: tests/smoke
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
docker run -d \
|
||||
--name smoke \
|
||||
--rm -p3000:3000 \
|
||||
-e WAHA_API_KEY=test \
|
||||
${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
|
||||
|
||||
sleep 3
|
||||
docker logs smoke
|
||||
goss validate --retry-timeout 30s --sleep 1s
|
||||
docker rm --force smoke
|
||||
|
||||
- name: Push
|
||||
if: ${{ steps.recent.outputs.has_recent == 'true' }}
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -0,0 +1,51 @@
|
||||
name: 'github-translate'
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
discussion:
|
||||
types: [created]
|
||||
discussion_comment:
|
||||
types: [created]
|
||||
# pull_request_target:
|
||||
# types: [opened, edited]
|
||||
# pull_request_review_comment:
|
||||
# types: [created, edited]
|
||||
|
||||
jobs:
|
||||
en:
|
||||
if: github.repository == 'devlikeapro/waha'
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: devlikeapro/github-translate-action@main
|
||||
env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
IS_MODIFY_TITLE: true
|
||||
CUSTOM_BOT_NOTE: 'English'
|
||||
TARGET_LANGUAGE: 'eng'
|
||||
TRANSLATED_LANGUAGE: 'en'
|
||||
# pt:
|
||||
# permissions:
|
||||
# issues: write
|
||||
# discussions: write
|
||||
# pull-requests: write
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: devlikeapro/github-translate-action@main
|
||||
# env:
|
||||
# # GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# IS_MODIFY_TITLE: false
|
||||
# CUSTOM_BOT_NOTE: 'Portuguese'
|
||||
# TARGET_LANGUAGE: 'por'
|
||||
# TRANSLATED_LANGUAGE: 'pt'
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Manual push
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to push'
|
||||
required: true
|
||||
default: 'dev'
|
||||
push:
|
||||
description: 'Push to registry'
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name: ${{ matrix.platform }} - ${{ matrix.browser }} - ${{ matrix.tag }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Chromium
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
tag: 'latest'
|
||||
platform: 'amd64'
|
||||
browser: 'chromium'
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
tag: 'arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'chromium'
|
||||
# Chrome
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
tag: 'chrome'
|
||||
platform: 'amd64'
|
||||
browser: 'chrome'
|
||||
# Chrome is not supported on arm64
|
||||
# - runner: "buildjet-4vcpu-ubuntu-2204-arm"
|
||||
# tag: "chrome-arm"
|
||||
# platform: "linux/arm64"
|
||||
# browser: "chrome"
|
||||
# No browser
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
tag: 'noweb'
|
||||
platform: 'amd64'
|
||||
browser: 'none'
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
tag: 'noweb-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'none'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ vars.DOCKER_IMAGE }}
|
||||
flavor: latest=false
|
||||
tags: |
|
||||
type=raw,value=${{ inputs.tag }}-${{ matrix.tag }}
|
||||
|
||||
- name: Login to image repository
|
||||
if: github.ref_type == 'tag'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
push: ${{ inputs.push }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,6 +1,12 @@
|
||||
tmp/*
|
||||
src/core/engines/gows/proto
|
||||
.env
|
||||
.envs/*
|
||||
*.heapsnapshot
|
||||
.yarn
|
||||
example.ts
|
||||
.*sessions
|
||||
sessions
|
||||
.secrets
|
||||
.minio
|
||||
.media
|
||||
@@ -14,6 +20,7 @@ test.js
|
||||
.wwebjs_cache
|
||||
src/dashboard
|
||||
.eslintcache
|
||||
__pycache__
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"env": {
|
||||
"builtin": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
+15
-12
@@ -2,7 +2,10 @@ exclude: |
|
||||
(?x)^(
|
||||
^src/dashboard/.*|
|
||||
^src/core/engines/webjs/.*html|
|
||||
^src/plus/engines/webjs/.*html
|
||||
^src/core/engines/gows/grpc/.*|
|
||||
^src/core/engines/gows/proto/.*|
|
||||
^src/plus/engines/webjs/.*html|
|
||||
^.yarn/.*
|
||||
)$
|
||||
|
||||
repos:
|
||||
@@ -14,18 +17,12 @@ repos:
|
||||
language: python
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: lint
|
||||
stages: [pre-commit]
|
||||
name: lint
|
||||
- id: oxlint
|
||||
name: oxlint
|
||||
entry: npx oxlint --fix --deny-warnings
|
||||
language: system
|
||||
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint-cache'
|
||||
pass_filenames: false
|
||||
files: |
|
||||
(?x)^(
|
||||
src/.*|
|
||||
)$
|
||||
pass_filenames: true
|
||||
files: \.(ts|tsx|js|jsx)$
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: 'v3.1.0'
|
||||
@@ -36,6 +33,7 @@ repos:
|
||||
(?x)^(
|
||||
docs/.*|
|
||||
README.md|
|
||||
tests/smoke/goss.yaml|
|
||||
)$
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
|
||||
rev: v1.1.2
|
||||
@@ -69,6 +67,11 @@ repos:
|
||||
exclude: |
|
||||
(?x)^(
|
||||
entrypoint.sh|
|
||||
AGENTS.md|
|
||||
.pre-commit-config.yaml|
|
||||
^src/core/engines/webjs/session.webjs.core.ts|
|
||||
^src/core/engines/webjs/_lodash.js|
|
||||
^tests/perf/.*|
|
||||
^scripts/.*|
|
||||
^scripts/init-waha.js|
|
||||
)$
|
||||
+4
-2
@@ -1,3 +1,5 @@
|
||||
nodeLinker: node-modules
|
||||
compressionLevel: mixed
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.6.3.cjs
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
@@ -0,0 +1,210 @@
|
||||
# WAHA Agent Playbook
|
||||
|
||||
This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
||||
(WAHA) codebase when assisting as an automation or coding agent.
|
||||
|
||||
- When you asked to refactor or "apply new lib" instead of current one - do not
|
||||
change the behavior, make a minimum amount of changes possible. If you see
|
||||
some edge case during that process that haven't been covered - tell it,
|
||||
suggest fix, but don't change the code.
|
||||
|
||||
## Product & Variants
|
||||
|
||||
- WAHA ships in **Core** and **Plus** editions. Core lives under `src/core` and
|
||||
only supports the default session plus minimal media features. Plus extends
|
||||
core via `src/plus` to add multi-session orchestration, richer media handling,
|
||||
and external storage integrations.
|
||||
- Core code must remain free from Plus-only references. A pre-commit hook
|
||||
rejects the word `plus` inside core files; reuse abstractions exposed through
|
||||
`@waha/core/**` instead of importing Plus modules from Core.
|
||||
- Commit subjects are validated: changes that touch `src/plus` require a
|
||||
`[PLUS] …` prefix and must not include non-Plus files; everything else must
|
||||
use `[core] …`. Verify against `./.precommit/validate_commit_message.py` if
|
||||
unsure.
|
||||
|
||||
## Tech Stack Snapshot
|
||||
|
||||
- **Runtime**: Node.js 22.x, Yarn 3.6 (Berry). Always install packages and run
|
||||
scripts with Yarn.
|
||||
- **Framework**: NestJS v11 with dependency injection, modular controllers in
|
||||
`src/api`, and Pino-based logging via `nestjs-pino`.
|
||||
- **Engines**: WhatsApp engines are abstracted (`WEBJS`, `GOWS`, `NOWEB`). Core
|
||||
uses `SessionManagerCore`; Plus swaps to `SessionManagerPlus` with extra
|
||||
storage backends (Mongo/Postgres/SQLite).
|
||||
- **ESM Bridge**: ESM-only dependencies (Baileys) load through
|
||||
`src/vendor/esm.ts`. Add new ESM modules there to ensure they load exactly
|
||||
once.
|
||||
- **Utilities**: RxJS streams (`SwitchObservable`, `DefaultMap`) drive webhook
|
||||
event fan-out. Prefer existing helpers in `src/utils` and `src/core/utils`
|
||||
before adding bespoke logic.
|
||||
|
||||
## Repository Landmarks
|
||||
|
||||
- `src/main.ts`: runtime entry point; dynamically loads the correct AppModule
|
||||
(Core vs Plus) and configures global interceptors/filters.
|
||||
- `src/api/**`: REST controllers and WebSocket gateway. Keep handlers thin;
|
||||
delegate to managers/services. HTTP routes follow `/api/{sessionName}/…`;
|
||||
always thread the session name through request DTOs and guards instead of
|
||||
hardcoding `default`.
|
||||
- `src/core/**`: shared abstractions (config services, engine bootstrap,
|
||||
storage, session management). Core only allows the `default` session and
|
||||
cleans up storage on boot.
|
||||
- `src/plus/**`: multi-session orchestration, advanced media services, and
|
||||
external persistence layers (Mongo, Postgres). Reuse this layer when adding
|
||||
Plus-only capabilities.
|
||||
- `src/apps/**`: integrations (e.g., ChatWoot) and application-specific
|
||||
services.
|
||||
- `src/structures/**` and `src/utils/**`: DTOs, enums (event names follow
|
||||
`domain.action`), helper utilities. Maintain naming consistency when
|
||||
introducing new events.
|
||||
- `tests/**`: Jest-based suites; do not add new tests unless explicitly asked,
|
||||
but keep existing tests working.
|
||||
|
||||
## Coding Expectations
|
||||
|
||||
- Favor composable, long-lived solutions. If a helper already exists (e.g.,
|
||||
`parseBool`, `DefaultMap`, media factories), extend it instead of reinventing
|
||||
logic. Lodash ships with the project—prefer its utilities over hand-rolled
|
||||
helpers. Import lodash as a namespace (`import * as lodash from 'lodash';`) so
|
||||
helpers like `lodash.camelCase` stay consistent across files. When a new
|
||||
third-party library seems necessary, confirm with the user, especially if the
|
||||
package looks stale.
|
||||
- Stick to NestJS patterns: inject dependencies through constructors, expose
|
||||
provider tokens from modules, and keep controllers free from business logic.
|
||||
- Logging goes through injected `PinoLogger` or helpers in
|
||||
`src/utils/logging.ts`. `console.log` is blocked by pre-commit.
|
||||
- Respect path aliases (`@waha/...`) defined in `tsconfig.json`; keep imports
|
||||
consistent (use absolute aliases, not relative `../../../`).
|
||||
- Prefer named function declarations over `const` arrow functions when possible.
|
||||
- Avoid naming unused variables with a leading underscore; if a parameter is
|
||||
required by a signature, explicitly `void` it instead.
|
||||
- Do not write verbose ternaries like
|
||||
`condition !== undefined ? condition : default`; use idiomatic helpers such as
|
||||
`??` (nullish coalescing) or existing boolean parsers so flags stay readable.
|
||||
- Do not place `await` or other async calls inside ternary expressions (`?:`);
|
||||
use explicit `if/else` blocks instead.
|
||||
- For configs, prefer runtime configurability over constants. Environment keys
|
||||
follow `WAHA_*` for global values and `WAHA_SESSION_CONFIG_*` /
|
||||
`session.config.*` for per-session overrides. If both env and config are
|
||||
supported, honor both (`WAHA_WEBJS_CONFIG_*` vs. `session.webjs.config.*`).
|
||||
|
||||
## Language & Localization
|
||||
|
||||
- Keep identifiers (classes, methods, variables) and code comments in English so
|
||||
the codebase stays consistent for the global team.
|
||||
- Route user-visible strings through the existing i18n structure rather than
|
||||
hardcoding text. ChatWoot copy belongs in `src/apps/chatwoot/i18n` with
|
||||
English as the source locale before adding translations.
|
||||
|
||||
## ChatWoot Integration Notes
|
||||
|
||||
- Avoid introducing synthetic events; map onto existing webhook or engine events
|
||||
whenever plausible. Always use the official ChatWoot API client located in
|
||||
`src/apps/chatwoot/client`.
|
||||
- When adding events, align consumer names with webhook/event identifiers (e.g.,
|
||||
`message.any`).
|
||||
|
||||
## Workflow Checklist
|
||||
|
||||
1. Identify whether work targets Core, Plus, or shared layers. If Plus-only,
|
||||
isolate changes to `src/plus` and ensure the commit prefix matches.
|
||||
2. Lean on existing services/managers; extend the appropriate session manager
|
||||
rather than branching logic inline.
|
||||
3. After edits run:
|
||||
- Do **not** run `pre-commit run --all-files` unless the user explicitly
|
||||
asks.
|
||||
- `yarn build`
|
||||
- `yarn test --watchman=false`
|
||||
4. Do **not** start the application yourself; ask the user to run it if runtime
|
||||
validation is required.
|
||||
5. Capture any assumptions or open questions for the user, especially when
|
||||
touching configs, introducing dependencies, or modifying public APIs.
|
||||
|
||||
## Additional Tips
|
||||
|
||||
- Concurrency-sensitive sections (session start/stop) rely on `async-lock`. When
|
||||
adding async flows, reuse `SessionManager.withLock` or existing retry
|
||||
utilities (`promiseTimeout`, `waitUntil`).
|
||||
- Media features centralize through `MediaManager` and `MediaStorageFactory`.
|
||||
When altering media behavior, update both Core and Plus variants where
|
||||
applicable.
|
||||
- Keep docs and code ASCII unless a file already uses other characters. When
|
||||
updating documentation, mirror the concise, actionable tone used here.
|
||||
|
||||
## Yes No
|
||||
|
||||
Always define key for objects (and in return too)
|
||||
|
||||
```js
|
||||
// NO
|
||||
const variable = 123;
|
||||
const b = { variable };
|
||||
|
||||
// YES
|
||||
const variable = 123;
|
||||
const b = { variable: variable };
|
||||
```
|
||||
|
||||
Use the three-line comment style for section headers inside files:
|
||||
|
||||
```ts
|
||||
// NO
|
||||
// ─── Section name ─────────────────────────────────────────────────────────────
|
||||
|
||||
// YES
|
||||
//
|
||||
// Section name
|
||||
//
|
||||
```
|
||||
|
||||
## Related Sources Code
|
||||
|
||||
You can find and read related source code in the following paths:
|
||||
|
||||
- WEBJS: `../whatsapp-web.js`
|
||||
- NOWEB: `../WhiskeySockets-Baileys`
|
||||
- whatsapp-rust-bridge - `../whatsapp-rust-bridge`
|
||||
- GOWS: `../gows`
|
||||
- whatsmeow - `../whatsmeow`
|
||||
- WPP: `../wa-js`, `../wppconnect`, `../wppconnect-server`
|
||||
- ChatWoot: `../chatwoot`
|
||||
|
||||
Following this playbook keeps contributions aligned with WAHA’s structure,
|
||||
automation hooks, and release process.
|
||||
|
||||
## How to run API
|
||||
|
||||
You can run the project outside of sandbox using the below command, then run
|
||||
queries against `default` session (if not asked to do something different) using
|
||||
`curl` and `X-Api-Key: 666` header.
|
||||
|
||||
```bash
|
||||
export DEBUG=1
|
||||
export WAHA_API_KEY=666
|
||||
export WAHA_DASHBOARD_PASSWORD=666
|
||||
export WAHA_DASHBOARD_USERNAME=admin
|
||||
export WWHATSAPP_SWAGGER_USERNAME=admin
|
||||
export WHATSAPP_SWAGGER_PASSWORD=666
|
||||
export WHATSAPP_DEFAULT_ENGINE={WEBJS|WPP|NOWEB|GOWS}
|
||||
export WAHA_DEBUG_MODE=True
|
||||
export WAHA_HTTP_STRICT_MODE=1
|
||||
export WAHA_MEDIA_STORAGE=LOCAL
|
||||
export WHATSAPP_FILES_FOLDER=./.media
|
||||
|
||||
npm run start
|
||||
```
|
||||
|
||||
- Ask user before running the server.
|
||||
- Before executing some queries make sure the session is in `WORKING` status.
|
||||
- If it's `FAILED` or `SCAN_QR_CODE` ask user to scan QR code or fix failed
|
||||
session.
|
||||
|
||||
## Code
|
||||
|
||||
### @Activity Decorator and Presence Tracking
|
||||
|
||||
Add `@Activity()` (from `src/core/abc/activity.ts`) to every engine method that
|
||||
makes a network call to WhatsApp servers. It triggers `maintainPresenceOnline()`
|
||||
before the method runs, keeping the session ONLINE during API activity and
|
||||
scheduling an OFFLINE transition after an idle period. Skip it on methods that
|
||||
only throw `NotImplementedByEngineError` / `AvailableInPlusVersion`.
|
||||
+158
-17
@@ -1,64 +1,157 @@
|
||||
ARG NODE_IMAGE_TAG=24.11-bookworm-slim
|
||||
ARG GOLANG_IMAGE_TAG=1.24-bookworm
|
||||
|
||||
#
|
||||
# Build
|
||||
#
|
||||
ARG NODE_VERSION=20.12.2-bullseye
|
||||
FROM node:${NODE_VERSION} as build
|
||||
FROM node:${NODE_IMAGE_TAG} AS build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
|
||||
# git + build toolchain for git deps
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends git python3 build-essential curl ca-certificates unzip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# npm packages
|
||||
WORKDIR /src
|
||||
WORKDIR /git
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
RUN yarn set version 3.6.3
|
||||
ENV YARN_CHECKSUM_BEHAVIOR=update
|
||||
|
||||
RUN npm install -g corepack && corepack enable
|
||||
RUN yarn set version 4.9.2
|
||||
RUN yarn install
|
||||
|
||||
# App
|
||||
WORKDIR /src
|
||||
ADD . /src
|
||||
WORKDIR /git
|
||||
ADD . /git
|
||||
RUN yarn install
|
||||
RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
FROM node:${NODE_VERSION} as dashboard
|
||||
FROM node:${NODE_IMAGE_TAG} AS dashboard
|
||||
|
||||
# Download WAHA Dashboard
|
||||
ENV WAHA_DASHBOARD_SHA c1a91f879a35f0ae32d46ddb2e061ac1819a3d4f
|
||||
# jq to parse json
|
||||
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# wget, unzip
|
||||
RUN apt-get update && apt-get install -y wget unzip && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY waha.config.json /tmp/waha.config.json
|
||||
RUN \
|
||||
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
|
||||
WAHA_DASHBOARD_GITHUB_REPO=$(jq -r '.waha.dashboard.repo' /tmp/waha.config.json) && \
|
||||
WAHA_DASHBOARD_SHA=$(jq -r '.waha.dashboard.ref' /tmp/waha.config.json) && \
|
||||
wget https://github.com/${WAHA_DASHBOARD_GITHUB_REPO}/archive/${WAHA_DASHBOARD_SHA}.zip \
|
||||
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
|
||||
&& mkdir -p /dashboard \
|
||||
&& mv /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}/* /dashboard/ \
|
||||
&& rm -rf ${WAHA_DASHBOARD_SHA}.zip \
|
||||
&& rm -rf /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}
|
||||
|
||||
#
|
||||
# GOWS
|
||||
#
|
||||
FROM golang:${GOLANG_IMAGE_TAG} AS gows
|
||||
|
||||
# jq to parse json
|
||||
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# install protoc
|
||||
RUN apt-get update && \
|
||||
apt-get install protobuf-compiler -y
|
||||
|
||||
# Image processing for thumbnails
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libvips-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY waha.config.json /tmp/waha.config.json
|
||||
WORKDIR /go/gows
|
||||
RUN \
|
||||
GOWS_GITHUB_REPO=$(jq -r '.waha.gows.repo' /tmp/waha.config.json) && \
|
||||
GOWS_SHA=$(jq -r '.waha.gows.ref' /tmp/waha.config.json) && \
|
||||
ARCH=$(uname -m) && \
|
||||
if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; \
|
||||
elif [ "$ARCH" = "aarch64" ]; then ARCH="arm64"; \
|
||||
else echo "Unsupported architecture: $ARCH" && exit 1; fi && \
|
||||
mkdir -p /go/gows/bin && \
|
||||
wget -O /go/gows/bin/gows https://github.com/${GOWS_GITHUB_REPO}/releases/download/${GOWS_SHA}/gows-${ARCH} && \
|
||||
chmod +x /go/gows/bin/gows
|
||||
|
||||
|
||||
#
|
||||
# Final
|
||||
#
|
||||
FROM node:${NODE_VERSION} as release
|
||||
FROM node:${NODE_IMAGE_TAG} AS release
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
# Quick fix for memory potential memory leaks
|
||||
# https://github.com/devlikeapro/waha/issues/347
|
||||
ENV NODE_OPTIONS="--max-old-space-size=16384"
|
||||
ARG USE_BROWSER=chromium
|
||||
ARG WHATSAPP_DEFAULT_ENGINE
|
||||
|
||||
RUN echo "USE_BROWSER=$USE_BROWSER"
|
||||
|
||||
# Install ffmpeg to generate previews for videos
|
||||
RUN apt-get update && apt-get install -y ffmpeg --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install zip and unzip
|
||||
RUN apt-get update && apt-get install -y zip unzip --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
||||
# Image processing for thumbnails
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libvips \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install zip and unzip - either for chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
&& apt-get install -y zip unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install wget - either for chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
&& apt-get install -y wget \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install fonts if using either chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
&& apt-get install -y fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf \
|
||||
&& apt-get install -y \
|
||||
fontconfig \
|
||||
fonts-freefont-ttf \
|
||||
fonts-gfs-neohellenic \
|
||||
fonts-indic \
|
||||
fonts-ipafont-gothic \
|
||||
fonts-kacst \
|
||||
fonts-liberation \
|
||||
fonts-noto-cjk \
|
||||
fonts-noto-color-emoji \
|
||||
fonts-roboto \
|
||||
fonts-thai-tlwg \
|
||||
fonts-wqy-zenhei \
|
||||
fonts-open-sans \
|
||||
--no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install xvfb, xauth
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
xvfb \
|
||||
xauth \
|
||||
libnss3 \
|
||||
libxss1 \
|
||||
libasound2 \
|
||||
libatk-bridge2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libdrm2 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install Chromium
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
apt-get update \
|
||||
@@ -71,7 +164,8 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="126.0.6478.182-1"
|
||||
ARG CHROME_VERSION="140.0.7339.80-1"
|
||||
ARG OPUSTAGS_VERSION="1.10.1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
&& apt-get update \
|
||||
@@ -80,12 +174,54 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# curl
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build and install opustags so audio metadata can be cleaned up inside the container
|
||||
RUN set -eux; \
|
||||
buildDeps='build-essential cmake pkg-config libogg-dev'; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends ${buildDeps}; \
|
||||
mkdir -p /tmp/opustags; \
|
||||
curl -L https://github.com/fmang/opustags/archive/refs/tags/${OPUSTAGS_VERSION}.tar.gz \
|
||||
| tar -xz -C /tmp/opustags; \
|
||||
cd /tmp/opustags/opustags-${OPUSTAGS_VERSION}; \
|
||||
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release; \
|
||||
cmake --build build --config Release; \
|
||||
cmake --install build; \
|
||||
rm -rf /tmp/opustags; \
|
||||
apt-get purge -y --auto-remove ${buildDeps}; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# GOWS requirements
|
||||
# libc6
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libc6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install tini for proper init process
|
||||
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the ENV for docker image
|
||||
ENV WHATSAPP_DEFAULT_ENGINE=$WHATSAPP_DEFAULT_ENGINE
|
||||
|
||||
# Attach sources, install packages
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY --from=build /src/node_modules ./node_modules
|
||||
COPY --from=build /src/dist ./dist
|
||||
COPY --from=build /git/node_modules ./node_modules
|
||||
COPY --from=build /git/dist ./dist
|
||||
COPY --from=dashboard /dashboard ./dist/dashboard
|
||||
COPY --from=gows /go/gows/bin/gows /app/gows
|
||||
COPY .env.example ./.env.example
|
||||
COPY scripts/init-waha.js ./scripts/init-waha.js
|
||||
RUN chmod +x ./scripts/init-waha.js \
|
||||
&& printf '%s\n' '#!/bin/sh' 'exec node /app/scripts/init-waha.js "$@"' > /usr/local/bin/init-waha \
|
||||
&& chmod +x /usr/local/bin/init-waha
|
||||
ENV WAHA_GOWS_PATH=/app/gows
|
||||
ENV WAHA_GOWS_SOCKET=/tmp/gows.sock
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Chokidar options to monitor file changes
|
||||
@@ -95,6 +231,11 @@ ENV CHOKIDAR_INTERVAL=5000
|
||||
# WAHA variables
|
||||
ENV WAHA_ZIPPER=ZIPUNZIP
|
||||
|
||||
# GOWS - use libc DNS resolver
|
||||
ENV GODEBUG netdns=cgo
|
||||
|
||||
# Run command, etc
|
||||
EXPOSE 3000
|
||||
# Use tini as init system to handle zombie processes properly
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -1,16 +1,19 @@
|
||||
build-all: build-plus build-chrome build-gows build-noweb
|
||||
|
||||
build:
|
||||
docker build . -t devlikeapro/waha
|
||||
|
||||
build-plus:
|
||||
docker build . -t devlikeapro/waha-plus
|
||||
|
||||
build-chrome:
|
||||
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
|
||||
|
||||
build-noweb:
|
||||
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none
|
||||
docker build . -t devlikeapro/waha-plus:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
build-all: build build-chrome build-noweb
|
||||
|
||||
build-plus:
|
||||
docker build . -t devlikeapro/waha-plus
|
||||
build-gows:
|
||||
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
|
||||
|
||||
build-ssh:
|
||||
# check IMAGE provided
|
||||
@@ -29,13 +32,34 @@ push:
|
||||
docker push devlikeapro/waha
|
||||
|
||||
for-swagger:
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
|
||||
export WHATSAPP_SWAGGER_CONFIG_ADVANCED=true && export WHATSAPP_SWAGGER_PASSWORD=666 && yarn start
|
||||
|
||||
up-noweb:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-2
|
||||
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2026-02-11
|
||||
|
||||
up-noweb-libsignal:
|
||||
yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
|
||||
|
||||
up-webjs:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels
|
||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18
|
||||
|
||||
up-wpp:
|
||||
yarn up @wppconnect-team/wppconnect
|
||||
|
||||
up-rust-bridge:
|
||||
yarn up -R whatsapp-rust-bridge
|
||||
|
||||
start-proxy:
|
||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||
|
||||
proto-gows:
|
||||
node scripts/gows-proto.js build --dir ../gows/proto
|
||||
|
||||
gows:
|
||||
cd ../gows && \
|
||||
export PATH=${HOME}/go/bin:${PATH} && \
|
||||
make all
|
||||
|
||||
copy-dashboard:
|
||||
cd ../waha-hub/ui && \
|
||||
make copy-waha
|
||||
@@ -6,22 +6,16 @@
|
||||
|
||||
**WAHA** - **W**hats**A**pp **H**TTP **A**PI (REST API) that you can install on your own server and run in less than 5 minutes!
|
||||
|
||||
[](https://hub.docker.com/r/devlikeapro/waha)
|
||||
[](https://hub.docker.com/r/devlikeapro/waha)
|
||||
|
||||
- Documentation: [https://waha.devlike.pro/](https://waha.devlike.pro/)
|
||||
- Dashboard Example: [https://waha.devlike.pro/dashboard](https://waha.devlike.pro/dashboard)
|
||||
- Swagger Example: [https://waha.devlike.pro/swagger](https://waha.devlike.pro/swagger)
|
||||
|
||||
# Tables of Contents
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Features](#features)
|
||||
* [Messages](#messages)
|
||||
* [Sessions](#sessions)
|
||||
* [Security](#security)
|
||||
* [Updates](#updates)
|
||||
* [Groups](#groups)
|
||||
* [Contacts](#contacts)
|
||||
- [Quick start](#quick-start)
|
||||
* [Requirements](#requirements)
|
||||
* [Send your first message](#send-your-first-message)
|
||||
@@ -32,52 +26,11 @@
|
||||
+ [5. Get the screenshot](#5-get-the-screenshot)
|
||||
+ [6. Send a text message](#6-send-a-text-message)
|
||||
* [What is next?](#what-is-next)
|
||||
- [Development](#development)
|
||||
* [Start the project](#start-the-project)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
# Features
|
||||
|
||||
## Messages
|
||||
- Send messages
|
||||
- Receive messages
|
||||
- Message replies
|
||||
- Send location
|
||||
- Receive location
|
||||
- React to messages
|
||||
- Send media (images/documents/files)
|
||||
- Send voice messages
|
||||
- Receive media (images/audio/video/documents)
|
||||
- Receive messages - webhook retries
|
||||
|
||||
## Sessions
|
||||
- Multi Device
|
||||
- Get the screenshot
|
||||
- Session saving (don't have to scan QR on every restart)
|
||||
- Single WhatsApp account running inside one container
|
||||
- Multiple WhatsApp account running inside one container
|
||||
|
||||
## Security
|
||||
- Swagger panel authentication
|
||||
- API authentication
|
||||
|
||||
## Updates
|
||||
- Bug fixes and updates come as soon as they are implemented.
|
||||
|
||||
## Groups
|
||||
- Create a group
|
||||
- Get invite for group
|
||||
- Modify group info (subject, description)
|
||||
- Add group participants
|
||||
- Kick group participants
|
||||
- Promote/demote group participants
|
||||
|
||||
## Contacts
|
||||
- Mute/unmute chats
|
||||
- Block/unblock contacts
|
||||
- Get contact info
|
||||
- Get profile pictures
|
||||
- Get presence (online\offline\typing) status
|
||||
|
||||
# Quick start
|
||||
|
||||
## Requirements
|
||||
@@ -87,13 +40,6 @@ instruction <a href="https://docs.docker.com/get-docker/" target="_blank" rel="n
|
||||
|
||||
When you are ready - come back and follows the below steps to send the first text message to WhatsApp via HTTP API!
|
||||
|
||||
{{< details "Why Docker?" >}}
|
||||
Docker makes it easy to ship all-in-one solution with the runtime and dependencies. You don't have to worry about
|
||||
language-specific libraries or chrome installation.
|
||||
|
||||
Also Docker makes installation and update processes so simple, just one command!
|
||||
{{< /details >}}
|
||||
|
||||
## Send your first message
|
||||
|
||||
Let's go over steps that allow you to send your first text message via WhatsApp HTTP API!
|
||||
@@ -114,7 +60,7 @@ docker pull devlikeapro/waha-plus
|
||||
docker logout
|
||||
```
|
||||
|
||||
Read more about how to get `PASSWORD` for [Plus Version →]({{< relref "plus-version" >}})
|
||||
Read more about how to get `PASSWORD` for [**➕ WAHA Plus**](https://waha.devlike.pro/docs/how-to/waha-plus/)
|
||||
|
||||
### 2. Run WhatsApp HTTP API
|
||||
|
||||
@@ -195,3 +141,30 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
|
||||
|
||||
## What is next?
|
||||
[Go and read the full documentation!](https://waha.devlike.pro/docs/overview/introduction/)
|
||||
|
||||
# Development
|
||||
|
||||
## Start the project
|
||||
1. Clone the repository
|
||||
2. Make sure you're using node>=22 (check [.nvmrc](/.nvmrc) to get the version)
|
||||
3. Install the **whatsapp-rust-bridge prerequisites**:
|
||||
```bash
|
||||
# Bun runtime used by whatsapp-rust-bridge prepare scripts
|
||||
curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.9
|
||||
|
||||
# Rust nightly toolchain used for whatsapp-rust-bridge
|
||||
curl -fsSL https://sh.rustup.rs | bash -s -- -y --default-toolchain nightly-2026-01-30
|
||||
rustup target add wasm32-unknown-unknown
|
||||
cargo install wasm-pack --vers 0.14.0 --locked
|
||||
```
|
||||
|
||||
4. Run the following commands:
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
# Fetch and compile proto files
|
||||
yarn gows:proto
|
||||
# Run
|
||||
yarn start
|
||||
# open http://localhost:3000
|
||||
```
|
||||
+84
-223
@@ -1,243 +1,104 @@
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/install/
|
||||
# Here's we provide a simple example of how to install WAHA using Docker Compose.
|
||||
#
|
||||
# We're assuming you have Docker and Docker Compose installed.
|
||||
# If you don't have Docker Compose installed, you can follow the official guide:
|
||||
# https://docs.docker.com/compose/install/
|
||||
#
|
||||
|
||||
services:
|
||||
waha:
|
||||
# Always restart the container on failure
|
||||
restart: always
|
||||
|
||||
#
|
||||
# Image
|
||||
#
|
||||
image: devlikeapro/waha-plus:latest # or devlikeapro/waha:latest if you're using WAHA Core
|
||||
#
|
||||
# We strongly recommend using the pinned version of WAHA
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#versions
|
||||
# image: devlikeapro/waha-plus:latest-2024.7.6 # 'latest' is not mistaken here
|
||||
#
|
||||
# Multiple images are available
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
# image: devlikeapro/waha-plus:chrome
|
||||
#
|
||||
# https://portal.devlike.pro/docker-image
|
||||
image: devlikeapro/waha-plus
|
||||
# WAHA Core
|
||||
# image: devlikeapro/waha:latest
|
||||
|
||||
# Add "dns" if you have a problem with resolving "web.whatsapp.com"
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '100m'
|
||||
max-file: '10'
|
||||
|
||||
# The first port can be any free port you want
|
||||
# The second port must be the same as WHATSAPP_API_PORT or 3000 by default
|
||||
# Consider using the same port for simplicity
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
# NOTE: if you're using HTTP, you can use the following port mapping
|
||||
#- '80:3000/tcp'
|
||||
# NOTE: if you're using HTTPS, you can use the following port mapping
|
||||
#- '443:3000/tcp'
|
||||
- '127.0.0.1:3000:3000/tcp'
|
||||
|
||||
volumes:
|
||||
# Store sessions in the .sessions folder (comment it if you're using MongoDB)
|
||||
# Doesn't work with WAHA Core
|
||||
- './.sessions:/app/.sessions'
|
||||
- './sessions:/app/.sessions'
|
||||
|
||||
# Save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
- './.media:/app/.media'
|
||||
- './media:/app/.media'
|
||||
|
||||
# If you're using certbot for HTTPS, you can use the following volume mapping
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
# - '/etc/letsencrypt:/etc/letsencrypt'
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# WAHA Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
environment:
|
||||
#
|
||||
# Common
|
||||
#
|
||||
# If you know the right URL, you can use it here.
|
||||
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
|
||||
# https://waha.devlike.pro/docs/how-to/config/#common
|
||||
#- WHATSAPP_API_SCHEMA=http
|
||||
#- WHATSAPP_API_PORT=3000
|
||||
#- WHATSAPP_API_HOSTNAME=localhost
|
||||
# NOTE: Only if you're using PostgreSQL to save sessions
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
|
||||
# postgres:
|
||||
# image: postgres:17.6
|
||||
# restart: always
|
||||
# environment:
|
||||
# POSTGRES_USER: postgres
|
||||
# POSTGRES_PASSWORD: postgres
|
||||
# POSTGRES_DB: postgres
|
||||
# ports:
|
||||
# - "127.0.0.1:5432:5432"
|
||||
# volumes:
|
||||
# - pg_data:/var/lib/postgresql/data
|
||||
# command:
|
||||
# - postgres
|
||||
# - "-c"
|
||||
# - "max_connections=3000"
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
#
|
||||
# Security
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/security/
|
||||
#
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
#- WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# NOTE: Only if you're using MongoDB
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
# Uncomment this block if you're using MongoDB
|
||||
# mongodb:
|
||||
# image: mongo
|
||||
# container_name: mongodb
|
||||
# ports:
|
||||
# - '127.0.0.1:27017:27017/tcp'
|
||||
# volumes:
|
||||
# - mongodb_data:/data/db
|
||||
# environment:
|
||||
# - MONGO_INITDB_ROOT_USERNAME=mongouser
|
||||
# - MONGO_INITDB_ROOT_PASSWORD=mongopassword
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
|
||||
# Enabled by default
|
||||
#- WAHA_DASHBOARD_ENABLED=true
|
||||
#- WAHA_DASHBOARD_USERNAME=waha
|
||||
#- WAHA_DASHBOARD_PASSWORD=waha
|
||||
|
||||
#
|
||||
# Logging
|
||||
# https://waha.devlike.pro/docs/how-to/observability/#logging
|
||||
#
|
||||
- WAHA_LOG_FORMAT=PRETTY
|
||||
# Set log format to JSON if you consume logs with log management systems
|
||||
#- WAHA_LOG_FORMAT=JSON
|
||||
- WAHA_LOG_LEVEL=info
|
||||
# If you want to see more logs, you can set the log level to debug
|
||||
#- WAHA_LOG_LEVEL=debug
|
||||
# DEBUG=1 # shortcut for setting log level to debug
|
||||
|
||||
#
|
||||
# Engine
|
||||
#
|
||||
# Choose the right engine for your needs (WEBJS by default)
|
||||
# https://waha.devlike.pro/docs/how-to/engines/
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
# Use NOWEB engine for the fastest performance
|
||||
#- WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
#
|
||||
# Sessions
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#sessions
|
||||
# Restart all sessions when container starts
|
||||
- WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# Do not print QR codes in logs
|
||||
- WAHA_PRINT_QR=False
|
||||
#
|
||||
# OR you can specify sessions by name, if you have handful amount of sessions
|
||||
#- WHATSAPP_START_SESSION=session1,session2
|
||||
# Do not use both WHATSAPP_RESTART_ALL_SESSIONS and WHATSAPP_START_SESSION parameters
|
||||
#
|
||||
|
||||
#
|
||||
# Swagger
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/
|
||||
#- WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Disable Swagger
|
||||
#- WHATSAPP_SWAGGER_ENABLED=false
|
||||
# Enable advanced Swagger configuration
|
||||
#- WHATSAPP_SWAGGER_CONFIG_ADVANCED=true
|
||||
#
|
||||
# Swagger White Label customization
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/#white-label
|
||||
#- WHATSAPP_SWAGGER_TITLE=WAHA API
|
||||
#
|
||||
|
||||
#
|
||||
# Global Proxy Settings
|
||||
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
|
||||
# You can also set proxy settings for each session later when starting it
|
||||
# https://waha.devlike.pro/docs/how-to/sessions/#configure-proxy
|
||||
#- WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
#-WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
#-WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
#
|
||||
|
||||
#
|
||||
# HTTPS Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#https
|
||||
# Consider using certbot for HTTPS
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
#- WAHA_HTTPS_ENABLED=true
|
||||
#- WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
#- WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
|
||||
#- WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
#- WHATSAPP_API_SCHEMA=https
|
||||
#- WHATSAPP_API_PORT=3000
|
||||
#- WHATSAPP_API_HOSTNAME=waha.example.pro
|
||||
#
|
||||
|
||||
#
|
||||
# Media Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#files
|
||||
#
|
||||
# Disable media (images, videos, files) download for incoming messages
|
||||
#- WHATSAPP_DOWNLOAD_MEDIA=false
|
||||
#
|
||||
# Download only specific media types
|
||||
#- WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
#
|
||||
|
||||
# Media - Local Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---local
|
||||
#
|
||||
# Set to 0 to keep media files forever
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
- WAHA_MEDIA_STORAGE=LOCAL
|
||||
- WHATSAPP_FILES_LIFETIME=0
|
||||
- WHATSAPP_FILES_FOLDER=/app/.media
|
||||
# Keep media files for 180 seconds (3 minutes)
|
||||
#- WHATSAPP_FILES_LIFETIME=180
|
||||
#
|
||||
|
||||
# Media - S3 Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
#- WAHA_MEDIA_STORAGE=S3
|
||||
#- WAHA_S3_REGION=eu-west-2
|
||||
#- WAHA_S3_BUCKET=waha
|
||||
#- WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
#- WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
#- WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
#- WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
#- WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
|
||||
#
|
||||
# Global Webhooks
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#webhooks
|
||||
# https://waha.devlike.pro/docs/how-to/webhooks/
|
||||
#- WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
#- WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
#
|
||||
|
||||
#
|
||||
# MongoDB Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
#- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
#
|
||||
|
||||
# NOTE: Only if you're using MongoDB
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
# Comment this block if you're using MongoDB
|
||||
mongodb:
|
||||
image: mongo
|
||||
container_name: mongodb
|
||||
ports:
|
||||
- '27017:27017/tcp'
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
restart: always
|
||||
|
||||
# NOTE: Only if you're using S3 to save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
# Comment this block if you're using AWS S3
|
||||
minio:
|
||||
image: quay.io/minio/minio
|
||||
container_name: minio
|
||||
restart: always
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
environment:
|
||||
MINIO_REGION: 'eu-west-2'
|
||||
MINIO_ROOT_USER: 'minioadmin'
|
||||
MINIO_ROOT_PASSWORD: 'minioadmin'
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
# NOTE: Only if you're using S3 to save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
# Uncomment this block if you're using AWS S3
|
||||
# minio:
|
||||
# image: quay.io/minio/minio
|
||||
# container_name: minio
|
||||
# restart: always
|
||||
# ports:
|
||||
# - '127.0.0.1:9000:9000'
|
||||
# - '127.0.0.1:9001:9001'
|
||||
# environment:
|
||||
# MINIO_REGION: 'eu-west-2'
|
||||
# MINIO_ROOT_USER: 'minioadmin'
|
||||
# MINIO_ROOT_PASSWORD: 'minioadmin'
|
||||
# volumes:
|
||||
# - minio_data:/data
|
||||
# command: server /data --console-address ":9001"
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
volumes:
|
||||
# NOTE: Only if you're using MongoDB
|
||||
mongodb_data: {}
|
||||
minio_data: {}
|
||||
pg_data: {}
|
||||
@@ -0,0 +1,265 @@
|
||||
# Learn about the various environment variables at
|
||||
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
|
||||
|
||||
# Increase rack default timeout (30s) to avoid timeout errors
|
||||
# Workaround for https://github.com/devlikeapro/waha/issues/1389
|
||||
RACK_TIMEOUT_SERVICE_TIMEOUT=60
|
||||
|
||||
# Used to verify the integrity of signed cookies. so ensure a secure value is set
|
||||
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
|
||||
# Use `rake secret` to generate this variable
|
||||
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||
|
||||
# Replace with the URL you are planning to use for your app
|
||||
FRONTEND_URL=http://chatwoot:3009
|
||||
# To use a dedicated URL for help center pages
|
||||
# HELPCENTER_URL=http://0.0.0.0:3000
|
||||
|
||||
# If the variable is set, all non-authenticated pages would fallback to the default locale.
|
||||
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
|
||||
# DEFAULT_LOCALE=en
|
||||
|
||||
# If you plan to use CDN for your assets, set Asset CDN Host
|
||||
ASSET_CDN_HOST=
|
||||
|
||||
# Force all access to the app over SSL, default is set to false
|
||||
FORCE_SSL=false
|
||||
|
||||
# This lets you control new sign ups on your chatwoot installation
|
||||
# true : default option, allows sign ups
|
||||
# false : disables all the end points related to sign ups
|
||||
# api_only: disables the UI for signup, but you can create sign ups via the account apis
|
||||
ENABLE_ACCOUNT_SIGNUP=false
|
||||
|
||||
# Redis config
|
||||
# specify the configs via single URL or individual variables
|
||||
# ref: https://www.iana.org/assignments/uri-schemes/prov/redis
|
||||
# You can also use the following format for the URL: redis://:password@host:port/db_number
|
||||
REDIS_URL=redis://redis:6379
|
||||
# If you are using docker-compose, set this variable's value to be any string,
|
||||
# which will be the password for the redis service running inside the docker-compose
|
||||
# to make it secure
|
||||
REDIS_PASSWORD=redis
|
||||
# Redis Sentinel can be used by passing list of sentinel host and ports e,g. sentinel_host1:port1,sentinel_host2:port2
|
||||
REDIS_SENTINELS=
|
||||
# Redis sentinel master name is required when using sentinel, default value is "mymaster".
|
||||
# You can find list of master using "SENTINEL masters" command
|
||||
REDIS_SENTINEL_MASTER_NAME=
|
||||
|
||||
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
|
||||
# Use the following environment variable to customize passwords for sentinels.
|
||||
# Use empty string if sentinels are configured with out passwords
|
||||
# REDIS_SENTINEL_PASSWORD=
|
||||
|
||||
# Redis premium breakage in heroku fix
|
||||
# enable the following configuration
|
||||
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||
# REDIS_OPENSSL_VERIFY_MODE=none
|
||||
|
||||
# Postgres Database config variables
|
||||
# You can leave POSTGRES_DATABASE blank. The default name of
|
||||
# the database in the production environment is chatwoot_production
|
||||
POSTGRES_DATABASE=chatwoot
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_USERNAME=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
RAILS_ENV=development
|
||||
# Changes the Postgres query timeout limit. The default is 14 seconds. Modify only when required.
|
||||
# POSTGRES_STATEMENT_TIMEOUT=14s
|
||||
RAILS_MAX_THREADS=5
|
||||
|
||||
# The email from which all outgoing emails are sent
|
||||
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
||||
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||
|
||||
#SMTP domain key is set up for HELO checking
|
||||
SMTP_DOMAIN=chatwoot.com
|
||||
# Set the value to "mailhog" if using docker-compose for development environments,
|
||||
# Set the value as "localhost" or your SMTP address in other environments
|
||||
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
|
||||
SMTP_ADDRESS=
|
||||
SMTP_PORT=1025
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
# plain,login,cram_md5
|
||||
SMTP_AUTHENTICATION=
|
||||
SMTP_ENABLE_STARTTLS_AUTO=true
|
||||
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
||||
SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
# Comment out the following environment variables if required by your SMTP server
|
||||
# SMTP_TLS=
|
||||
# SMTP_SSL=
|
||||
# SMTP_OPEN_TIMEOUT
|
||||
# SMTP_READ_TIMEOUT
|
||||
|
||||
# Mail Incoming
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
# Set this to the appropriate ingress channel with regards to incoming emails
|
||||
# Possible values are :
|
||||
# relay for Exim, Postfix, Qmail
|
||||
# mailgun for Mailgun
|
||||
# mandrill for Mandrill
|
||||
# postmark for Postmark
|
||||
# sendgrid for Sendgrid
|
||||
RAILS_INBOUND_EMAIL_SERVICE=
|
||||
# Use one of the following based on the email ingress service
|
||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||
# Set this to a password of your choice and use it in the Inbound webhook
|
||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||
|
||||
MAILGUN_INGRESS_SIGNING_KEY=
|
||||
MANDRILL_INGRESS_API_KEY=
|
||||
|
||||
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
|
||||
# Inbound webhook URL format:
|
||||
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
|
||||
# Note: Replace the values inside the brackets; do not include the brackets themselves.
|
||||
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
|
||||
# For Postmark
|
||||
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
|
||||
|
||||
# Storage
|
||||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
# Amazon S3
|
||||
# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage
|
||||
S3_BUCKET_NAME=
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_REGION=
|
||||
|
||||
# Log settings
|
||||
# Disable if you want to write logs to a file
|
||||
RAILS_LOG_TO_STDOUT=true
|
||||
LOG_LEVEL=info
|
||||
LOG_SIZE=500
|
||||
# Configure this environment variable if you want to use lograge instead of rails logger
|
||||
#LOGRAGE_ENABLED=true
|
||||
|
||||
### This environment variables are only required if you are setting up social media channels
|
||||
|
||||
# Facebook
|
||||
# documentation: https://www.chatwoot.com/docs/facebook-setup
|
||||
FB_VERIFY_TOKEN=
|
||||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
||||
IG_VERIFY_TOKEN=
|
||||
|
||||
# Twitter
|
||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||
TWITTER_APP_ID=
|
||||
TWITTER_CONSUMER_KEY=
|
||||
TWITTER_CONSUMER_SECRET=
|
||||
TWITTER_ENVIRONMENT=
|
||||
|
||||
#slack integration
|
||||
SLACK_CLIENT_ID=
|
||||
SLACK_CLIENT_SECRET=
|
||||
|
||||
# Google OAuth
|
||||
GOOGLE_OAUTH_CLIENT_ID=
|
||||
GOOGLE_OAUTH_CLIENT_SECRET=
|
||||
GOOGLE_OAUTH_CALLBACK_URL=
|
||||
|
||||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
||||
ANDROID_BUNDLE_ID=com.chatwoot.app
|
||||
|
||||
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
|
||||
ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:D4:5D:D4:53:F8:3B:FB:D3:C6:28:64:1D:AA:08:1E:D8
|
||||
|
||||
### Smart App Banner
|
||||
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
|
||||
# You can find your app-id in https://itunesconnect.apple.com
|
||||
#IOS_APP_IDENTIFIER=1495796682
|
||||
|
||||
## Push Notification
|
||||
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
||||
# VAPID_PUBLIC_KEY=
|
||||
# VAPID_PRIVATE_KEY=
|
||||
#
|
||||
# for mobile apps
|
||||
# FCM_SERVER_KEY=
|
||||
|
||||
### APM and Error Monitoring configurations
|
||||
## Elastic APM
|
||||
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
|
||||
# ELASTIC_APM_SERVER_URL=
|
||||
# ELASTIC_APM_SECRET_TOKEN=
|
||||
|
||||
## Sentry
|
||||
# SENTRY_DSN=
|
||||
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
# SCOUT_NAME=YOURAPPNAME (Production)
|
||||
# SCOUT_MONITOR=true
|
||||
|
||||
## NewRelic
|
||||
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
|
||||
# NEW_RELIC_LICENSE_KEY=
|
||||
# Set this to true to allow newrelic apm to send logs.
|
||||
# This is turned off by default.
|
||||
# NEW_RELIC_APPLICATION_LOGGING_ENABLED=
|
||||
|
||||
## Datadog
|
||||
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
||||
# DD_TRACE_AGENT_URL=
|
||||
|
||||
# MaxMindDB API key to download GeoLite2 City database
|
||||
# IP_LOOKUP_API_KEY=
|
||||
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=true
|
||||
# RACK_ATTACK_LIMIT=300
|
||||
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||
CONVERSATION_MESSAGE_PER_MINUTE_LIMIT=3000
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
## setting this value to true will disable the frontend dashboard endpoints
|
||||
# CW_API_ONLY_SERVER=false
|
||||
|
||||
## Development Only Config
|
||||
# if you want to use letter_opener for local emails
|
||||
# LETTER_OPENER=true
|
||||
# meant to be used in github codespaces
|
||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||
|
||||
# If you want to use official mobile app,
|
||||
# the notifications would be relayed via a Chatwoot server
|
||||
ENABLE_PUSH_RELAY_SERVER=true
|
||||
|
||||
# Stripe API key
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# Set to true if you want to upload files to cloud storage using the signed url
|
||||
# Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true.
|
||||
DIRECT_UPLOADS_ENABLED=
|
||||
|
||||
#MS OAUTH creds
|
||||
AZURE_APP_ID=
|
||||
AZURE_APP_SECRET=
|
||||
|
||||
## Advanced configurations
|
||||
## Change these values to fine tune performance
|
||||
# control the concurrency setting of sidekiq
|
||||
# SIDEKIQ_CONCURRENCY=10
|
||||
|
||||
|
||||
# AI powered features
|
||||
## OpenAI key
|
||||
# OPENAI_API_KEY=
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# WAHA - WhatsApp HTTP API
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
#
|
||||
|
||||
# ====================
|
||||
# ===== SECURITY =====
|
||||
# ====================
|
||||
WAHA_API_KEY_PLAIN=00000000000000000000000000000000
|
||||
WAHA_API_KEY=00000000000000000000000000000000
|
||||
WAHA_DASHBOARD_USERNAME=admin
|
||||
WAHA_DASHBOARD_PASSWORD=11111111111111111111111111111111
|
||||
WHATSAPP_SWAGGER_USERNAME=admin
|
||||
WHATSAPP_SWAGGER_PASSWORD=11111111111111111111111111111111
|
||||
|
||||
# Disable Dashboard or Swagger
|
||||
WAHA_DASHBOARD_ENABLED=True
|
||||
WHATSAPP_SWAGGER_ENABLED=True
|
||||
|
||||
# ==================
|
||||
# ===== COMMON =====
|
||||
# ==================
|
||||
# Base URL for the API (used for webhooks, file URLs, etc.)
|
||||
WAHA_BASE_URL=http://waha:3000
|
||||
WAHA_PUBLIC_URL=http://localhost:3000
|
||||
|
||||
# Server configuration (if you need to customize hostname/port)
|
||||
# WHATSAPP_API_SCHEMA=http
|
||||
# WHATSAPP_API_PORT=3000
|
||||
# WHATSAPP_API_HOSTNAME=localhost
|
||||
|
||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||
WHATSAPP_DEFAULT_ENGINE=GOWS
|
||||
|
||||
# WAHA_CLIENT_DEVICE_NAME=YourApp
|
||||
|
||||
# =================
|
||||
# ===== APPS ======
|
||||
# =================
|
||||
WAHA_APPS_ENABLED=True
|
||||
REDIS_URL=redis://:redis@redis:6379
|
||||
|
||||
# ===================
|
||||
# ===== LOGGING =====
|
||||
# ===================
|
||||
# Log format: JSON (for log management systems) or PRETTY (for development)
|
||||
WAHA_LOG_FORMAT=JSON
|
||||
|
||||
# Log level: info, debug, error, warn
|
||||
WAHA_LOG_LEVEL=info
|
||||
|
||||
# Don't print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
|
||||
# =========================
|
||||
# ===== MEDIA STORAGE =====
|
||||
# =========================
|
||||
# Local storage (default)
|
||||
WAHA_MEDIA_STORAGE=LOCAL
|
||||
WHATSAPP_FILES_LIFETIME=1800
|
||||
WHATSAPP_FILES_FOLDER=/app/.media
|
||||
|
||||
# Media download settings
|
||||
# WHATSAPP_DOWNLOAD_MEDIA=true
|
||||
# WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
|
||||
# S3 storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=S3
|
||||
# WAHA_S3_REGION=eu-west-2
|
||||
# WAHA_S3_BUCKET=waha
|
||||
# WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
# WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
# WAHA_S3_ENDPOINT=http://minio:9000
|
||||
# WAHA_S3_FORCE_PATH_STYLE=True
|
||||
# WAHA_S3_PROXY_FILES=True
|
||||
|
||||
# PostgreSQL storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=POSTGRESQL
|
||||
# WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# ===========================
|
||||
# ===== SESSION STORAGE =====
|
||||
# ===========================
|
||||
# PostgreSQL for sessions (uncomment to use)
|
||||
WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# MongoDB for sessions (uncomment to use)
|
||||
# WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
|
||||
|
||||
|
||||
# ==================================
|
||||
# ===== ADVANCED CONFIGURATION =====
|
||||
# ==================================
|
||||
|
||||
# Timezone for screenshots and logs
|
||||
# TZ=Europe/Dublin
|
||||
|
||||
# Session management
|
||||
# WHATSAPP_START_SESSION=session1,session2
|
||||
# WHATSAPP_RESTART_ALL_SESSIONS=False
|
||||
|
||||
# Webhooks
|
||||
# WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
|
||||
# Proxy configuration
|
||||
# WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
# WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
# WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
|
||||
# HTTPS configuration
|
||||
# !DEPRECATED!
|
||||
# Setup nginx reverse proxy to handle TLS connection
|
||||
# using Let's encrypt or self-issued certificate
|
||||
# WAHA_HTTPS_ENABLED=true
|
||||
# WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
# WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem
|
||||
# WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
@@ -0,0 +1,98 @@
|
||||
services:
|
||||
waha:
|
||||
restart: always
|
||||
image: devlikeapro/waha-plus
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000/tcp'
|
||||
volumes:
|
||||
- './sessions:/app/.sessions' # required for WAHA Core only
|
||||
- './media:/app/.media'
|
||||
env_file:
|
||||
- .waha.env
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- chatwoot
|
||||
- chatwoot-sidekiq
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '100m'
|
||||
max-file: '10'
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
|
||||
base: &base
|
||||
image: chatwoot/chatwoot:v4.7.0
|
||||
env_file: .chatwoot.env ## Change this file for customized env variables
|
||||
volumes:
|
||||
- chatwoot_storage:/app/storage
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '100m'
|
||||
max-file: '10'
|
||||
|
||||
chatwoot:
|
||||
<<: *base
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
ports:
|
||||
- '127.0.0.1:3009:3009'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- RAILS_ENV=production
|
||||
- INSTALLATION_ENV=docker
|
||||
entrypoint: docker/entrypoints/rails.sh
|
||||
command: ['bundle', 'exec', 'rails', 's', '-p', '3009', '-b', '0.0.0.0']
|
||||
restart: always
|
||||
|
||||
chatwoot-sidekiq:
|
||||
<<: *base
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- RAILS_ENV=production
|
||||
- INSTALLATION_ENV=docker
|
||||
command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
|
||||
restart: always
|
||||
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
restart: always
|
||||
ports:
|
||||
- '127.0.0.1:5432:5432'
|
||||
volumes:
|
||||
- chatwoot_pg_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=chatwoot
|
||||
- POSTGRES_USER=postgres
|
||||
# Please provide your own password.
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: always
|
||||
command:
|
||||
- 'sh'
|
||||
- '-c'
|
||||
- >
|
||||
redis-server --bind 0.0.0.0 --port 6379 --requirepass
|
||||
${REDIS_PASSWORD:-redis} --timeout 300 --tcp-keepalive 60 --save 900 1
|
||||
--save 300 10 --save 60 10000 --appendonly yes --appendfsync everysec
|
||||
env_file: .chatwoot.env
|
||||
volumes:
|
||||
- chatwoot_redis:/data
|
||||
ports:
|
||||
- '127.0.0.1:6379:6379'
|
||||
|
||||
volumes:
|
||||
chatwoot_pg_data: {}
|
||||
chatwoot_redis: {}
|
||||
chatwoot_storage: {}
|
||||
@@ -0,0 +1,97 @@
|
||||
# https://waha.devlike.pro/docs/how-to/install/
|
||||
services:
|
||||
# Just to connect
|
||||
waha-dashboard:
|
||||
restart: unless-stopped
|
||||
image: devlikeapro/waha-plus:latest
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000/tcp'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- WAHA_WORKER_ID=waha-dashboard
|
||||
|
||||
waha1:
|
||||
restart: unless-stopped
|
||||
image: devlikeapro/waha-plus:latest
|
||||
ports:
|
||||
- '127.0.0.1:3001:3000/tcp'
|
||||
volumes:
|
||||
- './media:/app/.media'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- WAHA_WORKER_ID=waha1
|
||||
- WAHA_BASE_URL=http://localhost:3001
|
||||
- WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
# MongoDB
|
||||
- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
# S3
|
||||
- WAHA_MEDIA_STORAGE=S3
|
||||
- WAHA_S3_REGION=eu-west-2
|
||||
- WAHA_S3_BUCKET=waha
|
||||
- WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
- WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
- WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
- WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
- WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
|
||||
waha2:
|
||||
restart: unless-stopped
|
||||
image: devlikeapro/waha-plus:latest
|
||||
ports:
|
||||
- '127.0.0.1:3002:3000/tcp'
|
||||
volumes:
|
||||
- './media:/app/.media'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- WAHA_WORKER_ID=waha2
|
||||
- WAHA_BASE_URL=http://localhost:3002
|
||||
- WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
# MongoDB
|
||||
- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
# S3
|
||||
- WAHA_MEDIA_STORAGE=S3
|
||||
- WAHA_S3_REGION=eu-west-2
|
||||
- WAHA_S3_BUCKET=waha
|
||||
- WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
- WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
- WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
- WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
- WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
|
||||
# NOTE: Only if you're using MongoDB
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
# Comment this block if you're not using MongoDB
|
||||
mongodb:
|
||||
image: mongo
|
||||
container_name: mongodb
|
||||
ports:
|
||||
- '127.0.0.1:27017:27017/tcp'
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
restart: always
|
||||
|
||||
# NOTE: Only if you're using S3 to save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
# Comment this block if you're using S3
|
||||
minio:
|
||||
image: quay.io/minio/minio
|
||||
container_name: minio
|
||||
restart: always
|
||||
ports:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '127.0.0.1:9001:9001'
|
||||
environment:
|
||||
MINIO_REGION: 'eu-west-2'
|
||||
MINIO_ROOT_USER: 'minioadmin'
|
||||
MINIO_ROOT_PASSWORD: 'minioadmin'
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
volumes:
|
||||
# NOTE: Only if you're using MongoDB
|
||||
mongodb_data: {}
|
||||
minio_data: {}
|
||||
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
waha:
|
||||
restart: always
|
||||
container_name: waha
|
||||
# image: devlikeapro/waha:latest
|
||||
image: devlikeapro/waha-plus:latest
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
volumes:
|
||||
- './sessions:/app/.sessions'
|
||||
- './media:/app/.media'
|
||||
environment:
|
||||
- WAHA_API_KEY=321
|
||||
- WAHA_DASHBOARD_USERNAME=admin
|
||||
- WAHA_DASHBOARD_PASSWORD=admin
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WAHA_PRINT_QR=False
|
||||
- WAHA_MEDIA_STORAGE=LOCAL
|
||||
- WHATSAPP_FILES_LIFETIME=0
|
||||
- WHATSAPP_FILES_FOLDER=/app/.media
|
||||
|
||||
n8n:
|
||||
image: docker.n8n.io/n8nio/n8n
|
||||
container_name: n8n
|
||||
restart: always
|
||||
environment:
|
||||
- WEBHOOK_URL=http://n8n:5678
|
||||
ports:
|
||||
- '5678:5678'
|
||||
volumes:
|
||||
- n8n_data:/home/node/.n8n
|
||||
|
||||
volumes:
|
||||
n8n_data: {}
|
||||
@@ -25,20 +25,19 @@ services:
|
||||
- './.sessions:/app/.sessions'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_API_KEY=321
|
||||
# Username and password for Swagger
|
||||
- WHATSAPP_SWAGGER_USERNAME=swagger
|
||||
- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Username and password for WAHA Dashboard
|
||||
# you can use the same username and password for both Swagger and Dashboard
|
||||
# Default - waha / waha
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
- WAHA_DASHBOARD_USERNAME=admin
|
||||
- WAHA_DASHBOARD_PASSWORD=admin
|
||||
# Engine specific environment variables
|
||||
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
|
||||
restart: always
|
||||
@@ -54,10 +53,9 @@ services:
|
||||
- './.sessions:/app/.sessions'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_KEY=321
|
||||
# - WAHA_API_KEY=321
|
||||
# - WHATSAPP_SWAGGER_USERNAME=admin
|
||||
# - WHATSAPP_SWAGGER_PASSWORD=123
|
||||
# - WAHA_DASHBOARD_USERNAME=admin
|
||||
+74
-3
@@ -1,11 +1,82 @@
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
#
|
||||
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
|
||||
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
|
||||
#
|
||||
# Try to get CPU count using Node.js, fallback to 1 if it fails
|
||||
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);" 2>/dev/null) || cpus=1
|
||||
# Make sure cpus is a number, default to 1 if not
|
||||
case $cpus in
|
||||
''|*[!0-9]*) cpus=1 ;;
|
||||
esac
|
||||
uv_threadpool_size=$(($cpus * 2))
|
||||
|
||||
# Check if uv_threadpool_size is less than 4 (default), set it to 4 if it is
|
||||
if [ "$uv_threadpool_size" -lt 4 ]; then
|
||||
uv_threadpool_size=4
|
||||
fi
|
||||
|
||||
# Set UV_THREADPOOL_SIZE as an environment variable
|
||||
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
|
||||
|
||||
# Start your application using yarn
|
||||
yarn start:prod
|
||||
#
|
||||
# Handle API key hashing
|
||||
#
|
||||
# Save WHATSAPP_API_KEY or WAHA_API_KEY in a variable (WHATSAPP_API_KEY has priority)
|
||||
if [ -n "$WHATSAPP_API_KEY" ]; then
|
||||
key="$WHATSAPP_API_KEY"
|
||||
elif [ -n "$WAHA_API_KEY" ]; then
|
||||
key="$WAHA_API_KEY"
|
||||
fi
|
||||
|
||||
# Unset both environment variables
|
||||
unset WHATSAPP_API_KEY
|
||||
unset WAHA_API_KEY
|
||||
|
||||
# Process the key if it exists
|
||||
if [ -n "$key" ]; then
|
||||
# Check if key is already hashed
|
||||
if echo "$key" | grep -q "^sha512:"; then
|
||||
# If already hashed, use it as is
|
||||
export WAHA_API_KEY="$key"
|
||||
else
|
||||
# Hash the key using sha512sum
|
||||
HASHED_KEY=$(echo -n "$key" | sha512sum | awk '{print $1}')
|
||||
export WAHA_API_KEY="sha512:$HASHED_KEY"
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# xvfb-run
|
||||
#
|
||||
USE_XVFB=false
|
||||
|
||||
# Check WAHA_RUN_XVFB parameter - only test for "false" case, treat all others as True
|
||||
if [ "$WAHA_RUN_XVFB" = "false" ] || [ "$WAHA_RUN_XVFB" = "False" ] || [ "$WAHA_RUN_XVFB" = "0" ]; then
|
||||
# Explicitly disabled by user
|
||||
echo "WAHA_RUN_XVFB value: $WAHA_RUN_XVFB - xvfb is disabled"
|
||||
USE_XVFB=false
|
||||
else
|
||||
# Check engine and run test if it's WEBJS or not specified
|
||||
if [ -z "$WHATSAPP_DEFAULT_ENGINE" ] || [ "$WHATSAPP_DEFAULT_ENGINE" = "WEBJS" ]; then
|
||||
# Try to run xvfb-run with a test command
|
||||
if xvfb-run --auto-servernum echo "xvfb-run is working!"; then
|
||||
USE_XVFB=true
|
||||
else
|
||||
echo "xvfb-run test failed, do not run it"
|
||||
USE_XVFB=false
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Start your application using node with exec to ensure proper signal handling
|
||||
#
|
||||
if [ "$USE_XVFB" = "true" ]; then
|
||||
echo "Executing node with xvfb-run..."
|
||||
exec xvfb-run --auto-servernum node dist/main
|
||||
else
|
||||
echo "Executing node without xvfb-run..."
|
||||
exec node dist/main
|
||||
fi
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
+7
-1
@@ -3,7 +3,13 @@
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"plugins": ["@nestjs/swagger"],
|
||||
"assets": ["dashboard/**", "core/engines/webjs/*", "plus/engines/webjs/*"],
|
||||
"assets": [
|
||||
"dashboard/**",
|
||||
"core/engines/webjs/*",
|
||||
"plus/engines/webjs/*",
|
||||
"apps/chatwoot/i18n/locales/*.yaml",
|
||||
"apps/chatwoot/i18n/locales/*.yml"
|
||||
],
|
||||
"watchAssets": true
|
||||
}
|
||||
}
|
||||
+131
-50
@@ -14,108 +14,189 @@
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"start:prod-exit": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --max-warnings=0",
|
||||
"lint-cache": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --cache --max-warnings=0",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
"lint": "oxlint --deny-warnings",
|
||||
"lint-fix": "oxlint --fix --deny-warnings",
|
||||
"test": "yarn test:unit",
|
||||
"test:watch": "jest --selectProjects unit --watch",
|
||||
"test:cov": "jest --selectProjects unit --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --selectProjects unit --runInBand",
|
||||
"test:unit": "jest --selectProjects unit",
|
||||
"test:e2e": "jest --selectProjects e2e",
|
||||
"test:e8e": "yarn test:e2e",
|
||||
"test:all": "yarn test:unit && yarn test:e8e",
|
||||
"gows:proto:fetch": "node scripts/gows-proto.js fetch",
|
||||
"gows:proto:build": "node scripts/gows-proto.js build",
|
||||
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-2",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2026-02-11",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@aws-sdk/client-s3": "^3.633.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||
"@bull-board/api": "^6.9.1",
|
||||
"@bull-board/express": "^6.9.1",
|
||||
"@bull-board/nestjs": "^6.9.1",
|
||||
"@casl/ability": "^6.8.0",
|
||||
"@figuro/chatwoot-sdk": "^1.1.17",
|
||||
"@liaoliaots/nestjs-redis": "^9",
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
"@nestjs/common": "^9.0.9",
|
||||
"@nestjs/bullmq": "^11.0.2",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^9.0.9",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.0.9",
|
||||
"@nestjs/platform-ws": "^9.0.9",
|
||||
"@nestjs/serve-static": "^2.1.3",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.1.5",
|
||||
"@nestjs/platform-ws": "^11.0.0",
|
||||
"@nestjs/serve-static": "^5.0.0",
|
||||
"@nestjs/swagger": "^7.1.11",
|
||||
"@nestjs/terminus": "^10.2.3",
|
||||
"@nestjs/websockets": "^9.0.9",
|
||||
"@nestjs/websockets": "^11.0.0",
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/sqlite3": "^5.1.0",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@wppconnect-team/wppconnect": "^1.40.1",
|
||||
"adm-zip": "0.5.10",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"async-lock": "^1.4.1",
|
||||
"better-sqlite3": "9.6.0",
|
||||
"audio-decode": "^2.2.2",
|
||||
"axios": "^1.9.0",
|
||||
"axios-retry": "^4.5.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"bullmq": "^5.48.1",
|
||||
"check-disk-space": "^3.4.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "0.14.0",
|
||||
"del": "^6.0.0",
|
||||
"commander": "^14.0.1",
|
||||
"del": "^8.0.0",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"google-protobuf": "^3.21.4",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"ical.js": "^2.2.1",
|
||||
"ioredis": "^5",
|
||||
"joi": "^17.13.3",
|
||||
"knex": "^3.1.0",
|
||||
"libphonenumber-js": "^1.10.36",
|
||||
"libsignal": "github:WhiskeySockets/libsignal-node#master",
|
||||
"link-preview-js": "^3.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"long": "^5.3.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"mongodb": "^6.3.0",
|
||||
"mongodb": "6.9.0",
|
||||
"ms": "^2.1.3",
|
||||
"mustache": "^4.2.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"passport": "^0.6.0",
|
||||
"node-cache": "5.1.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"puppeteer": "^21.0.0",
|
||||
"puppeteer": "^24.31.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"request": "^2.88.2",
|
||||
"requestretry": "^4.1.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.1.0",
|
||||
"rimraf": "^4.3.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.4",
|
||||
"shell-quote": "^1.8.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"venom-bot": "5.0.1",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels"
|
||||
"ulid": "^2.3.0",
|
||||
"undici": "^7.16.0",
|
||||
"uniqid": "^5.4.0",
|
||||
"user-agents": "^1.1.669",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18",
|
||||
"write-file-atomic": "^6.0.0",
|
||||
"yaml": "^2.7.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"typescript": "5.9.2",
|
||||
"bufferutil": "^4.0.8",
|
||||
"ws": "^8.18.0",
|
||||
"puppeteer": "^24.31.0",
|
||||
"whatwg-url": "13.0.0",
|
||||
"axios": "^1.9.0",
|
||||
"whatsapp-rust-bridge": "npm:@devlikeapro/whatsapp-rust-bridge@0.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.1",
|
||||
"@nestjs/testing": "^9.0.9",
|
||||
"@grpc/grpc-js": "^1.14.1",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.0",
|
||||
"@types/express": "^4.17.3",
|
||||
"@types/jest": "26.0.10",
|
||||
"@types/node": "^18.14.2",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "3.9.1",
|
||||
"@typescript-eslint/parser": "3.9.1",
|
||||
"eslint": "7.7.0",
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"@types/user-agents": "^1",
|
||||
"dotenv": "^17.2.3",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"jest": "^29.7.0",
|
||||
"oxlint": "^1.13.0",
|
||||
"prettier": "^1.19.1",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
"supertest": "^4.0.2",
|
||||
"ts-jest": "^29.1.3",
|
||||
"ts-loader": "^6.2.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".test.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
"projects": [
|
||||
{
|
||||
"displayName": "unit",
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"moduleNameMapper": {
|
||||
"^@waha/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
"testRegex": ".test.ts$",
|
||||
"testPathIgnorePatterns": [
|
||||
"/__tests__/e2e/"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
{
|
||||
"displayName": "e2e",
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"moduleNameMapper": {
|
||||
"^@waha/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
"testRegex": "__tests__/e2e/.*\\.test\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@3.6.3"
|
||||
"packageManager": "yarn@4.9.2"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set source and output file
|
||||
SOURCE_DIR="src"
|
||||
OUTPUT_DIR="$HOME/Downloads/chatgpt"
|
||||
OUTPUT_FILE="$OUTPUT_DIR/waha.ts"
|
||||
|
||||
# Create or clear the output file
|
||||
rm -f "$OUTPUT_FILE"
|
||||
touch "$OUTPUT_FILE"
|
||||
|
||||
# Find all .md files in the source directory and combine them
|
||||
find "$SOURCE_DIR" -type f -name "*.ts" | while read -r file; do
|
||||
# Add file name to OUTPUT_FILE
|
||||
echo "\n-------" >> "$OUTPUT_FILE"
|
||||
echo "File: $file" >> "$OUTPUT_FILE"
|
||||
echo "-------\n" >> "$OUTPUT_FILE"
|
||||
# Append the content of the file to the output file
|
||||
cat "$file" >> "$OUTPUT_FILE"
|
||||
done
|
||||
|
||||
echo "All .ts files combined into $OUTPUT_FILE with separators."
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
const { readFileSync } = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const YAML = require('yaml');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const enPath = path.join(repoRoot, 'src/apps/chatwoot/i18n/locales/en-US.yaml');
|
||||
const contents = readFileSync(enPath, 'utf8');
|
||||
const document = YAML.parse(contents);
|
||||
|
||||
const flatten = (node, prefix = '') => {
|
||||
if (node && typeof node === 'object' && !Array.isArray(node)) {
|
||||
return Object.entries(node).reduce((acc, [key, value]) => {
|
||||
const next = prefix ? `${prefix}.${key}` : key;
|
||||
return acc.concat(flatten(value, next));
|
||||
}, []);
|
||||
}
|
||||
|
||||
return [prefix];
|
||||
};
|
||||
|
||||
const allKeys = flatten(document).filter(Boolean);
|
||||
|
||||
const ignoredPrefixes = ['datetime.', 'locale.'];
|
||||
|
||||
const ignored = new Set();
|
||||
const unused = [];
|
||||
|
||||
const searchRoots = ['src', 'tests'];
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (
|
||||
ignoredPrefixes.some((prefix) => key.startsWith(prefix)) ||
|
||||
!key.includes('.')
|
||||
) {
|
||||
ignored.add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
'rg',
|
||||
[
|
||||
'--files-with-matches',
|
||||
'--fixed-strings',
|
||||
key,
|
||||
...searchRoots,
|
||||
'--glob',
|
||||
'!apps/chatwoot/i18n/locales/*.yaml',
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status !== 0 || !result.stdout.trim()) {
|
||||
unused.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
const message = [
|
||||
`Scanned ${allKeys.length} keys.`,
|
||||
`Ignored ${ignored.size} keys (datetime.*, locale.*, or without dot).`,
|
||||
unused.length
|
||||
? `Found ${unused.length} potentially unused keys:\n - ${unused.join(
|
||||
'\n - ',
|
||||
)}`
|
||||
: 'No unused keys detected.',
|
||||
];
|
||||
|
||||
console.log(message.join('\n'));
|
||||
@@ -0,0 +1,144 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const path = require('path');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const axios = require('axios');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { execSync } = require('child_process');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const yargs = require('yargs');
|
||||
|
||||
// Defaults
|
||||
const CONFIG_FILE = 'waha.config.json';
|
||||
// Load defaults from package.json
|
||||
gows = (() => {
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||||
return config.waha.gows || {};
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
const DEFAULT_REPO = gows.repo;
|
||||
if (!DEFAULT_REPO) {
|
||||
throw new Error(`Missing default repo in ${CONFIG_FILE}`);
|
||||
}
|
||||
const DEFAULT_REF = gows.ref;
|
||||
if (!DEFAULT_REF) {
|
||||
throw new Error('Missing default ref in ${CONFIG_FILE}');
|
||||
}
|
||||
const DEFAULT_DIR = './src/core/engines/gows/proto';
|
||||
|
||||
const PROTO_FILES = ['gows.proto'];
|
||||
const PROTO_OUTPUT = './src/core/engines/gows/grpc';
|
||||
|
||||
// Helper function to clean directory
|
||||
function cleanDirectory(directory, suffix) {
|
||||
if (!fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(directory);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directory, file);
|
||||
if (file.endsWith(suffix)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to download files
|
||||
async function downloadFiles(repo, ref, directory) {
|
||||
for (const file of PROTO_FILES) {
|
||||
const url = `https://github.com/${repo}/releases/download/${ref}/${file}`;
|
||||
const filePath = path.join(directory, file);
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
fs.writeFileSync(filePath, response.data);
|
||||
console.log(`Downloaded: ${file}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to download ${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for fetch command
|
||||
async function handleFetch(repo, ref, dir) {
|
||||
console.log(`Fetching .proto files from ${repo}@${ref} to ${dir}...`);
|
||||
cleanDirectory(dir, '.proto');
|
||||
await downloadFiles(repo, ref, dir);
|
||||
}
|
||||
|
||||
// Handler for build command
|
||||
function handleBuild(dir) {
|
||||
console.log('Building gRPC files...');
|
||||
cleanDirectory(PROTO_OUTPUT);
|
||||
|
||||
const command = `grpc_tools_node_protoc \
|
||||
--plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts \
|
||||
--plugin=protoc-gen-grpc=node_modules/.bin/grpc_tools_node_protoc_plugin \
|
||||
--js_out=import_style=commonjs,binary:${PROTO_OUTPUT} \
|
||||
--grpc_out=grpc_js:${PROTO_OUTPUT} \
|
||||
--ts_out=grpc_js:${PROTO_OUTPUT} \
|
||||
-I ${dir} ${dir}/gows.proto`;
|
||||
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
console.log('gRPC files built successfully.');
|
||||
} catch (error) {
|
||||
console.error(`Failed to build gRPC files: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Commands
|
||||
//
|
||||
|
||||
// fetch
|
||||
yargs.command(
|
||||
'fetch',
|
||||
'Fetch .proto files from GitHub',
|
||||
(yargs) => {
|
||||
yargs
|
||||
.option('repo', {
|
||||
describe: 'GitHub repository (owner/repo)',
|
||||
type: 'string',
|
||||
default: DEFAULT_REPO,
|
||||
})
|
||||
.option('ref', {
|
||||
describe: 'Git reference (branch or commit SHA)',
|
||||
type: 'string',
|
||||
default: DEFAULT_REF,
|
||||
})
|
||||
.option('dir', {
|
||||
describe: 'Directory to output .proto files',
|
||||
type: 'string',
|
||||
default: DEFAULT_DIR,
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
await handleFetch(argv.repo, argv.ref, argv.dir);
|
||||
},
|
||||
);
|
||||
|
||||
// build
|
||||
yargs.command(
|
||||
'build',
|
||||
'Build gRPC files from .proto files',
|
||||
(yargs) => {
|
||||
yargs.option('dir', {
|
||||
describe: 'Directory containing .proto files',
|
||||
type: 'string',
|
||||
default: DEFAULT_DIR,
|
||||
});
|
||||
},
|
||||
(argv) => {
|
||||
handleBuild(argv.dir);
|
||||
},
|
||||
);
|
||||
|
||||
// Parse arguments
|
||||
yargs.parse();
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const forceIndex = args.indexOf('--force');
|
||||
const forceOverwrite = forceIndex !== -1;
|
||||
|
||||
if (forceOverwrite) {
|
||||
args.splice(forceIndex, 1);
|
||||
}
|
||||
|
||||
const folderArg = args[0] || '.';
|
||||
const sourceArgRaw = args[1];
|
||||
const targetArg = args[2] || '.env';
|
||||
|
||||
const envDir = path.resolve(process.cwd(), folderArg);
|
||||
const sourceArg = sourceArgRaw || '.env.example';
|
||||
const templateBaseDir = sourceArgRaw ? envDir : projectRoot;
|
||||
const templatePath = path.isAbsolute(sourceArg)
|
||||
? sourceArg
|
||||
: path.resolve(templateBaseDir, sourceArg);
|
||||
const targetPath = path.isAbsolute(targetArg)
|
||||
? targetArg
|
||||
: path.resolve(envDir, targetArg);
|
||||
|
||||
function exitWithMessage(message, code = 1) {
|
||||
console.error(message);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(envDir)) {
|
||||
exitWithMessage(`Target directory does not exist: ${envDir}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
exitWithMessage(`Missing template file: ${templatePath}`);
|
||||
}
|
||||
|
||||
if (!forceOverwrite && fs.existsSync(targetPath)) {
|
||||
const stats = fs.statSync(targetPath);
|
||||
if (stats.size > 0) {
|
||||
exitWithMessage(
|
||||
`${targetPath} already exists and is not empty. Remove it manually or rerun with --force to overwrite.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const exampleContent = fs.readFileSync(templatePath, 'utf8');
|
||||
|
||||
function generateSecret() {
|
||||
return crypto.randomUUID({ disableEntropyCache: true }).replace(/-/g, '');
|
||||
}
|
||||
|
||||
const apiKey = generateSecret();
|
||||
const adminPassword = generateSecret();
|
||||
|
||||
const updatedContent = exampleContent
|
||||
.replace(/^WAHA_API_KEY=.*/m, `WAHA_API_KEY=${apiKey}`)
|
||||
.replace(/^WAHA_API_KEY_PLAIN=.*/m, `WAHA_API_KEY_PLAIN=${apiKey}`)
|
||||
.replace(/^WAHA_DASHBOARD_USERNAME=.*/m, 'WAHA_DASHBOARD_USERNAME=admin')
|
||||
.replace(
|
||||
/^WAHA_DASHBOARD_PASSWORD=.*/m,
|
||||
`WAHA_DASHBOARD_PASSWORD=${adminPassword}`,
|
||||
)
|
||||
.replace(/^WHATSAPP_SWAGGER_USERNAME=.*/m, 'WHATSAPP_SWAGGER_USERNAME=admin')
|
||||
.replace(
|
||||
/^WHATSAPP_SWAGGER_PASSWORD=.*/m,
|
||||
`WHATSAPP_SWAGGER_PASSWORD=${adminPassword}`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(targetPath, updatedContent, { encoding: 'utf8' });
|
||||
|
||||
const generatedEnvValues = [
|
||||
`WAHA_API_KEY=${apiKey}`,
|
||||
`WAHA_API_KEY_PLAIN=${apiKey}`,
|
||||
'WAHA_DASHBOARD_USERNAME=admin',
|
||||
`WAHA_DASHBOARD_PASSWORD=${adminPassword}`,
|
||||
'WHATSAPP_SWAGGER_USERNAME=admin',
|
||||
`WHATSAPP_SWAGGER_PASSWORD=${adminPassword}`,
|
||||
];
|
||||
|
||||
const lines = [
|
||||
'Credentials generated.',
|
||||
'',
|
||||
'Generated env values:',
|
||||
...generatedEnvValues.map((line) => ` - ${line}`),
|
||||
'',
|
||||
'Use these credentials to login in Dashboard or Swagger:',
|
||||
' - Username: admin',
|
||||
` - Password: ${adminPassword}`,
|
||||
'',
|
||||
'Use this API key in the x-api-key header:',
|
||||
` - ${apiKey}`,
|
||||
'',
|
||||
'Read more:',
|
||||
' - https://waha.devlike.pro/docs/how-to/dashboard/#api-key',
|
||||
' - https://waha.devlike.pro/docs/how-to/security/',
|
||||
];
|
||||
|
||||
console.log(lines.join('\n'));
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo "Starting WAHA..."
|
||||
|
||||
docker run -d \
|
||||
--restart=always \
|
||||
--name waha \
|
||||
-p 3000:3000/tcp \
|
||||
-v ./.sessions:/app/.sessions \
|
||||
-v ./.media:/app/.media \
|
||||
--env WHATSAPP_API_KEY=321 \
|
||||
--env WAHA_DASHBOARD_USERNAME=waha \
|
||||
--env WAHA_DASHBOARD_PASSWORD=waha \
|
||||
--env WAHA_LOG_FORMAT=PRETTY \
|
||||
--env WAHA_LOG_LEVEL=info \
|
||||
--env WHATSAPP_DEFAULT_ENGINE=WEBJS \
|
||||
--env WHATSAPP_RESTART_ALL_SESSIONS=True \
|
||||
--env WAHA_PRINT_QR=False \
|
||||
--env WHATSAPP_FILES_LIFETIME=0 \
|
||||
--env WHATSAPP_FILES_FOLDER=/app/.media \
|
||||
devlikeapro/waha-plus:latest
|
||||
|
||||
echo "WAHA started."
|
||||
@@ -1,21 +0,0 @@
|
||||
set -e
|
||||
echo "Updating WAHA..."
|
||||
echo "Logging in to Docker..."
|
||||
docker login -u devlikeapro # -p {KEY} - you can hard code key here https://portal.devlike.pro/
|
||||
|
||||
echo "Pulling the latest image..."
|
||||
docker pull devlikeapro/waha-plus
|
||||
|
||||
echo "Logging out of Docker..."
|
||||
docker logout
|
||||
|
||||
# Just pull if you're using WAHA Core
|
||||
# docker pull devlikeapro/waha
|
||||
|
||||
echo "Stopping the current container..."
|
||||
docker stop waha || true
|
||||
echo "Removing the current container..."
|
||||
docker rm waha || true
|
||||
|
||||
echo "Running WAHA..."
|
||||
sh ./waha-run.sh
|
||||
@@ -0,0 +1,391 @@
|
||||
import axios from 'axios';
|
||||
import WebSocket = require('ws');
|
||||
|
||||
const VALID_API_KEY = '666';
|
||||
const VALID_USERNAME = 'admin';
|
||||
const VALID_PASSWORD = '666';
|
||||
const VALID_SESSION_API_KEY = 'key_0fQfI3n3rFVsczBbBfknLXKUreY2my6J';
|
||||
|
||||
const BASE_URL = (process.env.WAHA_BASE_URL ?? 'http://localhost:3000').replace(
|
||||
/\/$/,
|
||||
'',
|
||||
);
|
||||
const WS_BASE_URL = BASE_URL.replace(/^http/, 'ws');
|
||||
|
||||
const HTTP_OK = 200;
|
||||
const HTTP_CREATED = 201;
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
const HTTP_FORBIDDEN = 403;
|
||||
const HTTP_NOT_FOUND = 404;
|
||||
const HTTP_UNPROCESSABLE = 422;
|
||||
const WS_POLICY_VIOLATION = 1008;
|
||||
const WS_OK_CODES = [1000, 1005];
|
||||
|
||||
describe('admin - GET /api/sessions/{name}', () => {
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('invalid api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
headers: { 'X-Api-Key': '123' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin - POST /api/sessions', () => {
|
||||
test('admin key can create a session without name and remove it', async () => {
|
||||
const createResponse = await axios.post(
|
||||
`${BASE_URL}/api/sessions`,
|
||||
{},
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
expect(createResponse.status).toBe(HTTP_CREATED);
|
||||
expect(createResponse.data?.name).toBeTruthy();
|
||||
|
||||
const deleteResponse = await axios.delete(
|
||||
`${BASE_URL}/api/sessions/${createResponse.data.name}`,
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
expect(deleteResponse.status).toBe(HTTP_OK);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/sessions?all=true', () => {
|
||||
beforeAll(async () => {
|
||||
const createResponse = await axios.post(
|
||||
`${BASE_URL}/api/sessions`,
|
||||
{ name: 'another' },
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
if (![HTTP_CREATED, HTTP_UNPROCESSABLE].includes(createResponse.status)) {
|
||||
throw new Error(
|
||||
`Unexpected status for creating "another" session: ${createResponse.status}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const deleteResponse = await axios.delete(
|
||||
`${BASE_URL}/api/sessions/another`,
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
if (![HTTP_OK, HTTP_NOT_FOUND].includes(deleteResponse.status)) {
|
||||
throw new Error(
|
||||
`Unexpected status for deleting "another" session: ${deleteResponse.status}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('admin key returns two sessions', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions?all=true`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
expect(response.data).toHaveLength(2);
|
||||
expect(response.data.map((session) => session.name).sort()).toEqual([
|
||||
'another',
|
||||
'default',
|
||||
]);
|
||||
});
|
||||
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions?all=true`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('session key returns one session', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions?all=true`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
expect(response.data).toHaveLength(1);
|
||||
expect(response.data[0]?.name).toBe('default');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/server/version', () => {
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/version`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('admin api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/version`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('session api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/version`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/server/environment', () => {
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/environment`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('admin api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/environment`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('session api key is forbidden', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/environment`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_FORBIDDEN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin - GET /health', () => {
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/health`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/health`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('invalid api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/health`, {
|
||||
headers: { 'X-Api-Key': '123' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/ws', () => {
|
||||
const buildWsUrl = (apiKey?: string) => {
|
||||
const wsUrl = new URL('/ws', WS_BASE_URL);
|
||||
wsUrl.searchParams.set('session', '*');
|
||||
wsUrl.searchParams.set('events', '*');
|
||||
if (apiKey) {
|
||||
wsUrl.searchParams.set('x-api-key', apiKey);
|
||||
}
|
||||
return wsUrl.toString();
|
||||
};
|
||||
|
||||
const waitForOpen = (socket: WebSocket, timeoutMs = 5_000) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('WebSocket open timeout'));
|
||||
}, timeoutMs);
|
||||
socket.once('open', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
socket.once('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
const waitForClose = (socket: WebSocket, timeoutMs = 5_000) =>
|
||||
new Promise<{ code: number; reason: string }>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('WebSocket close timeout'));
|
||||
}, timeoutMs);
|
||||
socket.once('close', (code, reason) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ code, reason: reason.toString() });
|
||||
});
|
||||
socket.once('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
test('no api key is unauthorized', async () => {
|
||||
const socket = new WebSocket(buildWsUrl());
|
||||
const { code } = await waitForClose(socket);
|
||||
expect(code).toBe(WS_POLICY_VIOLATION);
|
||||
});
|
||||
|
||||
test('admin api key is ok', async () => {
|
||||
const socket = new WebSocket(buildWsUrl(VALID_API_KEY));
|
||||
await waitForOpen(socket);
|
||||
socket.close();
|
||||
const { code } = await waitForClose(socket);
|
||||
expect(WS_OK_CODES).toContain(code);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/files/test.txt', () => {
|
||||
// create "test.txt" in current dir/.media/test.txt
|
||||
let file = null;
|
||||
beforeAll(() => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mediaDir = path.resolve('./.media');
|
||||
if (!fs.existsSync(mediaDir)) {
|
||||
fs.mkdirSync(mediaDir);
|
||||
}
|
||||
const filePath = path.join(mediaDir, 'test.txt');
|
||||
fs.writeFileSync(filePath, 'This is a test file.');
|
||||
file = filePath;
|
||||
});
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/files/test.txt`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/files/test.txt`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('invalid api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/files/test.txt`, {
|
||||
headers: { 'X-Api-Key': '123' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /', () => {
|
||||
test('no auth is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid auth is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/`, {
|
||||
auth: { username: VALID_USERNAME, password: VALID_PASSWORD },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('wrong auth is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/`, {
|
||||
auth: { username: VALID_USERNAME, password: 'password-another' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /dashboard', () => {
|
||||
test('no auth is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/dashboard/`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid auth is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/dashboard/`, {
|
||||
auth: { username: VALID_USERNAME, password: VALID_PASSWORD },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('wrong auth is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/dashboard/`, {
|
||||
auth: { username: VALID_USERNAME, password: 'password-another' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('session key - GET /api/sessions/{name}', () => {
|
||||
test('default key - default session - is authorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('default key - create a new sessions - forbidden', async () => {
|
||||
const response = await axios.post(
|
||||
`${BASE_URL}/api/sessions`,
|
||||
{ name: 'newone' },
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
expect(response.status).toBe(HTTP_FORBIDDEN);
|
||||
});
|
||||
|
||||
test('another key - default session - unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
headers: { 'X-Api-Key': 'key_another' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('default key - another session - forbidden', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/another`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_FORBIDDEN);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { ApiKey, CheckInvariant } from '@waha/core/storage/IApiKeyRepository';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { ApiKeyDTO, ApiKeyRequest } from '@waha/structures/apikeys.dto';
|
||||
import { generatePrefixedId, generateSecret } from '@waha/utils/ids';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/keys')
|
||||
@ApiTags('🔑 Api Keys')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
export class ApiKeysController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('/')
|
||||
@ApiOperation({ summary: 'Create a new API key' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() body: ApiKeyRequest): Promise<ApiKeyDTO> {
|
||||
CheckInvariant(body);
|
||||
if (body.session) {
|
||||
const exists = await this.manager.exists(body.session);
|
||||
if (!exists) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Session "${body.session}" does not exist`,
|
||||
);
|
||||
}
|
||||
}
|
||||
let apikey: ApiKey | null = null;
|
||||
// Try 5 times to check there's no conflict on id and key
|
||||
for (let i = 0; i < 5; i++) {
|
||||
apikey = {
|
||||
id: generatePrefixedId('key_id'),
|
||||
key: `key_${generateSecret(32)}`,
|
||||
isActive: body.isActive,
|
||||
isAdmin: body.isAdmin,
|
||||
session: body.session,
|
||||
rules: null,
|
||||
};
|
||||
const idExists = await this.manager.apiKeyRepository.getById(apikey.id);
|
||||
if (idExists) {
|
||||
continue;
|
||||
}
|
||||
const keyExists = await this.manager.apiKeyRepository.getByKey(
|
||||
apikey.key,
|
||||
);
|
||||
if (keyExists) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!apikey) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Failed to generate API key, try again`,
|
||||
);
|
||||
}
|
||||
CheckInvariant(apikey);
|
||||
await this.manager.apiKeyRepository.upsert(apikey);
|
||||
return ApiKeyToDTO(apikey);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({ summary: 'Get all API keys' })
|
||||
async list(): Promise<ApiKeyDTO[]> {
|
||||
const keys = await this.manager.apiKeyRepository.list();
|
||||
return keys.map((key) => ApiKeyToDTO(key));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@ApiOperation({ summary: 'Update an API key' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() body: ApiKeyRequest,
|
||||
): Promise<ApiKeyDTO> {
|
||||
const existing = await this.manager.apiKeyRepository.getById(id);
|
||||
if (!existing) {
|
||||
throw new NotFoundException('API key not found');
|
||||
}
|
||||
|
||||
const apikey: ApiKey = {
|
||||
...existing,
|
||||
isActive: body.isActive,
|
||||
isAdmin: body.isAdmin,
|
||||
session: body.session,
|
||||
};
|
||||
CheckInvariant(apikey);
|
||||
if (apikey.session) {
|
||||
const exists = await this.manager.exists(apikey.session);
|
||||
if (!exists) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Session "${apikey.session}" does not exist`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CheckInvariant(apikey);
|
||||
await this.manager.apiKeyRepository.upsert(apikey);
|
||||
return ApiKeyToDTO(apikey);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Delete an API key' })
|
||||
async delete(@Param('id') id: string): Promise<{ result: true }> {
|
||||
const existing = await this.manager.apiKeyRepository.getById(id);
|
||||
if (!existing) {
|
||||
throw new NotFoundException('API key not found');
|
||||
}
|
||||
await this.manager.apiKeyRepository.deleteById(id);
|
||||
return { result: true };
|
||||
}
|
||||
}
|
||||
|
||||
function ApiKeyToDTO(apikey: ApiKey): ApiKeyDTO {
|
||||
return {
|
||||
id: apikey.id,
|
||||
key: apikey.key,
|
||||
isActive: apikey.isActive,
|
||||
isAdmin: apikey.isAdmin,
|
||||
session: apikey.session,
|
||||
};
|
||||
}
|
||||
+13
-48
@@ -5,11 +5,12 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import {
|
||||
QRCodeSessionParam,
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
@@ -18,19 +19,23 @@ import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import {
|
||||
CaptchaBody,
|
||||
OTPRequest,
|
||||
QRCodeFormat,
|
||||
QRCodeQuery,
|
||||
QRCodeValue,
|
||||
RequestCodeRequest,
|
||||
} from '../structures/auth.dto';
|
||||
import { WAHASessionStatus } from '../structures/enums.dto';
|
||||
import { Base64File } from '../structures/files.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/auth')
|
||||
@ApiTags('🔑 Auth')
|
||||
@ApiTags('📱 Pairing')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
class AuthController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -39,16 +44,12 @@ class AuthController {
|
||||
summary: 'Get QR code for pairing WhatsApp API.',
|
||||
})
|
||||
@SessionApiParam
|
||||
@ApiFileAcceptHeader(Base64File, QRCodeValue)
|
||||
@UseInterceptors(new BufferResponseInterceptor())
|
||||
@ApiFileAcceptHeader('image/png', Base64File, QRCodeValue)
|
||||
@UseInterceptors(new BufferResponseInterceptor('image/png'))
|
||||
async getQR(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@QRCodeSessionParam session: WhatsappSession,
|
||||
@Query() query: QRCodeQuery,
|
||||
): Promise<Buffer | QRCodeValue> {
|
||||
if (session.status != WAHASessionStatus.SCAN_QR_CODE) {
|
||||
const err = `Can get QR code only in SCAN_QR_CODE status. The current status is '${session.status}'`;
|
||||
throw new UnprocessableEntityException(err);
|
||||
}
|
||||
const qr = session.getQR();
|
||||
if (query.format == QRCodeFormat.RAW) {
|
||||
return { value: qr.raw };
|
||||
@@ -67,42 +68,6 @@ class AuthController {
|
||||
) {
|
||||
return session.requestCode(request.phoneNumber, request.method, request);
|
||||
}
|
||||
|
||||
@Post('authorize-code')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Send OTP authentication code.',
|
||||
})
|
||||
authorizeCode(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() request: OTPRequest,
|
||||
) {
|
||||
return session.authorizeCode(request.code);
|
||||
}
|
||||
|
||||
@Get('captcha')
|
||||
@SessionApiParam
|
||||
@ApiFileAcceptHeader()
|
||||
@UseInterceptors(new BufferResponseInterceptor())
|
||||
@ApiOperation({
|
||||
summary: 'Get captcha image.',
|
||||
})
|
||||
async getCaptcha(@SessionParam session: WhatsappSession): Promise<Buffer> {
|
||||
const image = await session.getCaptcha();
|
||||
return image.get();
|
||||
}
|
||||
|
||||
@Post('captcha')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Enter captcha code.',
|
||||
})
|
||||
saveCaptcha(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() body: CaptchaBody,
|
||||
) {
|
||||
return session.saveCaptcha(body.code);
|
||||
}
|
||||
}
|
||||
|
||||
export { AuthController };
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Post,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { RejectCallRequest } from '../structures/calls.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/calls')
|
||||
@ApiTags('📞 Calls')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class CallsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('reject')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Reject incoming call' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
rejectCall(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: RejectCallRequest,
|
||||
) {
|
||||
return session.rejectCall(request.from, request.id);
|
||||
}
|
||||
}
|
||||
+128
-19
@@ -3,37 +3,65 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
|
||||
import { isJidNewsletter } from '@waha/core/utils/jids';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import {
|
||||
Channel,
|
||||
ChannelCategory,
|
||||
ChannelCountry,
|
||||
ChannelListResult,
|
||||
ChannelMessage,
|
||||
ChannelSearchByText,
|
||||
ChannelSearchByView,
|
||||
ChannelView,
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
NewsletterIdApiParam,
|
||||
NewsletterIdOrInviteCodeApiParam,
|
||||
PreviewChannelMessages,
|
||||
} from '@waha/structures/channels.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { isNewsletter, WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
parseChannelInviteLink,
|
||||
WhatsappSession,
|
||||
} from '../core/abc/session.abc';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/channels')
|
||||
@ApiTags('📢 Channels')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class ChannelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
private channelsInfoService: ChannelsInfoServiceCore,
|
||||
) {}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get list of know channels' })
|
||||
async list(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() query: ListChannelsQuery,
|
||||
): Promise<Channel[]> {
|
||||
return session.channelsList(query);
|
||||
@@ -43,7 +71,7 @@ export class ChannelsController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Create a new channel.' })
|
||||
create(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: CreateChannelRequest,
|
||||
): Promise<Channel> {
|
||||
return session.channelsCreateChannel(request);
|
||||
@@ -53,7 +81,10 @@ export class ChannelsController {
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Delete the channel.' })
|
||||
delete(@SessionParam session: WhatsappSession, @Param('id') id: string) {
|
||||
delete(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.channelsDeleteChannel(id);
|
||||
}
|
||||
|
||||
@@ -67,59 +98,137 @@ export class ChannelsController {
|
||||
'OR invite code (https://www.whatsapp.com/channel/123)',
|
||||
})
|
||||
get(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<Channel> {
|
||||
if (isNewsletter(id)) {
|
||||
if (isJidNewsletter(id)) {
|
||||
return session.channelsGetChannel(id);
|
||||
} else {
|
||||
// https://www.whatsapp.com/channel/123 => 123
|
||||
const inviteCode = id.split('/').pop();
|
||||
const inviteCode = parseChannelInviteLink(id);
|
||||
return session.channelsGetChannelByInviteCode(inviteCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('follow')
|
||||
@Get(':id/messages/preview')
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'Channel id or invite code',
|
||||
required: true,
|
||||
type: 'string',
|
||||
schema: {
|
||||
default: '0029Va4K0PZ5a245NkngBA2M',
|
||||
},
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: 'Preview channel messages',
|
||||
description:
|
||||
'You can use either ' +
|
||||
'invite code (https://www.whatsapp.com/channel/123) or (123)' +
|
||||
'OR' +
|
||||
'Channel ID (123@newsletter).',
|
||||
})
|
||||
async previewChannelMessages(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') codeOrId: string,
|
||||
@Query() query: PreviewChannelMessages,
|
||||
): Promise<ChannelMessage[]> {
|
||||
let code = null;
|
||||
if (isJidNewsletter(codeOrId)) {
|
||||
const channel = await session.channelsGetChannel(codeOrId);
|
||||
code = parseChannelInviteLink(channel.invite);
|
||||
}
|
||||
const inviteCode = parseChannelInviteLink(code || codeOrId);
|
||||
return session.previewChannelMessages(inviteCode, query);
|
||||
}
|
||||
|
||||
@Post(':id/follow')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Follow the channel.' })
|
||||
follow(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<void> {
|
||||
return session.channelsFollowChannel(id);
|
||||
}
|
||||
|
||||
@Post('unfollow')
|
||||
@Post(':id/unfollow')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Unfollow the channel.' })
|
||||
unfollow(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<void> {
|
||||
return session.channelsUnfollowChannel(id);
|
||||
}
|
||||
|
||||
@Post('mute')
|
||||
@Post(':id/mute')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Mute the channel.' })
|
||||
mute(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<void> {
|
||||
return session.channelsMuteChannel(id);
|
||||
}
|
||||
|
||||
@Post('unmute')
|
||||
@Post(':id/unmute')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Unmute the channel.' })
|
||||
unmute(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<void> {
|
||||
return session.channelsUnmuteChannel(id);
|
||||
}
|
||||
|
||||
@Post('/search/by-view')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiOperation({ summary: 'Search for channels (by view)' })
|
||||
async searchByView(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ChannelSearchByView,
|
||||
): Promise<ChannelListResult> {
|
||||
return session.searchChannelsByView(request);
|
||||
}
|
||||
|
||||
@Post('/search/by-text')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiOperation({ summary: 'Search for channels (by text)' })
|
||||
async searchByText(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ChannelSearchByText,
|
||||
): Promise<ChannelListResult> {
|
||||
return session.searchChannelsByText(request);
|
||||
}
|
||||
|
||||
@Get('/search/views')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get list of views for channel search' })
|
||||
getSearchViews(): Promise<ChannelView[]> {
|
||||
return this.channelsInfoService.getViews();
|
||||
}
|
||||
|
||||
@Get('/search/countries')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get list of countries for channel search' })
|
||||
getSearchCountries(): Promise<ChannelCountry[]> {
|
||||
return this.channelsInfoService.getCountries();
|
||||
}
|
||||
|
||||
@Get('/search/categories')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get list of categories for channel search' })
|
||||
getSearchCategories(): Promise<ChannelCategory[]> {
|
||||
return this.channelsInfoService.getCategories();
|
||||
}
|
||||
}
|
||||
+157
-15
@@ -3,10 +3,12 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -15,19 +17,42 @@ import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import { MessageIdApiParam } from '@waha/nestjs/params/MessageIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseBool } from '../helpers';
|
||||
import { GetChatMessagesQuery, GetChatsQuery } from '../structures/chats.dto';
|
||||
import {
|
||||
ChatPictureQuery,
|
||||
ChatPictureResponse,
|
||||
ChatSummary,
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
GetChatsOverviewParams,
|
||||
GetChatsParams,
|
||||
MessageSortField,
|
||||
OverviewBodyRequest,
|
||||
OverviewFilter,
|
||||
PinMessageRequest,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
transformAck,
|
||||
} from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
import { SortOrder } from '@waha/structures/pagination.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/chats')
|
||||
@ApiTags('💬 Chats')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
class ChatsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -35,10 +60,39 @@ class ChatsController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get chats' })
|
||||
getChats(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Query() query: GetChatsQuery,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: GetChatsParams,
|
||||
) {
|
||||
return session.getChats(query);
|
||||
return session.getChats(pagination);
|
||||
}
|
||||
|
||||
@Get('overview')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Get chats overview. Includes all necessary things to build UI "your chats overview" page - chat id, name, picture, last message. Sorting by last message timestamp',
|
||||
})
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
getChatsOverview(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: GetChatsOverviewParams,
|
||||
@Query() filter: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
return session.getChatsOverview(pagination, filter);
|
||||
}
|
||||
|
||||
@Post('overview')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Get chats overview. Use POST if you have too many "ids" params - GET can limit it',
|
||||
})
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
postChatsOverview(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() body: OverviewBodyRequest,
|
||||
): Promise<ChatSummary[]> {
|
||||
return session.getChatsOverview(body.pagination, body.filter);
|
||||
}
|
||||
|
||||
@Delete(':chatId')
|
||||
@@ -46,23 +100,100 @@ class ChatsController {
|
||||
@ApiOperation({ summary: 'Deletes the chat' })
|
||||
@ChatIdApiParam
|
||||
deleteChat(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
return session.deleteChat(chatId);
|
||||
}
|
||||
|
||||
@Get(':chatId/picture')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets chat picture' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getChatPicture(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Query() query: ChatPictureQuery,
|
||||
): Promise<ChatPictureResponse> {
|
||||
const url = await session.getContactProfilePicture(chatId, query.refresh);
|
||||
return { url: url };
|
||||
}
|
||||
|
||||
@Get(':chatId/messages')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets messages in the chat' })
|
||||
@ChatIdApiParam
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
getChatMessages(
|
||||
@Query() query: GetChatMessagesQuery,
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Query() filter: GetChatMessagesFilter,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
const downloadMedia = parseBool(query.downloadMedia);
|
||||
return session.getChatMessages(chatId, query.limit, downloadMedia);
|
||||
if (query.sortBy == MessageSortField.MESSAGE_TIMESTAMP) {
|
||||
query.sortBy = MessageSortField.TIMESTAMP;
|
||||
}
|
||||
query.sortBy = query.sortBy || MessageSortField.TIMESTAMP;
|
||||
query.sortOrder = query.sortOrder || SortOrder.DESC;
|
||||
filter = transformAck(filter);
|
||||
return session.getChatMessages(chatId, query, filter);
|
||||
}
|
||||
|
||||
@Post(':chatId/messages/read')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Read unread messages in the chat' })
|
||||
@ChatIdApiParam
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
readChatMessages(
|
||||
@Query() query: ReadChatMessagesQuery,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
): Promise<ReadChatMessagesResponse> {
|
||||
return session.readChatMessages(chatId, query);
|
||||
}
|
||||
|
||||
@Get(':chatId/messages/:messageId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets message by id' })
|
||||
@ChatIdApiParam
|
||||
async getChatMessage(
|
||||
@Query() query: GetChatMessageQuery,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
) {
|
||||
const message = await session.getChatMessage(chatId, messageId, query);
|
||||
if (!message) {
|
||||
throw new NotFoundException('Message not found');
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
@Post(':chatId/messages/:messageId/pin')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Pins a message in the chat' })
|
||||
@ChatIdApiParam
|
||||
async pinMessage(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
@Body() body: PinMessageRequest,
|
||||
) {
|
||||
const result = await session.pinMessage(chatId, messageId, body.duration);
|
||||
return { success: result };
|
||||
}
|
||||
|
||||
@Post(':chatId/messages/:messageId/unpin')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Unpins a message in the chat' })
|
||||
@ChatIdApiParam
|
||||
async unpinMessage(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
) {
|
||||
const result = await session.unpinMessage(chatId, messageId);
|
||||
return { success: result };
|
||||
}
|
||||
|
||||
@Delete(':chatId/messages')
|
||||
@@ -70,7 +201,7 @@ class ChatsController {
|
||||
@ApiOperation({ summary: 'Clears all messages from the chat' })
|
||||
@ChatIdApiParam
|
||||
clearMessages(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
return session.clearMessages(chatId);
|
||||
@@ -82,7 +213,7 @@ class ChatsController {
|
||||
@MessageIdApiParam
|
||||
@ApiOperation({ summary: 'Deletes a message from the chat' })
|
||||
deleteMessage(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
) {
|
||||
@@ -95,7 +226,7 @@ class ChatsController {
|
||||
@MessageIdApiParam
|
||||
@ApiOperation({ summary: 'Edits a message in the chat' })
|
||||
editMessage(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
@Body() body: EditMessageRequest,
|
||||
@@ -108,7 +239,7 @@ class ChatsController {
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Archive the chat' })
|
||||
archiveChat(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
return session.chatsArchiveChat(chatId);
|
||||
@@ -119,11 +250,22 @@ class ChatsController {
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Unarchive the chat' })
|
||||
unarchiveChat(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
return session.chatsUnarchiveChat(chatId);
|
||||
}
|
||||
|
||||
@Post(':chatId/unread')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Unread the chat' })
|
||||
unreadChat(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
return session.chatsUnreadChat(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
export { ChatsController };
|
||||
+202
-40
@@ -1,17 +1,39 @@
|
||||
import { Body, Controller, Get, Post, Put, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
ReadChatMessagesQuery,
|
||||
transformAck,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
import { SendListRequest } from '@waha/structures/chatting.list.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
GetMessageQuery,
|
||||
MessageButtonReply,
|
||||
MessageContactVcardRequest,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLinkCustomPreviewRequest,
|
||||
MessageLinkPreviewRequest,
|
||||
MessageLocationRequest,
|
||||
MessagePollRequest,
|
||||
MessagePollVoteRequest,
|
||||
MessageReactionRequest,
|
||||
MessageReplyRequest,
|
||||
MessageStarRequest,
|
||||
@@ -23,17 +45,32 @@ import {
|
||||
WANumberExistResult,
|
||||
} from '../structures/chatting.dto';
|
||||
import { WAMessage } from '../structures/responses.dto';
|
||||
import {
|
||||
mentionsAll,
|
||||
validateRequestMentions,
|
||||
} from '@waha/core/utils/mentions.all';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromBody, FromQuery } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@ApiTags('📤 Chatting')
|
||||
@UseGuards(PoliciesGuard)
|
||||
export class ChattingController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message' })
|
||||
sendText(@Body() request: MessageTextRequest): Promise<WAMessage> {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendText(@Body() request: MessageTextRequest): Promise<WAMessage> {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
validateRequestMentions(request);
|
||||
request.mentions = await whatsapp.resolveMentionsAll(request.chatId);
|
||||
}
|
||||
return whatsapp.sendText(request);
|
||||
}
|
||||
|
||||
@@ -43,8 +80,13 @@ export class ChattingController {
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
sendImage(@Body() request: MessageImageRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendImage(@Body() request: MessageImageRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
validateRequestMentions(request);
|
||||
request.mentions = await whatsapp.resolveMentionsAll(request.chatId);
|
||||
}
|
||||
return whatsapp.sendImage(request);
|
||||
}
|
||||
|
||||
@@ -54,8 +96,13 @@ export class ChattingController {
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
sendFile(@Body() request: MessageFileRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendFile(@Body() request: MessageFileRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
validateRequestMentions(request);
|
||||
request.mentions = await whatsapp.resolveMentionsAll(request.chatId);
|
||||
}
|
||||
return whatsapp.sendFile(request);
|
||||
}
|
||||
|
||||
@@ -65,8 +112,9 @@ export class ChattingController {
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
sendVoice(@Body() request: MessageVoiceRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendVoice(@Body() request: MessageVoiceRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendVoice(request);
|
||||
}
|
||||
|
||||
@@ -76,43 +124,116 @@ export class ChattingController {
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
sendVideo(@Body() request: MessageVideoRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendVideo(@Body() request: MessageVideoRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
validateRequestMentions(request);
|
||||
request.mentions = await whatsapp.resolveMentionsAll(request.chatId);
|
||||
}
|
||||
return whatsapp.sendVideo(request);
|
||||
}
|
||||
|
||||
@Post('/send/link-custom-preview')
|
||||
@ApiOperation({
|
||||
summary: 'Send a text message with a CUSTOM link preview.',
|
||||
description:
|
||||
'You can use regular /api/sendText if you wanna send auto-generated link preview.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendLinkCustomPreview(
|
||||
@Body() request: MessageLinkCustomPreviewRequest,
|
||||
): Promise<any> {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (!request.text.includes(request.preview.url)) {
|
||||
throw new Error(
|
||||
'"text" must include the URL provided in the "preview.url"',
|
||||
);
|
||||
}
|
||||
return whatsapp.sendLinkCustomPreview(request);
|
||||
}
|
||||
|
||||
@Post('/sendButtons')
|
||||
@ApiOperation({
|
||||
summary: 'Send buttons message (interactive)',
|
||||
description: 'Send Buttons',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendButtons(@Body() request: SendButtonsRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendButtons(request);
|
||||
}
|
||||
|
||||
@Post('/sendList')
|
||||
@ApiOperation({
|
||||
summary: 'Send a list message (interactive)',
|
||||
description: 'Send a List message with sections and rows',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendList(@Body() request: SendListRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendList(request);
|
||||
}
|
||||
|
||||
@Post('/forwardMessage')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async forwardMessage(
|
||||
@Body() request: MessageForwardRequest,
|
||||
): Promise<WAMessage> {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return await whatsapp.forwardMessage(request);
|
||||
}
|
||||
|
||||
@Post('/sendSeen')
|
||||
sendSeen(@Body() chat: SendSeenRequest) {
|
||||
const whatsapp = this.manager.getSession(chat.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendSeen(@Body() chat: SendSeenRequest) {
|
||||
const hasMessageId = chat.messageIds?.length > 0 || Boolean(chat.messageId);
|
||||
if (!hasMessageId) {
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
const query: ReadChatMessagesQuery = {
|
||||
messages: null,
|
||||
days: 7,
|
||||
};
|
||||
return whatsapp.readChatMessages(chat.chatId, query);
|
||||
}
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
return whatsapp.sendSeen(chat);
|
||||
}
|
||||
|
||||
@Post('/startTyping')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async startTyping(@Body() chat: ChatRequest) {
|
||||
// It's infinitive action
|
||||
const whatsapp = this.manager.getSession(chat.session);
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
await whatsapp.startTyping(chat);
|
||||
return { result: true };
|
||||
}
|
||||
|
||||
@Post('/stopTyping')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async stopTyping(@Body() chat: ChatRequest) {
|
||||
const whatsapp = this.manager.getSession(chat.session);
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
await whatsapp.stopTyping(chat);
|
||||
return { result: true };
|
||||
}
|
||||
|
||||
@Put('/reaction')
|
||||
@ApiOperation({ summary: 'React to a message with an emoji' })
|
||||
setReaction(@Body() request: MessageReactionRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async setReaction(@Body() request: MessageReactionRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.setReaction(request);
|
||||
}
|
||||
|
||||
@Put('/star')
|
||||
@ApiOperation({ summary: 'Star or unstar a message' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async setStar(@Body() request: MessageStarRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
await whatsapp.setStar(request);
|
||||
return;
|
||||
}
|
||||
@@ -122,44 +243,75 @@ export class ChattingController {
|
||||
summary: 'Send a poll with options',
|
||||
description: 'You can use it as buttons or list replacement',
|
||||
})
|
||||
sendPoll(@Body() request: MessagePollRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendPoll(@Body() request: MessagePollRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendPoll(request);
|
||||
}
|
||||
|
||||
@Post('/sendPollVote')
|
||||
@ApiOperation({
|
||||
summary: 'Vote on a poll',
|
||||
description: 'Cast vote(s) on an existing poll message',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendPollVote(@Body() request: MessagePollVoteRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendPollVote(request);
|
||||
}
|
||||
|
||||
@Post('/sendLocation')
|
||||
sendLocation(@Body() request: MessageLocationRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendLocation(@Body() request: MessageLocationRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendLocation(request);
|
||||
}
|
||||
|
||||
@Post('/sendLinkPreview')
|
||||
sendLinkPreview(@Body() request: MessageLinkPreviewRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendLinkPreview(request);
|
||||
@Post('/sendContactVcard')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendContactVCard(request);
|
||||
}
|
||||
|
||||
@Get('/messages')
|
||||
@ApiOperation({ summary: 'Get messages in a chat' })
|
||||
getMessages(@Query() query: GetMessageQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
return whatsapp.getMessages(query);
|
||||
@Post('/send/buttons/reply')
|
||||
@ApiOperation({
|
||||
summary: 'Reply on a button message',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async sendButtonsReply(@Body() request: MessageButtonReply) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendButtonsReply(request);
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message', deprecated: true })
|
||||
sendTextGet(@Query() query: MessageTextQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async sendTextGet(@Query() query: MessageTextQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
const msg = new MessageTextRequest();
|
||||
msg.chatId = query.phone;
|
||||
msg.text = query.text;
|
||||
return whatsapp.sendText(msg);
|
||||
}
|
||||
|
||||
@Post('/sendContactVcard')
|
||||
sendContactVcard(@Body() request: MessageContactVcardRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendContactVCard(request);
|
||||
@Get('/messages')
|
||||
@ApiOperation({
|
||||
summary: 'Get messages in a chat',
|
||||
description: 'DEPRECATED. Use "GET /api/chats/{id}/messages" instead',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getMessages(
|
||||
@Query() query: GetMessageQuery,
|
||||
@Query() filter: GetChatMessagesFilter,
|
||||
) {
|
||||
filter = transformAck(filter);
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getChatMessages(query.chatId, query, filter);
|
||||
}
|
||||
|
||||
@Get('/checkNumberStatus')
|
||||
@@ -168,10 +320,11 @@ export class ChattingController {
|
||||
description: 'DEPRECATED. Use "POST /contacts/check-exists" instead',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async DEPRECATED_checkNumberStatus(
|
||||
@Query() request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.checkNumberStatus(request);
|
||||
}
|
||||
|
||||
@@ -181,8 +334,17 @@ export class ChattingController {
|
||||
'DEPRECATED - you can set "reply_to" field when sending text, image, etc',
|
||||
deprecated: true,
|
||||
})
|
||||
reply(@Body() request: MessageReplyRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async reply(@Body() request: MessageReplyRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.reply(request);
|
||||
}
|
||||
|
||||
@Post('/sendLinkPreview')
|
||||
@ApiOperation({ deprecated: true })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendLinkPreview_DEPRECATED(@Body() request: MessageLinkPreviewRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendLinkPreview(request);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
@@ -7,38 +16,56 @@ import {
|
||||
CheckNumberStatusQuery,
|
||||
WANumberExistResult,
|
||||
} from '../structures/chatting.dto';
|
||||
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
|
||||
import {
|
||||
ContactProfilePictureQuery,
|
||||
ContactQuery,
|
||||
ContactRequest,
|
||||
ContactsPaginationParams,
|
||||
} from '../structures/contacts.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromBody, FromQuery } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/contacts')
|
||||
@ApiTags('👤 Contacts')
|
||||
@UseGuards(PoliciesGuard)
|
||||
export class ContactsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/all')
|
||||
@ApiOperation({ summary: 'Get all contacts' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getAll(
|
||||
@Query() query: SessionQuery,
|
||||
@Query() pagination: ContactsPaginationParams,
|
||||
) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getContacts(pagination);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({
|
||||
summary: 'Get contact basic info',
|
||||
description:
|
||||
'The method always return result, even if the phone number is not registered in WhatsApp. For that - use /contacts/check-exists endpoint below.',
|
||||
})
|
||||
get(@Query() query: ContactQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async get(@Query() query: ContactQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getContact(query);
|
||||
}
|
||||
|
||||
@Get('/all')
|
||||
@ApiOperation({ summary: 'Get all contacts' })
|
||||
getAll(@Query() query: SessionQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
return whatsapp.getContacts();
|
||||
}
|
||||
|
||||
@Get('/check-exists')
|
||||
@ApiOperation({ summary: 'Check phone number is registered in WhatsApp.' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async checkExists(
|
||||
@Query() request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.checkNumberStatus(request);
|
||||
}
|
||||
|
||||
@@ -48,8 +75,9 @@ export class ContactsController {
|
||||
description:
|
||||
'Returns null if you do not have permission to read their status.',
|
||||
})
|
||||
getAbout(@Query() query: ContactQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async getAbout(@Query() query: ContactQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getContactAbout(query);
|
||||
}
|
||||
|
||||
@@ -59,22 +87,30 @@ export class ContactsController {
|
||||
description:
|
||||
'If privacy settings do not allow to get the picture, the method will return null.',
|
||||
})
|
||||
getProfilePicture(@Query() query: ContactQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
return whatsapp.getContactProfilePicture(query);
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getProfilePicture(@Query() query: ContactProfilePictureQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
const url = await whatsapp.getContactProfilePicture(
|
||||
query.contactId,
|
||||
query.refresh,
|
||||
);
|
||||
return { profilePictureURL: url };
|
||||
}
|
||||
|
||||
@Post('/block')
|
||||
@ApiOperation({ summary: 'Block contact' })
|
||||
block(@Body() request: ContactRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async block(@Body() request: ContactRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.blockContact(request);
|
||||
}
|
||||
|
||||
@Post('/unblock')
|
||||
@ApiOperation({ summary: 'Unblock contact' })
|
||||
unblock(@Body() request: ContactRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async unblock(@Body() request: ContactRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.unblockContact(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Put,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { Result } from '../structures/base.dto';
|
||||
import { ContactUpdateBody } from '../structures/contacts.dto';
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/contacts')
|
||||
@ApiTags('👤 Contacts')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class ContactsSessionController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/:id')
|
||||
@SessionApiParam
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Contact ID',
|
||||
example: '123456789@c.us',
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: 'Get contact basic info',
|
||||
description:
|
||||
'The method always return result, even if the phone number is not registered in WhatsApp. For that - use /contacts/check-exists endpoint below.',
|
||||
})
|
||||
async get(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.getContact({ session: session.name, contactId: id });
|
||||
}
|
||||
|
||||
@Put('/:chatId')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Create or update contact',
|
||||
description:
|
||||
'Create or update contact on the phone address book. May not work if you have installed many WhatsApp apps on the same phone',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async put(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Body() body: ContactUpdateBody,
|
||||
): Promise<Result> {
|
||||
await session.upsertContact(chatId, body);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import {
|
||||
EventCancelRequest,
|
||||
EventMessageRequest,
|
||||
} from '../structures/events.dto';
|
||||
import { WAMessage } from '../structures/responses.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/events')
|
||||
@ApiTags('📅 Events')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class EventsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Send an event message' })
|
||||
@UsePipes(new ValidationPipe())
|
||||
@SessionApiParam
|
||||
async sendEvent(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: EventMessageRequest,
|
||||
): Promise<WAMessage> {
|
||||
return session.sendEvent(request);
|
||||
}
|
||||
|
||||
// @Post(':id/cancel')
|
||||
// @ApiOperation({ summary: 'Cancel an event by ID' })
|
||||
// @UsePipes(new ValidationPipe())
|
||||
// @SessionApiParam
|
||||
// async cancelEvent(
|
||||
// @WorkingSessionParam session: WhatsappSession,
|
||||
// @Param('id') id: string,
|
||||
// ): Promise<WAMessage> {
|
||||
// return session.cancelEvent(id);
|
||||
// }
|
||||
}
|
||||
+222
-78
@@ -3,30 +3,55 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { GroupIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { CountResponse, Result } from '@waha/structures/base.dto';
|
||||
import {
|
||||
ChatPictureQuery,
|
||||
ChatPictureResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { ProfilePictureRequest } from '@waha/structures/profile.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseGroupInviteLink, WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
DescriptionRequest,
|
||||
GroupField,
|
||||
GroupParticipant,
|
||||
GroupsListFields,
|
||||
GroupsPaginationParams,
|
||||
JoinGroupRequest,
|
||||
JoinGroupResponse,
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
SubjectRequest,
|
||||
} from '../structures/groups.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/groups')
|
||||
@ApiTags('👥 Groups')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class GroupsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -34,104 +59,146 @@ export class GroupsController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Create a new group.' })
|
||||
createGroup(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: CreateGroupRequest,
|
||||
) {
|
||||
return session.createGroup(request);
|
||||
}
|
||||
|
||||
@Get('join-info')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get info about the group before joining.' })
|
||||
async joinInfoGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() query: JoinGroupRequest,
|
||||
): Promise<any> {
|
||||
const code = parseGroupInviteLink(query.code);
|
||||
return session.joinInfoGroup(code);
|
||||
}
|
||||
|
||||
@Post('join')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Join group via code' })
|
||||
async joinGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: JoinGroupRequest,
|
||||
): Promise<JoinGroupResponse> {
|
||||
const code = parseGroupInviteLink(request.code);
|
||||
const id = await session.joinGroup(code);
|
||||
return { id: id };
|
||||
}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all groups.' })
|
||||
getGroups(@SessionParam session: WhatsappSession) {
|
||||
return session.getGroups();
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getGroups(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: GroupsPaginationParams,
|
||||
@Query() fields: GroupsListFields,
|
||||
) {
|
||||
let groups: any = await session.getGroups(pagination);
|
||||
groups = session.filterGroupsFields(groups, fields);
|
||||
return groups;
|
||||
}
|
||||
|
||||
@Get('/count')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get the number of groups.' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getGroupsCount(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<CountResponse> {
|
||||
const data: any = await session.getGroups({});
|
||||
const groups: any[] = Array.isArray(data) ? data : Object.values(data);
|
||||
return {
|
||||
count: groups.length,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Refresh groups from the server.' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async refreshGroups(@WorkingSessionParam session: WhatsappSession) {
|
||||
return { success: await session.refreshGroups() };
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@GroupIdApiParam
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get the group.' })
|
||||
getGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
|
||||
getGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.getGroup(id);
|
||||
}
|
||||
|
||||
@Put(':id/settings/security/info-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Updates the group "info admin only" settings.',
|
||||
description:
|
||||
'You can allow only admins to edit group info (title, description, photo).',
|
||||
})
|
||||
setInfoAdminOnly(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SettingsSecurityChangeInfo,
|
||||
) {
|
||||
return session.setInfoAdminsOnly(id, request.adminsOnly);
|
||||
}
|
||||
|
||||
@Get(':id/settings/security/info-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: "Get the group's 'info admin only' settings.",
|
||||
description:
|
||||
'You can allow only admins to edit group info (title, description, photo).',
|
||||
})
|
||||
getInfoAdminOnly(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<SettingsSecurityChangeInfo> {
|
||||
return session.getInfoAdminsOnly(id);
|
||||
}
|
||||
|
||||
@Put(':id/settings/security/messages-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Update settings - who can send messages',
|
||||
description:
|
||||
'Updates the group settings to only allow admins to send messages.',
|
||||
})
|
||||
setMessagesAdminOnly(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SettingsSecurityChangeInfo,
|
||||
) {
|
||||
return session.setMessagesAdminsOnly(id, request.adminsOnly);
|
||||
}
|
||||
|
||||
@Get(':id/settings/security/messages-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Get settings - who can send messages',
|
||||
description: 'The group settings to only allow admins to send messages.',
|
||||
})
|
||||
getMessagesAdminOnly(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<SettingsSecurityChangeInfo> {
|
||||
return session.getMessagesAdminsOnly(id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Delete the group.' })
|
||||
deleteGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
|
||||
deleteGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.deleteGroup(id);
|
||||
}
|
||||
|
||||
@Post(':id/leave')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Leave the group.' })
|
||||
leaveGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
|
||||
leaveGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.leaveGroup(id);
|
||||
}
|
||||
|
||||
@Get(':id/picture')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Get group picture' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getChatPicture(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Query() query: ChatPictureQuery,
|
||||
): Promise<ChatPictureResponse> {
|
||||
const url = await session.getContactProfilePicture(id, query.refresh);
|
||||
return { url: url };
|
||||
}
|
||||
|
||||
@Put(':id/picture')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Set group picture' })
|
||||
async setPicture(
|
||||
@Param('id') id: string,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ProfilePictureRequest,
|
||||
): Promise<Result> {
|
||||
const success = await session.updateGroupPicture(id, request.file);
|
||||
return { success: success ?? true };
|
||||
}
|
||||
|
||||
@Delete(':id/picture')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Delete group picture' })
|
||||
async deletePicture(
|
||||
@Param('id') id: string,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<Result> {
|
||||
const success = await session.updateGroupPicture(id, null);
|
||||
return { success: success || true };
|
||||
}
|
||||
|
||||
@Put(':id/description')
|
||||
@ApiOperation({
|
||||
summary: 'Updates the group description.',
|
||||
@@ -141,7 +208,7 @@ export class GroupsController {
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
setDescription(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: DescriptionRequest,
|
||||
) {
|
||||
@@ -157,25 +224,87 @@ export class GroupsController {
|
||||
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
|
||||
})
|
||||
setSubject(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SubjectRequest,
|
||||
) {
|
||||
return session.setSubject(id, request.subject);
|
||||
}
|
||||
|
||||
@Put(':id/settings/security/info-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Updates the group "info admin only" settings.',
|
||||
description:
|
||||
'You can allow only admins to edit group info (title, description, photo).',
|
||||
})
|
||||
setInfoAdminOnly(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SettingsSecurityChangeInfo,
|
||||
) {
|
||||
return session.setInfoAdminsOnly(id, request.adminsOnly);
|
||||
}
|
||||
|
||||
@Get(':id/settings/security/info-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: "Get the group's 'info admin only' settings.",
|
||||
description:
|
||||
'You can allow only admins to edit group info (title, description, photo).',
|
||||
})
|
||||
getInfoAdminOnly(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<SettingsSecurityChangeInfo> {
|
||||
return session.getInfoAdminsOnly(id);
|
||||
}
|
||||
|
||||
@Put(':id/settings/security/messages-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Update settings - who can send messages',
|
||||
description:
|
||||
'Updates the group settings to only allow admins to send messages.',
|
||||
})
|
||||
setMessagesAdminOnly(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SettingsSecurityChangeInfo,
|
||||
) {
|
||||
return session.setMessagesAdminsOnly(id, request.adminsOnly);
|
||||
}
|
||||
|
||||
@Get(':id/settings/security/messages-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Get settings - who can send messages',
|
||||
description: 'The group settings to only allow admins to send messages.',
|
||||
})
|
||||
getMessagesAdminOnly(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<SettingsSecurityChangeInfo> {
|
||||
return session.getMessagesAdminsOnly(id);
|
||||
}
|
||||
|
||||
@Get(':id/invite-code')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Gets the invite code for the group.' })
|
||||
getInviteCode(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<string> {
|
||||
return session.getInviteCode(id);
|
||||
}
|
||||
|
||||
@Post(':id/invite-code/revoke')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
@@ -183,7 +312,7 @@ export class GroupsController {
|
||||
'Invalidates the current group invite code and generates a new one.',
|
||||
})
|
||||
revokeInviteCode(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<string> {
|
||||
return session.revokeInviteCode(id);
|
||||
@@ -194,18 +323,30 @@ export class GroupsController {
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Get participants' })
|
||||
getParticipants(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.getParticipants(id);
|
||||
}
|
||||
|
||||
@Get(':id/participants/v2')
|
||||
@GroupIdApiParam
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get group participants.' })
|
||||
getGroupParticipants(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<GroupParticipant[]> {
|
||||
return session.getGroupParticipants(id);
|
||||
}
|
||||
|
||||
@Post(':id/participants/add')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Add participants' })
|
||||
addParticipants(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: ParticipantsRequest,
|
||||
) {
|
||||
@@ -213,13 +354,14 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Post(':id/participants/remove')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Remove participants',
|
||||
})
|
||||
removeParticipants(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: ParticipantsRequest,
|
||||
) {
|
||||
@@ -227,11 +369,12 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Post(':id/admin/promote')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Promote participants to admin users.' })
|
||||
promoteToAdmin(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: ParticipantsRequest,
|
||||
) {
|
||||
@@ -239,11 +382,12 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Post(':id/admin/demote')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Demotes participants to regular users.' })
|
||||
demoteToAdmin(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: ParticipantsRequest,
|
||||
) {
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { HealthCheck } from '@nestjs/terminus';
|
||||
|
||||
import { WAHAHealthCheckService } from '../core/abc/WAHAHealthCheckService';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('health')
|
||||
@ApiTags('🔍 Observability')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
export class HealthController {
|
||||
constructor(private wahaHealth: WAHAHealthCheckService) {}
|
||||
|
||||
|
||||
@@ -1,34 +1,131 @@
|
||||
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { Label, SetLabelsRequest } from '@waha/structures/labels.dto';
|
||||
import {
|
||||
Label,
|
||||
LabelBody,
|
||||
SetLabelsRequest,
|
||||
} from '@waha/structures/labels.dto';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/labels')
|
||||
@ApiTags('🏷️ Labels')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class LabelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all labels' })
|
||||
getAll(@SessionParam session: WhatsappSession): Promise<Label[]> {
|
||||
getAll(@WorkingSessionParam session: WhatsappSession): Promise<Label[]> {
|
||||
return session.getLabels();
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Create a new label' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() body: LabelBody,
|
||||
): Promise<Label> {
|
||||
const labelDto = body.toDTO();
|
||||
|
||||
const labels = await session.getLabels();
|
||||
if (labels.length >= 20) {
|
||||
throw new UnprocessableEntityException('Maximum 20 labels allowed');
|
||||
}
|
||||
|
||||
const labelByName = lodash.find(labels, { name: labelDto.name });
|
||||
if (labelByName) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Label with '${labelByName.name}' already exists, id: ${labelByName.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return session.createLabel(labelDto);
|
||||
}
|
||||
|
||||
@Put('/:labelId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Update a label' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async update(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('labelId') labelId: string,
|
||||
@Body() body: LabelBody,
|
||||
): Promise<Label> {
|
||||
const label = await session.getLabel(labelId);
|
||||
if (!label) {
|
||||
throw new NotFoundException('Label not found');
|
||||
}
|
||||
|
||||
const labelDto = body.toDTO();
|
||||
const labels = await session.getLabels();
|
||||
|
||||
const labelByName = lodash.find(labels, { name: labelDto.name });
|
||||
if (labelByName) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Label with '${labelByName.name}' already exists, id: ${labelByName.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
label.name = labelDto.name;
|
||||
label.color = labelDto.color;
|
||||
label.colorHex = Label.toHex(label.color);
|
||||
await session.updateLabel(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
@Delete('/:labelId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Delete a label' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async delete(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('labelId') labelId: string,
|
||||
): Promise<any> {
|
||||
const label = await session.getLabel(labelId);
|
||||
if (!label) {
|
||||
throw new NotFoundException('Label not found');
|
||||
}
|
||||
await session.deleteLabel(label);
|
||||
return { result: true };
|
||||
}
|
||||
|
||||
@Get('/chats/:chatId')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Get labels for the chat' })
|
||||
getChatLabels(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
): Promise<Label[]> {
|
||||
return session.getChatLabels(chatId);
|
||||
@@ -39,7 +136,7 @@ export class LabelsController {
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Save labels for the chat' })
|
||||
putChatLabels(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Body() request: SetLabelsRequest,
|
||||
) {
|
||||
@@ -50,7 +147,7 @@ export class LabelsController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get chats by label' })
|
||||
getChatsByLabel(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('labelId') labelId: string,
|
||||
) {
|
||||
return session.getChatsByLabelId(labelId);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { CountResponse } from '@waha/structures/base.dto';
|
||||
import {
|
||||
LidsListQueryParams,
|
||||
LidToPhoneNumber,
|
||||
} from '@waha/structures/lids.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { isLidUser } from '@waha/core/utils/jids';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/lids')
|
||||
@ApiTags('👤 Contacts')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class LidsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all known lids to phone number mapping' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getAll(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() params: LidsListQueryParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
// Always lid
|
||||
const pagination: PaginationParams = params;
|
||||
pagination.sortBy = 'lid';
|
||||
pagination.sortOrder = SortOrder.ASC;
|
||||
const lids = await session.getAllLids(pagination);
|
||||
return lids;
|
||||
}
|
||||
|
||||
@Get('/count')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get the number of known lids' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getLidsCount(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<CountResponse> {
|
||||
const count = await session.getLidsCount();
|
||||
return {
|
||||
count: count,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/:lid')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get phone number by lid' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async findPNByLid(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('lid') lid: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
if (!lid.includes('@')) {
|
||||
lid = lid + '@lid';
|
||||
}
|
||||
|
||||
if (!isLidUser(lid)) {
|
||||
throw new UnprocessableEntityException(
|
||||
'Invalid LID - it must end with @lid',
|
||||
);
|
||||
}
|
||||
const result = await session.findPNByLid(lid);
|
||||
result.pn = result.pn || null;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('/pn/:phoneNumber')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get lid by phone number (chat id)' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async findLIDByPhoneNumber(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('phoneNumber') phoneNumber: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
if (isLidUser(phoneNumber)) {
|
||||
return {
|
||||
lid: phoneNumber,
|
||||
pn: null,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await session.findLIDByPhoneNumber(phoneNumber);
|
||||
result.lid = result.lid || null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Post,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WAMimeType } from '@waha/core/media/WAMimeType';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import {
|
||||
FileDTO,
|
||||
VideoFileDTO,
|
||||
VoiceFileDTO,
|
||||
} from '@waha/structures/media.dto';
|
||||
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/media')
|
||||
@ApiTags('🖼️ Media')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
class MediaController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('convert/voice')
|
||||
@ApiOperation({
|
||||
summary: 'Convert voice to WhatsApp format (opus)',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UseInterceptors(
|
||||
new BufferResponseInterceptor(WAMimeType.VOICE, 'output.opus'),
|
||||
)
|
||||
@ApiFileAcceptHeader(WAMimeType.VOICE)
|
||||
async convertVoice(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() file: VoiceFileDTO,
|
||||
): Promise<Buffer> {
|
||||
const data = await this.buffer(session, file);
|
||||
const content = await session.mediaConverter.voice(data);
|
||||
return content;
|
||||
}
|
||||
|
||||
@Post('convert/video')
|
||||
@ApiOperation({
|
||||
summary: 'Convert video to WhatsApp format (mp4)',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UseInterceptors(
|
||||
new BufferResponseInterceptor(WAMimeType.VIDEO, 'output.mp4'),
|
||||
)
|
||||
@ApiFileAcceptHeader(WAMimeType.VIDEO)
|
||||
async convertVideo(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() file: VideoFileDTO,
|
||||
): Promise<Buffer> {
|
||||
const data = await this.buffer(session, file);
|
||||
const content = await session.mediaConverter.video(data);
|
||||
return content;
|
||||
}
|
||||
|
||||
private async buffer(
|
||||
session: WhatsappSession,
|
||||
file: FileDTO,
|
||||
): Promise<Buffer> {
|
||||
if ('url' in file) {
|
||||
return session.fetch(file.url);
|
||||
} else if ('data' in file) {
|
||||
return Buffer.from(file.data, 'base64');
|
||||
} else {
|
||||
throw new UnprocessableEntityException(
|
||||
'Either "url" or "data" must be specified.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MediaController };
|
||||
@@ -2,7 +2,6 @@ import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { PingResponse } from '@waha/structures/ping.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('ping')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class PingController {
|
||||
|
||||
@@ -5,12 +5,13 @@ import {
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
@@ -20,10 +21,17 @@ import {
|
||||
WAHAChatPresences,
|
||||
WAHASessionPresence,
|
||||
} from '../structures/presence.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/presence')
|
||||
@ApiTags('✅ Presence')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class PresenceController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -31,7 +39,7 @@ export class PresenceController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Set session presence' })
|
||||
setPresence(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: WAHASessionPresence,
|
||||
) {
|
||||
// Validate request
|
||||
@@ -61,7 +69,7 @@ export class PresenceController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all subscribed presence information.' })
|
||||
getPresenceAll(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<WAHAChatPresences[]> {
|
||||
return session.getPresences();
|
||||
}
|
||||
@@ -74,7 +82,7 @@ export class PresenceController {
|
||||
"Get the presence for the chat id. If it hasn't been subscribed - it also subscribes to it.",
|
||||
})
|
||||
getPresence(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
): Promise<WAHAChatPresences> {
|
||||
return session.getPresence(chatId);
|
||||
@@ -87,7 +95,7 @@ export class PresenceController {
|
||||
summary: 'Subscribe to presence events for the chat.',
|
||||
})
|
||||
subscribe(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
): Promise<void> {
|
||||
return session.subscribePresence(chatId);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Put,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { Result } from '@waha/structures/base.dto';
|
||||
import {
|
||||
MyProfile,
|
||||
ProfileNameRequest,
|
||||
ProfilePictureRequest,
|
||||
ProfileStatusRequest,
|
||||
} from '@waha/structures/profile.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/profile')
|
||||
@ApiTags('🆔 Profile')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class ProfileController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get my profile' })
|
||||
async getMyProfile(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<MyProfile> {
|
||||
const me = session.getSessionMeInfo();
|
||||
if (!me) {
|
||||
throw new UnprocessableEntityException('No profile found');
|
||||
}
|
||||
const picture = await session.getContactProfilePicture(me.id, false);
|
||||
return {
|
||||
id: me.id,
|
||||
name: me.pushName,
|
||||
picture: picture || null,
|
||||
};
|
||||
}
|
||||
|
||||
@Put('/name')
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiOperation({ summary: 'Set my profile name' })
|
||||
async setProfileName(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ProfileNameRequest,
|
||||
): Promise<Result> {
|
||||
const success = await session.setProfileName(request.name);
|
||||
return { success: success ?? true };
|
||||
}
|
||||
|
||||
@Put('/status')
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiOperation({ summary: 'Set profile status (About)' })
|
||||
async setProfileStatus(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ProfileStatusRequest,
|
||||
): Promise<Result> {
|
||||
const success = await session.setProfileStatus(request.status);
|
||||
return { success: success ?? true };
|
||||
}
|
||||
|
||||
@Put('/picture')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Set profile picture' })
|
||||
async setProfilePicture(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ProfilePictureRequest,
|
||||
): Promise<Result> {
|
||||
const success = await session.updateProfilePicture(request.file);
|
||||
return { success: success ?? true };
|
||||
}
|
||||
|
||||
@Delete('/picture')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Delete profile picture' })
|
||||
async deleteProfilePicture(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<Result> {
|
||||
const success = await session.updateProfilePicture(null);
|
||||
return { success: success || true };
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,37 @@ import {
|
||||
Query,
|
||||
Res,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import { SessionQuery } from '../structures/base.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromQuery } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@ApiTags('🖼️ Screenshot')
|
||||
@ApiTags('📱 Pairing')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
export class ScreenshotController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/screenshot')
|
||||
@UseInterceptors(new BufferResponseInterceptor())
|
||||
@ApiFileAcceptHeader()
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Get a screenshot of the current WhatsApp session (**WEBJS/WPP** only)',
|
||||
})
|
||||
@UseInterceptors(new BufferResponseInterceptor('image/jpeg'))
|
||||
@ApiFileAcceptHeader('image/jpeg')
|
||||
async screenshot(
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Query() sessionQuery: SessionQuery,
|
||||
|
||||
@@ -7,38 +7,49 @@ import {
|
||||
Logger,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { getApp } from '@waha/main';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { WAHAEnvironment } from '@waha/structures/environment.dto';
|
||||
import {
|
||||
EnvironmentQuery,
|
||||
ServerStatusResponse,
|
||||
StopRequest,
|
||||
StopResponse,
|
||||
} from '@waha/structures/server.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { VERSION } from '@waha/version';
|
||||
import * as lodash from 'lodash';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server')
|
||||
@ApiTags('🔍 Observability')
|
||||
@UseGuards(PoliciesGuard)
|
||||
export class ServerController {
|
||||
private logger: Logger;
|
||||
|
||||
constructor() {
|
||||
constructor(private config: WhatsappConfigService) {
|
||||
this.logger = new Logger('ServerController');
|
||||
}
|
||||
|
||||
@Get('version')
|
||||
@ApiOperation({ summary: 'Get the version of the server' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
get(): WAHAEnvironment {
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
@Get('environment')
|
||||
@ApiOperation({ summary: 'Get the server environment' })
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
environment(
|
||||
@Query(new WAHAValidationPipe()) query: EnvironmentQuery,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
@@ -65,6 +76,7 @@ export class ServerController {
|
||||
|
||||
@Get('status')
|
||||
@ApiOperation({ summary: 'Get the server status' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
async status(): Promise<ServerStatusResponse> {
|
||||
const now = Date.now();
|
||||
const uptime = Math.floor(process.uptime() * 1000);
|
||||
@@ -72,6 +84,9 @@ export class ServerController {
|
||||
return {
|
||||
startTimestamp: startTimestamp,
|
||||
uptime: uptime,
|
||||
worker: {
|
||||
id: this.config.workerId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,26 +97,26 @@ export class ServerController {
|
||||
"If you're using docker, after calling this endpoint Docker will start a new container, " +
|
||||
'so you can use this endpoint to restart the server',
|
||||
})
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async stop(@Body() request: StopRequest) {
|
||||
async stop(@Body() request: StopRequest): Promise<StopResponse> {
|
||||
const timeout = 1_000;
|
||||
if (request.force) {
|
||||
this.logger.log(`Force stopping the server in ${timeout}ms`);
|
||||
setTimeout(() => {
|
||||
this.logger.log('Force stopping the server');
|
||||
process.kill(process.pid, 'SIGKILL');
|
||||
process.exit(0);
|
||||
}, timeout);
|
||||
} else {
|
||||
this.logger.log(`Gracefully stopping the server in ${timeout}ms`);
|
||||
setTimeout(async () => {
|
||||
this.logger.log('Gracefully closing the application...');
|
||||
const app = getApp();
|
||||
if (app) {
|
||||
await app.close();
|
||||
}
|
||||
this.logger.log('Application closed');
|
||||
process.kill(process.pid, 'SIGTERM');
|
||||
await sleep(10_000);
|
||||
process.exit(0);
|
||||
}, timeout);
|
||||
}
|
||||
return { stopping: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import * as inspector from 'node:inspector';
|
||||
import * as v8 from 'node:v8';
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Query,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import {
|
||||
BrowserTraceQuery,
|
||||
CpuProfileQuery,
|
||||
} from '@waha/structures/server.debug.dto';
|
||||
import { createReadStream } from 'fs';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server/debug')
|
||||
@ApiTags('🔍 Observability')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
export class ServerDebugController {
|
||||
private logger: Logger;
|
||||
private readonly enabled: boolean;
|
||||
|
||||
constructor(
|
||||
private config: WhatsappConfigService,
|
||||
private manager: SessionManager,
|
||||
) {
|
||||
this.logger = new Logger('ServerDebugController');
|
||||
this.enabled = this.config.debugModeEnabled;
|
||||
}
|
||||
|
||||
@Get('cpu')
|
||||
@ApiOperation({
|
||||
summary: 'Collect and return a CPU profile for the current nodejs process',
|
||||
description: 'Uses the Node.js inspector profiler to capture a .cpuprofile',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async cpuProfile(@Query() query: CpuProfileQuery) {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
const { seconds } = query;
|
||||
this.logger.log(`Collecting CPU profile for ${seconds}s...`);
|
||||
|
||||
const session = new inspector.Session();
|
||||
session.connect();
|
||||
|
||||
const profile = await new Promise<inspector.Profiler.Profile>(
|
||||
(resolve, reject) => {
|
||||
session.post('Profiler.enable', (enableError) => {
|
||||
if (enableError) {
|
||||
session.disconnect();
|
||||
return reject(enableError);
|
||||
}
|
||||
session.post('Profiler.start', (startError) => {
|
||||
if (startError) {
|
||||
session.disconnect();
|
||||
return reject(startError);
|
||||
}
|
||||
setTimeout(() => {
|
||||
session.post('Profiler.stop', (stopError, params) => {
|
||||
session.disconnect();
|
||||
if (stopError) {
|
||||
return reject(stopError);
|
||||
}
|
||||
resolve(params.profile);
|
||||
});
|
||||
}, seconds * 1000);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const filename = `CPU.${Date.now()}.${process.pid}.cpuprofile`;
|
||||
const stream = Readable.from([JSON.stringify(profile)]);
|
||||
return new StreamableFile(stream, {
|
||||
type: 'application/json',
|
||||
disposition: `attachment; filename=${filename}`,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('heapsnapshot')
|
||||
@ApiOperation({
|
||||
summary: 'Return a heapsnapshot for the current nodejs process',
|
||||
description: "Return a heapsnapshot of the server's memory",
|
||||
})
|
||||
async heapsnapshot() {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
this.logger.log('Creating a heap snapshot...');
|
||||
const heap = v8.getHeapSnapshot();
|
||||
const fileName = `${Date.now()}.heapsnapshot`;
|
||||
return new StreamableFile(heap, {
|
||||
type: 'application/octet-stream',
|
||||
disposition: `attachment; filename=${fileName}`,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('browser/trace/:session')
|
||||
@ApiOperation({
|
||||
summary: 'Collect and get a trace.json for Chrome DevTools ',
|
||||
description: 'Uses https://pptr.dev/api/puppeteer.tracing',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async browserTrace(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() query: BrowserTraceQuery,
|
||||
) {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
const filepath = await session.browserTrace(query);
|
||||
const stream = createReadStream(filepath);
|
||||
const filename = `trace - ${session.name} - ${new Date()}.json`;
|
||||
return new StreamableFile(stream, {
|
||||
type: 'application/octet-stream',
|
||||
disposition: `attachment; filename=${filename}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
+116
-39
@@ -3,28 +3,39 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiBody,
|
||||
ApiOAuth2,
|
||||
ApiOperation,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { WAHASessionStatus } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
AppsService,
|
||||
IAppsService,
|
||||
} from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import {
|
||||
SessionLogoutDeprecatedRequest,
|
||||
SessionStartDeprecatedRequest,
|
||||
SessionStopDeprecatedRequest,
|
||||
} from '@waha/structures/sessions.deprecated.dto';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -33,50 +44,77 @@ import {
|
||||
MeInfo,
|
||||
SessionCreateRequest,
|
||||
SessionDTO,
|
||||
SessionExpand,
|
||||
SessionInfo,
|
||||
SessionInfoQuery,
|
||||
SessionUpdateRequest,
|
||||
} from '../structures/sessions.dto';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
import { SessionExamples } from './sessions.examples';
|
||||
import { FilterSessions } from '../core/auth/casl.ability';
|
||||
import { CheckPolicies } from '../core/auth/policies.decorator';
|
||||
import { PoliciesGuard } from '../core/auth/policies.guard';
|
||||
import { CanSession, FromBody, FromParam } from '../core/auth/policies';
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/sessions')
|
||||
@ApiTags('🖥️ Sessions')
|
||||
@UseGuards(PoliciesGuard)
|
||||
class SessionsController {
|
||||
private lock: any;
|
||||
|
||||
constructor(private manager: SessionManager) {
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
}
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
@Inject(AppsService) private appsService: IAppsService,
|
||||
) {}
|
||||
|
||||
private withLock(name: string, fn: () => any) {
|
||||
return this.lock.acquire(name, fn);
|
||||
return this.manager.withLock(name, fn);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({ summary: 'List all sessions' })
|
||||
list(
|
||||
@ApiOperation({
|
||||
summary: 'List all sessions',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.List))
|
||||
@ApiOAuth2(['read:items'])
|
||||
async list(
|
||||
@Query(new WAHAValidationPipe()) query: ListSessionsQuery,
|
||||
@Req() req,
|
||||
): Promise<SessionInfo[]> {
|
||||
return this.manager.getSessions(query.all);
|
||||
let sessions = await this.manager.getSessions(query.all);
|
||||
if (!req.user.isAdmin) {
|
||||
sessions = FilterSessions(req.ability, Action.Read, sessions);
|
||||
}
|
||||
if (query.expand?.includes(SessionExpand.apps)) {
|
||||
for (const session of sessions) {
|
||||
session.apps = await this.appsService.list(this.manager, session.name);
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
@Get('/:session')
|
||||
@ApiOperation({ summary: 'Get session information' })
|
||||
@SessionApiParam
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async get(@Param('session') name: string): Promise<SessionInfo> {
|
||||
const session = this.manager.getSessionInfo(name);
|
||||
async get(
|
||||
@Param('session') name: string,
|
||||
@Query() query: SessionInfoQuery,
|
||||
): Promise<SessionInfo> {
|
||||
const session = await this.manager.getSessionInfo(name);
|
||||
if (session === null) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
if (query.expand?.includes(SessionExpand.apps)) {
|
||||
session.apps = await this.appsService.list(this.manager, name);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
@Get(':session/me')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get information about the authenticated account' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
getMe(@SessionParam session: WhatsappSession): MeInfo | null {
|
||||
return session.getSessionMeInfo();
|
||||
}
|
||||
@@ -87,6 +125,8 @@ class SessionsController {
|
||||
description:
|
||||
'Create session a new session (and start it at the same time if required).',
|
||||
})
|
||||
@ApiBody({ type: SessionCreateRequest, examples: SessionExamples })
|
||||
@CheckPolicies(CanSession(Action.Create))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() request: SessionCreateRequest): Promise<SessionDTO> {
|
||||
const name = request.name || generatePrefixedId('session');
|
||||
@@ -95,15 +135,26 @@ class SessionsController {
|
||||
const msg = `Session '${name}' already exists. Use PUT to update it.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
}
|
||||
|
||||
const config = request.config;
|
||||
const start = request.start || false;
|
||||
await this.manager.upsert(name, config);
|
||||
if (request.apps) {
|
||||
await this.appsService.syncSessionApps(
|
||||
this.manager,
|
||||
name,
|
||||
request.apps,
|
||||
);
|
||||
}
|
||||
if (start) {
|
||||
await this.manager.assign(name);
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
const session = await this.manager.getSessionInfo(name);
|
||||
if (request.apps) {
|
||||
session.apps = await this.appsService.list(this.manager, name);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
@Put(':session')
|
||||
@@ -112,7 +163,9 @@ class SessionsController {
|
||||
description: '',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiBody({ type: SessionUpdateRequest, examples: SessionExamples })
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe({ forbidNonWhitelisted: false }))
|
||||
async update(
|
||||
@Param('session') name: string,
|
||||
@Body() request: SessionUpdateRequest,
|
||||
@@ -125,11 +178,22 @@ class SessionsController {
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.upsert(name, config);
|
||||
if (request.apps) {
|
||||
await this.appsService.syncSessionApps(
|
||||
this.manager,
|
||||
name,
|
||||
request.apps,
|
||||
);
|
||||
}
|
||||
if (isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
const session = await this.manager.getSessionInfo(name);
|
||||
if (request.apps) {
|
||||
session.apps = await this.appsService.list(this.manager, name);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
@Delete(':session')
|
||||
@@ -139,9 +203,12 @@ class SessionsController {
|
||||
description:
|
||||
'Delete the session with the given name. Stop and logout as well. Idempotent operation.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Delete, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('session') name: string): Promise<void> {
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
await this.manager.unpair(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
@@ -153,8 +220,9 @@ class SessionsController {
|
||||
@ApiOperation({
|
||||
summary: 'Start the session',
|
||||
description:
|
||||
'Start the session with the given name. The session must exist. Identity operation.',
|
||||
'Start the session with the given name. The session must exist. Idempotent operation.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async start(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
@@ -162,10 +230,8 @@ class SessionsController {
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
if (!isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
await this.manager.assign(name);
|
||||
await this.manager.start(name);
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
@@ -176,13 +242,11 @@ class SessionsController {
|
||||
summary: 'Stop the session',
|
||||
description: 'Stop the session with the given name. Idempotent operation.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async stop(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
await this.manager.unassign(name);
|
||||
await this.manager.stop(name, false);
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
@@ -194,6 +258,7 @@ class SessionsController {
|
||||
summary: 'Logout from the session',
|
||||
description: 'Logout the session, restart a session if it was not STOPPED',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async logout(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
@@ -202,6 +267,7 @@ class SessionsController {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
await this.manager.unpair(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
if (isRunning) {
|
||||
@@ -217,17 +283,10 @@ class SessionsController {
|
||||
summary: 'Restart the session',
|
||||
description: 'Restart the session with the given name.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async restart(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.start(name);
|
||||
});
|
||||
|
||||
await this.manager.restart(name);
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@@ -238,10 +297,14 @@ class SessionsController {
|
||||
'Create session (if not exists) or update a config (if exists) and start it.',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
|
||||
async DEPRACATED_start(
|
||||
@Body() request: SessionStartDeprecatedRequest,
|
||||
): Promise<SessionDTO> {
|
||||
const name = request.name;
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
if (this.manager.isRunning(name)) {
|
||||
const msg = `Session '${name}' is already started.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
@@ -250,6 +313,7 @@ class SessionsController {
|
||||
return await this.withLock(name, async () => {
|
||||
const config = request.config;
|
||||
await this.manager.upsert(name, config);
|
||||
await this.manager.assign(name);
|
||||
return await this.manager.start(name);
|
||||
});
|
||||
}
|
||||
@@ -260,19 +324,26 @@ class SessionsController {
|
||||
description: 'Stop session and Logout by default.',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
|
||||
async DEPRECATED_stop(
|
||||
@Body() request: SessionStopDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
const name = request.name;
|
||||
if (request.logout) {
|
||||
// Old API did remove the session complete
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
await this.manager.unpair(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
});
|
||||
} else {
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
await this.manager.stop(name, false);
|
||||
});
|
||||
}
|
||||
@@ -285,11 +356,17 @@ class SessionsController {
|
||||
description: 'Stop, Logout and Delete session.',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
|
||||
async DEPRECATED_logout(
|
||||
@Body() request: SessionLogoutDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
const name = request.name;
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
await this.manager.unpair(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
|
||||
export const SessionExamples = {
|
||||
basic: {
|
||||
summary: 'Basic Session',
|
||||
value: {
|
||||
name: 'default',
|
||||
},
|
||||
},
|
||||
withWebhooks: {
|
||||
summary: 'Session Webhooks',
|
||||
value: {
|
||||
name: 'default',
|
||||
config: {
|
||||
webhooks: [
|
||||
{
|
||||
url: 'https://example.com/webhooks/waha',
|
||||
events: ['message', 'session.status'],
|
||||
hmac: {
|
||||
key: 'super-secret',
|
||||
},
|
||||
retries: {
|
||||
delaySeconds: 2,
|
||||
attempts: 5,
|
||||
policy: 'linear',
|
||||
},
|
||||
customHeaders: [
|
||||
{
|
||||
name: 'X-Request-ID',
|
||||
value: '123',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
withApps: {
|
||||
summary: 'Session Apps',
|
||||
value: {
|
||||
name: 'default',
|
||||
apps: [
|
||||
{
|
||||
app: AppName.calls,
|
||||
id: 'app_unique_id',
|
||||
session: '',
|
||||
config: {
|
||||
dm: {
|
||||
reject: true,
|
||||
message:
|
||||
"📞❌ We don't take calls right now.\n🎤 Please send a voice message or 📝 text — we'll reply ASAP!",
|
||||
},
|
||||
group: {
|
||||
reject: true,
|
||||
message:
|
||||
"📞❌ We don't take calls right now.\n🎤 Please send a voice message or 📝 text — we'll reply ASAP!",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
start: true,
|
||||
},
|
||||
},
|
||||
full: {
|
||||
summary: 'Full Session Example',
|
||||
value: {
|
||||
name: 'default',
|
||||
start: true,
|
||||
config: {
|
||||
metadata: {
|
||||
'user.id': '123',
|
||||
},
|
||||
debug: true,
|
||||
webhooks: [
|
||||
{
|
||||
url: 'https://example.com/webhooks/waha',
|
||||
events: ['message', 'session.status'],
|
||||
hmac: {
|
||||
key: 'super-secret',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
app: AppName.calls,
|
||||
enabled: true,
|
||||
config: {
|
||||
dm: {
|
||||
reject: true,
|
||||
message:
|
||||
"📞❌ We don't take calls right now.\n🎤 Please send a voice message or 📝 text — we'll reply ASAP!",
|
||||
},
|
||||
group: {
|
||||
reject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { NewMessageIDResponse } from '@waha/structures/chatting.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -14,10 +15,17 @@ import {
|
||||
VideoStatus,
|
||||
VoiceStatus,
|
||||
} from '../structures/status.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/status')
|
||||
@ApiTags('🟢 Status')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
class StatusController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -25,7 +33,7 @@ class StatusController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Send text status' })
|
||||
sendTextStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() status: TextStatus,
|
||||
) {
|
||||
return session.sendTextStatus(status);
|
||||
@@ -35,7 +43,7 @@ class StatusController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Send image status' })
|
||||
sendImageStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() status: ImageStatus,
|
||||
) {
|
||||
return session.sendImageStatus(status);
|
||||
@@ -45,7 +53,7 @@ class StatusController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Send voice status' })
|
||||
sendVoiceStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() status: VoiceStatus,
|
||||
) {
|
||||
return session.sendVoiceStatus(status);
|
||||
@@ -55,7 +63,7 @@ class StatusController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Send video status' })
|
||||
sendVideoStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() status: VideoStatus,
|
||||
) {
|
||||
return session.sendVideoStatus(status);
|
||||
@@ -65,11 +73,23 @@ class StatusController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'DELETE sent status' })
|
||||
deleteStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() status: DeleteStatusRequest,
|
||||
) {
|
||||
return session.deleteStatus(status);
|
||||
}
|
||||
|
||||
@Get('new-message-id')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Generate message ID you can use to batch contacts',
|
||||
})
|
||||
async getNewMessageId(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<NewMessageIDResponse> {
|
||||
const id = await session.generateNewMessageId();
|
||||
return { id: id };
|
||||
}
|
||||
}
|
||||
|
||||
export { StatusController };
|
||||
@@ -1,17 +1,19 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { WAHAEnvironment } from '../structures/environment.dto';
|
||||
import { VERSION } from '../version';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/version')
|
||||
@ApiTags('🔍 Observability')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
export class VersionController {
|
||||
@Get('')
|
||||
@ApiOperation({
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
BeforeApplicationShutdown,
|
||||
Logger,
|
||||
LoggerService,
|
||||
} from '@nestjs/common';
|
||||
import { sleep } from '@nestjs/terminus/dist/utils';
|
||||
import {
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
OnGatewayInit,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
||||
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
|
||||
import { WebSocket } from '@waha/nestjs/ws/ws';
|
||||
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
|
||||
import { EventWildUnmask } from '@waha/utils/events';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { URL } from 'url';
|
||||
import { Server } from 'ws';
|
||||
import { CaslAbilityFactory } from '@waha/core/auth/casl.ability';
|
||||
import { Action, session as SessionName } from '@waha/core/auth/casl.types';
|
||||
|
||||
export enum WebSocketCloseCode {
|
||||
NORMAL = 1000,
|
||||
GOING_AWAY = 1001,
|
||||
PROTOCOL_ERROR = 1002,
|
||||
UNSUPPORTED_DATA = 1003,
|
||||
POLICY_VIOLATION = 1008,
|
||||
INTERNAL_ERROR = 1011,
|
||||
}
|
||||
|
||||
@WebSocketGateway({
|
||||
path: '/ws',
|
||||
cors: true,
|
||||
})
|
||||
export class WebsocketGatewayCore
|
||||
implements
|
||||
OnGatewayInit,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
BeforeApplicationShutdown
|
||||
{
|
||||
HEARTBEAT_INTERVAL = 60_000;
|
||||
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
private readonly logger: LoggerService;
|
||||
private heartbeat: WebsocketHeartbeatJob;
|
||||
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
|
||||
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
private auth: WebSocketAuth,
|
||||
private readonly casl: CaslAbilityFactory,
|
||||
) {
|
||||
this.logger = new Logger('WebsocketGateway');
|
||||
this.heartbeat = new WebsocketHeartbeatJob(
|
||||
this.logger,
|
||||
this.HEARTBEAT_INTERVAL,
|
||||
);
|
||||
}
|
||||
|
||||
async handleConnection(
|
||||
socket: WebSocket,
|
||||
request: IncomingMessage,
|
||||
...args
|
||||
): Promise<any> {
|
||||
// wsc - websocket client
|
||||
socket.id = generatePrefixedId('wsc');
|
||||
|
||||
const user = await this.auth.validateRequest(request);
|
||||
if (!user) {
|
||||
// Not authorized - close connection
|
||||
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Unauthorized');
|
||||
this.logger.debug(
|
||||
`Unauthorized websocket connection attempt: ${request.url} - ${socket.id}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = this.getParams(request);
|
||||
let session: string = params.session;
|
||||
const ability = this.casl.createForUser(user);
|
||||
if (session == '*' && !ability.can(Action.Use, 'all')) {
|
||||
// Limit user to listen only the session events
|
||||
session = user.session;
|
||||
}
|
||||
|
||||
if (!ability.can(Action.Use, new SessionName(session))) {
|
||||
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`New client connected: ${request.url} - ${socket.id}`);
|
||||
const events: WAHAEvents[] = params.events;
|
||||
this.logger.debug(
|
||||
`Client connected to session: '${session}', events: ${events}, ${socket.id}`,
|
||||
);
|
||||
|
||||
const sub = this.manager
|
||||
.getSessionEvents(session, events)
|
||||
.subscribe((data) => {
|
||||
setImmediate(() => {
|
||||
this.logger.debug(
|
||||
`Sending data to client, event.id: ${data.id}`,
|
||||
data,
|
||||
);
|
||||
socket.send(JSON.stringify(data), (err) => {
|
||||
if (!err) {
|
||||
return;
|
||||
}
|
||||
this.logger.error(`Error sending data to client: ${err}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
socket.on('close', () => {
|
||||
this.logger.debug(`Client disconnected - ${socket.id}`);
|
||||
sub.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
private getParams(request: IncomingMessage) {
|
||||
// We need only search params, so localhost is fine here
|
||||
const query = new URL(request.url, 'http://localhost').searchParams;
|
||||
const session = query.get('session') || '*';
|
||||
const paramsEvents = query.getAll('events');
|
||||
const eventsRaw = paramsEvents.length > 0 ? paramsEvents : ['*'];
|
||||
const eventsList = eventsRaw.flatMap((value) => value.split(','));
|
||||
const events = this.eventUnmask.unmask(eventsList);
|
||||
return { session, events };
|
||||
}
|
||||
|
||||
handleDisconnect(socket: WebSocket): any {
|
||||
this.logger.debug(`Client disconnected - ${socket.id}`);
|
||||
}
|
||||
|
||||
async beforeApplicationShutdown(signal?: string) {
|
||||
this.logger.log('Shutting down websocket server');
|
||||
this.heartbeat?.stop();
|
||||
// Allow pending messages to be sent, it can be even 1ms, just to release the event loop
|
||||
await sleep(100);
|
||||
this.logger.log('Websocket server is down');
|
||||
}
|
||||
|
||||
afterInit(server: Server) {
|
||||
this.logger.debug('Websocket server initialized');
|
||||
|
||||
this.logger.debug('Starting heartbeat service...');
|
||||
this.heartbeat.start(server);
|
||||
this.logger.debug('Heartbeat service started');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { JOB_DELAY, JOB_LOCK_TTL } from '@waha/apps/app_sdk/constants';
|
||||
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { BaseWorkerHost } from '@waha/utils/bull/BaseWorkerHost';
|
||||
import { Job } from 'bullmq';
|
||||
import { DelayedError } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
export interface JobDataTimeout {
|
||||
timeout?: {
|
||||
job?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for app consumers that provides common functionality
|
||||
* like mutex locking, logging setup, and error handling.
|
||||
*/
|
||||
export abstract class AppConsumer extends BaseWorkerHost {
|
||||
protected readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
appName: string,
|
||||
componentName: string,
|
||||
log: PinoLogger,
|
||||
protected readonly rmutex: RMutexService,
|
||||
) {
|
||||
super();
|
||||
this.logger = log.logger.child({
|
||||
app: appName,
|
||||
component: componentName,
|
||||
});
|
||||
}
|
||||
|
||||
protected signal(job) {
|
||||
if (job.data?.timeout?.job != null) {
|
||||
return AbortSignal.timeout(job.data?.timeout?.job);
|
||||
}
|
||||
// Aka never abort
|
||||
return new AbortController().signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a job with mutex locking to ensure only one job processes a specific resource at a time.
|
||||
* @param job The job to process
|
||||
* @param mutexKey The key to use for mutex locking
|
||||
* @param processor The function that processes the job
|
||||
* @returns The result of the processor function
|
||||
*/
|
||||
protected async withMutex<T, R>(
|
||||
job: Job<T, R, any>,
|
||||
mutexKey: string,
|
||||
processor: () => Promise<R>,
|
||||
): Promise<R> {
|
||||
const mutex = this.rmutex.get(mutexKey, JOB_LOCK_TTL);
|
||||
const lock = await mutex.lock();
|
||||
|
||||
if (!lock) {
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
logger.debug(
|
||||
`Postponing job '${job.id}' for ${JOB_DELAY}ms, another job is already running the mutex.key='${mutexKey}'`,
|
||||
);
|
||||
await job.moveToDelayed(Date.now() + JOB_DELAY);
|
||||
throw new DelayedError();
|
||||
}
|
||||
|
||||
try {
|
||||
return await processor();
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export class AxiosLogging {
|
||||
constructor(private readonly logger: ILogger) {}
|
||||
|
||||
applyTo(instance: AxiosInstance) {
|
||||
instance.interceptors.request.use(this.onRequest.bind(this));
|
||||
instance.interceptors.response.use(
|
||||
this.onResponse.bind(this),
|
||||
this.onError.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
private onRequest(config: AxiosRequestConfig) {
|
||||
const method = config.method?.toUpperCase() ?? 'GET';
|
||||
const url = config.url ?? '';
|
||||
const query = config.params
|
||||
? `?${new URLSearchParams(config.params).toString()}`
|
||||
: '';
|
||||
const body = config.data ? JSON.stringify(config.data) : '';
|
||||
|
||||
this.logger.debug(`${method} ${url}${query} ${body}`);
|
||||
return config;
|
||||
}
|
||||
|
||||
private onResponse(response: AxiosResponse) {
|
||||
const { method, url } = response.config;
|
||||
const status = response.status;
|
||||
const methodStr = method?.toUpperCase() ?? 'GET';
|
||||
|
||||
const type: any = response.headers['content-type'];
|
||||
// Log warnings for non-2xx/3xx responses
|
||||
if (status < 200 || status >= 400) {
|
||||
const data = JSON.stringify(response.data);
|
||||
this.logger.warn(`${methodStr} ${status} ${url} ${data}`);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Log debug for 2xx/3xx responses
|
||||
this.logger.debug(`${methodStr} ${status}:OK ${url}`);
|
||||
if (type?.startsWith('application/json')) {
|
||||
const data = JSON.stringify(response.data);
|
||||
this.logger.trace(`${methodStr} ${status}:OK ${url} ${data}`);
|
||||
} else {
|
||||
this.logger.trace(`${methodStr} ${status}:OK ${url} [${type}]`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private onError(error: any) {
|
||||
if (error.response) {
|
||||
const { method, url } = error.config;
|
||||
const status = error.response.status;
|
||||
const message = `${
|
||||
method?.toUpperCase() ?? 'GET'
|
||||
} ${status} ${url} ${JSON.stringify(error.response.data)}`;
|
||||
this.logger.error(message);
|
||||
} else {
|
||||
this.logger.error(`Axios error: ${error.message}`);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { RegisterQueueOptions } from '@nestjs/bullmq/dist/interfaces/register-queue-options.interface';
|
||||
import { DynamicModule } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Registers a queue with both BullModule and BullBoardModule
|
||||
* This ensures that whenever a queue is registered, it's also added to the Bull Board
|
||||
*
|
||||
* @param options Queue options including name
|
||||
* @returns An array of dynamic modules for both BullModule and BullBoardModule
|
||||
*/
|
||||
export function RegisterAppQueue(
|
||||
options: RegisterQueueOptions,
|
||||
): DynamicModule[] {
|
||||
return [
|
||||
BullModule.registerQueue({
|
||||
...options,
|
||||
}),
|
||||
BullBoardModule.forFeature({
|
||||
name: options.name,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface ILogger {
|
||||
fatal(log: string): void;
|
||||
error(log: string): void;
|
||||
warn(log: string): void;
|
||||
info(log: string): void;
|
||||
debug(log: string): void;
|
||||
trace(log: string): void;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Job } from 'bullmq';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
/**
|
||||
* JobLoggerWrapper is a wrapper around the BullMQ Job class that provides
|
||||
* a logger with the job ID as a child context.
|
||||
* It allows logging messages to the job's log
|
||||
* and also to the logger itself.
|
||||
*/
|
||||
export class JobLoggerWrapper implements ILogger {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
private job: Job,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.logger = logger.child({ jobId: job.id });
|
||||
}
|
||||
|
||||
fatal(log: string): void {
|
||||
this.log(log, 'fatal');
|
||||
}
|
||||
|
||||
error(log: string): void {
|
||||
this.log(log, 'error');
|
||||
}
|
||||
|
||||
warn(log: string): void {
|
||||
this.log(log, 'warn');
|
||||
}
|
||||
|
||||
info(log: string): void {
|
||||
this.log(log, 'info');
|
||||
}
|
||||
|
||||
debug(log: string): void {
|
||||
this.log(log, 'debug');
|
||||
}
|
||||
|
||||
trace(log: string): void {
|
||||
this.log(log, 'trace');
|
||||
}
|
||||
|
||||
private log(msg: string, level: string) {
|
||||
if (!this.logger.isLevelEnabled(level)) {
|
||||
return;
|
||||
}
|
||||
this.logger[level](msg);
|
||||
const timestamp = new Date().toISOString();
|
||||
this.job
|
||||
.log(`[${timestamp}] ${level.toUpperCase()}: ${msg}`)
|
||||
.catch((err) => {
|
||||
this.logger.error('Error logging message to job', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Backoffs, Job } from 'bullmq';
|
||||
import { FlowJob } from 'bullmq/dist/esm/interfaces';
|
||||
|
||||
/**
|
||||
* Calculates the delay until the next attempt for a job.
|
||||
*
|
||||
* @param job The job to calculate the next attempt delay for
|
||||
* @returns The delay in milliseconds, or null if there is no next attempt or backoff is not set
|
||||
*/
|
||||
export function NextAttemptDelayInMs(job: Job): number | null {
|
||||
const attemptsMade = job.attemptsMade + 1;
|
||||
const maxAttempts = job.opts?.attempts || 1;
|
||||
// If this is the last attempt, return null
|
||||
if (attemptsMade >= maxAttempts) {
|
||||
return null;
|
||||
}
|
||||
// Normalize the backoff options
|
||||
const backoff = Backoffs.normalize(job.opts.backoff);
|
||||
if (!backoff) {
|
||||
return null;
|
||||
}
|
||||
// Calculate the delay using Backoffs.calculate
|
||||
const delay = Backoffs.calculate(backoff, attemptsMade, new Error(), job);
|
||||
// The delay can be a Promise, a number, or undefined
|
||||
if (delay === undefined) {
|
||||
return null;
|
||||
}
|
||||
// If it's a Promise, we can't handle it synchronously, so return null
|
||||
if (delay instanceof Promise) {
|
||||
return null;
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
|
||||
export function NextAttemptDelayInWholeSeconds(job: Job) {
|
||||
const delay = NextAttemptDelayInMs(job);
|
||||
if (delay == null) {
|
||||
return null;
|
||||
}
|
||||
return Math.round(delay / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a job has been retried (not on its first attempt).
|
||||
*
|
||||
* @param job The job to check
|
||||
* @returns True if the job has been retried (attemptsMade > 1), false otherwise
|
||||
*/
|
||||
export function HasBeenRetried(job: Job): boolean {
|
||||
const attemptsMade = job.attemptsMade + 1;
|
||||
return attemptsMade > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide chatwoot and waha from queue name for user faced messages
|
||||
*/
|
||||
export function QueueNameRepr(name: string): string {
|
||||
// No "chatwoot" in the name always (it start with chatwoot)
|
||||
name = name.replace('chatwoot.', '');
|
||||
// Replace "waha" with "whatsapp"
|
||||
name = name.replace('waha |', 'whatsapp |');
|
||||
return name;
|
||||
}
|
||||
|
||||
let base =
|
||||
process.env.WAHA_PUBLIC_URL ||
|
||||
process.env.WAHA_BASE_URL ||
|
||||
'http://localhost:3000';
|
||||
// cut / at the end
|
||||
base = base.replace(/\/+$/, '');
|
||||
|
||||
export function JobLink(job: Job): { text: string; url: string } {
|
||||
// Use repr name for text
|
||||
const name = QueueNameRepr(job.queueName);
|
||||
const text = `${name} => ${job.id}`;
|
||||
// Use original queue name in the URL
|
||||
const queue = encodeURIComponent(job.queueName);
|
||||
const id = encodeURIComponent(job.id);
|
||||
const url = `${base}/jobs/queue/${queue}/${id}`;
|
||||
return { text: text, url: url };
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain jobs so that they run one at a time
|
||||
* A | B | C
|
||||
* to
|
||||
* A -> B -> C
|
||||
*/
|
||||
export function ChainJobsOneAtATime(jobs: FlowJob[]): FlowJob {
|
||||
if (jobs.length == 0) {
|
||||
throw new Error('No jobs provided');
|
||||
}
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job.children.length != 0) {
|
||||
throw new Error('Jobs with children are not supported');
|
||||
}
|
||||
}
|
||||
const left = [...jobs];
|
||||
const root = left.pop();
|
||||
let parent = root;
|
||||
while (left.length != 0) {
|
||||
const job = left.pop();
|
||||
parent.children = [job];
|
||||
parent = job;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
AppsService,
|
||||
IAppsService,
|
||||
} from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import {
|
||||
CanServer,
|
||||
CanSession,
|
||||
FromBody,
|
||||
FromQuery,
|
||||
} from '@waha/core/auth/policies';
|
||||
import { Action, session as SessionName } from '@waha/core/auth/casl.types';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
|
||||
import { App } from '../dto/app.dto';
|
||||
import { ListAppsQuery } from '../dto/query.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/apps')
|
||||
@ApiTags('🧩 Apps')
|
||||
@UseGuards(PoliciesGuard)
|
||||
export class AppsController {
|
||||
constructor(
|
||||
@Inject(AppsService)
|
||||
private appsService: IAppsService,
|
||||
private manager: SessionManager,
|
||||
) {}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({ summary: 'List all apps for a session' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async list(
|
||||
@Query(new WAHAValidationPipe()) query: ListAppsQuery,
|
||||
): Promise<App[]> {
|
||||
return this.appsService.list(this.manager, query.session);
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
@ApiOperation({ summary: 'Create a new app' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() app: App): Promise<App> {
|
||||
const result = await this.appsService.create(this.manager, app);
|
||||
const isRunning = this.manager.isRunning(app.session);
|
||||
if (isRunning && app.enabled) {
|
||||
await this.manager.restart(app.session);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@ApiOperation({ summary: 'Get app by ID' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async get(@Param('id') id: string, @Req() req: any): Promise<App> {
|
||||
const app = await this.appsService.get(this.manager, id);
|
||||
if (!app) {
|
||||
throw new NotFoundException(`App '${id}' not found`);
|
||||
}
|
||||
if (!req.ability?.can(Action.Use, new SessionName(app.session))) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@ApiOperation({ summary: 'Update an existing app' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() app: App,
|
||||
@Req() req: any,
|
||||
): Promise<App> {
|
||||
const existing = await this.appsService.get(this.manager, id);
|
||||
if (existing) {
|
||||
if (!req.ability?.can(Action.Use, new SessionName(existing.session))) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
} else {
|
||||
if (!req.ability?.can(Action.Use, new SessionName(app.session))) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
if (!app.id) {
|
||||
app.id = id;
|
||||
} else if (app.id !== id) {
|
||||
throw new NotFoundException(
|
||||
`App ID in path (${id}) does not match ID in body (${app.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.appsService.upsert(this.manager, app);
|
||||
const isRunning = this.manager.isRunning(app.session);
|
||||
if (isRunning) {
|
||||
await this.manager.restart(app.session);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Delete an app' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('id') id: string, @Req() req: any): Promise<void> {
|
||||
const existing = await this.appsService.get(this.manager, id);
|
||||
if (!existing) {
|
||||
throw new NotFoundException(`App '${id}' not found`);
|
||||
}
|
||||
if (!req.ability?.can(Action.Use, new SessionName(existing.session))) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
const app = await this.appsService.delete(this.manager, id);
|
||||
const isRunning = this.manager.isRunning(app.session);
|
||||
if (isRunning) {
|
||||
await this.manager.restart(app.session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { AppEnv } from '@waha/apps/app_sdk/env';
|
||||
import { AppDefinition, APPS } from '@waha/apps/app_sdk/apps/definition';
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
|
||||
class AppRuntimeConfigC {
|
||||
private constructor(private apps: AppDefinition[] | null) {}
|
||||
|
||||
static FromEnv(env: typeof AppEnv) {
|
||||
if (!env.enabled) {
|
||||
return new AppRuntimeConfigC(null);
|
||||
}
|
||||
let apps = Object.values(APPS);
|
||||
// Include
|
||||
if (env.on && env.on.length > 0) {
|
||||
apps = apps.filter((app) => env.on!.includes(app.name));
|
||||
}
|
||||
// Exclude
|
||||
if (env.off && env.off.length > 0) {
|
||||
apps = apps.filter((app) => !env.off!.includes(app.name));
|
||||
}
|
||||
return new AppRuntimeConfigC(apps);
|
||||
}
|
||||
|
||||
Enabled() {
|
||||
return this.apps !== null;
|
||||
}
|
||||
|
||||
GetApps() {
|
||||
return this.apps || [];
|
||||
}
|
||||
|
||||
GetAppsWithMigration() {
|
||||
return this.GetApps().filter((app) => app.migrations);
|
||||
}
|
||||
|
||||
GetAppsRequiringPlainKey() {
|
||||
return this.GetApps().filter((app) => app.plainkey);
|
||||
}
|
||||
|
||||
GetAppsRequiringQueue() {
|
||||
return this.GetApps().filter((app) => app.queue);
|
||||
}
|
||||
|
||||
HasApp(name: AppName) {
|
||||
return this.GetApps().some((app) => app.name === name);
|
||||
}
|
||||
|
||||
HasAppsRequiringPlainKey() {
|
||||
return this.GetAppsRequiringPlainKey().length > 0;
|
||||
}
|
||||
|
||||
HasAppsRequiringQueue() {
|
||||
return this.GetAppsRequiringQueue().length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const AppRuntimeConfig = AppRuntimeConfigC.FromEnv(AppEnv);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
|
||||
export interface AppDefinition {
|
||||
// App name
|
||||
name: AppName;
|
||||
// If app requires WAHA_API_KEY_PLAIN to work
|
||||
plainkey: boolean;
|
||||
// If app requires queue to work
|
||||
queue: boolean;
|
||||
// If app has any migrations
|
||||
migrations: boolean;
|
||||
}
|
||||
|
||||
// All Apps
|
||||
export const APPS: Record<AppName, AppDefinition> = {
|
||||
[AppName.calls]: {
|
||||
name: AppName.calls,
|
||||
plainkey: false,
|
||||
queue: false,
|
||||
migrations: false,
|
||||
},
|
||||
[AppName.chatwoot]: {
|
||||
name: AppName.chatwoot,
|
||||
plainkey: true,
|
||||
queue: true,
|
||||
migrations: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum AppName {
|
||||
chatwoot = 'chatwoot',
|
||||
calls = 'calls',
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as process from 'node:process';
|
||||
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import * as basicAuth from 'express-basic-auth';
|
||||
import { Auth } from '@waha/core/auth/config';
|
||||
|
||||
export function BullAuthMiddleware() {
|
||||
let username = Auth.dashboard.username.value || '';
|
||||
let password = Auth.dashboard.password.value || '';
|
||||
if (process.env.WAHA_DASHBOARD_ENABLED) {
|
||||
const enabled = parseBool(process.env.WAHA_DASHBOARD_ENABLED);
|
||||
if (!enabled) {
|
||||
// Generate a random uuid4 username/password to effectively disable access
|
||||
username = 'admin';
|
||||
password = crypto.randomUUID();
|
||||
}
|
||||
}
|
||||
|
||||
return basicAuth({
|
||||
challenge: true,
|
||||
users: {
|
||||
[String(username)]: String(password),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DefaultJobOptions } from 'bullmq';
|
||||
|
||||
export const JOB_DELAY = 1_000;
|
||||
export const JOB_LOCK_TTL = 20_000;
|
||||
export const JOB_CONCURRENCY =
|
||||
parseInt(process.env.WAHA_APPS_JOBS_CONCURRENCY) || 50;
|
||||
|
||||
function jobRemoveOptions() {
|
||||
// in seconds
|
||||
const HOUR = 60 * 60;
|
||||
const DAY = 24 * HOUR;
|
||||
return {
|
||||
removeOnComplete: {
|
||||
age:
|
||||
parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_AGE) || 3 * DAY,
|
||||
count:
|
||||
parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_COUNT) || 1000,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_FAIL_AGE) || 31 * DAY,
|
||||
count: parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_FAIL_COUNT) || 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const JobRemoveOptions = jobRemoveOptions();
|
||||
|
||||
function exponentialRetriesJobOptions(): DefaultJobOptions {
|
||||
return {
|
||||
attempts: parseInt(process.env.WAHA_APPS_JOBS_ATTEMPTS) || 3,
|
||||
delay: parseInt(process.env.WAHA_APPS_JOBS_DELAY) || 0,
|
||||
backoff: {
|
||||
type: process.env.WAHA_APPS_JOBS_BACKOFF_TYPE || 'exponential',
|
||||
delay: parseInt(process.env.WAHA_APPS_JOBS_BACKOFF_DELAY) || 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const ExponentialRetriesJobOptions = exponentialRetriesJobOptions();
|
||||
|
||||
export const NoRetriesJobOptions: DefaultJobOptions = {
|
||||
attempts: 1,
|
||||
};
|
||||
|
||||
export function merge(...args: any[]) {
|
||||
return Object.assign({}, ...args);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
|
||||
import { CallsAppConfig } from '@waha/apps/calls/dto/config.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
|
||||
export type AllowedAppConfig = ChatWootAppConfig | CallsAppConfig;
|
||||
|
||||
@ApiExtraModels(ChatWootAppConfig, CallsAppConfig)
|
||||
export class App<T extends AllowedAppConfig = any> {
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
session: string;
|
||||
|
||||
// App name (aka type)
|
||||
@IsEnum(AppName)
|
||||
app: AppName;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Enable or disable this app without deleting it. If omitted, treated as enabled (true).',
|
||||
required: false,
|
||||
default: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enabled?: boolean = true;
|
||||
|
||||
@ValidateNested()
|
||||
@Type((options) => {
|
||||
if (options && options.object && options.object.app) {
|
||||
switch (options.object.app) {
|
||||
case AppName.chatwoot:
|
||||
return ChatWootAppConfig;
|
||||
case AppName.calls:
|
||||
return CallsAppConfig;
|
||||
default:
|
||||
return Object;
|
||||
}
|
||||
}
|
||||
return Object;
|
||||
})
|
||||
config: T;
|
||||
}
|
||||
|
||||
export class ChatWootAppDto extends App<ChatWootAppConfig> {
|
||||
@Type(() => ChatWootAppConfig)
|
||||
config: ChatWootAppConfig;
|
||||
}
|
||||
|
||||
export class CallsAppDto extends App<CallsAppConfig> {
|
||||
@Type(() => CallsAppConfig)
|
||||
config: CallsAppConfig;
|
||||
}
|
||||
|
||||
export type AppDto = ChatWootAppDto | CallsAppDto;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ListAppsQuery {
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
required: true,
|
||||
description: 'Session name to list apps for',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
session: string;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import { APPS } from '@waha/apps/app_sdk/apps/definition';
|
||||
|
||||
// Apps that don't require queue (Redis) - derived from AppDefinition
|
||||
const IN_MEMORY_APPS = Object.values(APPS)
|
||||
.filter((app) => !app.queue)
|
||||
.map((app) => app.name);
|
||||
|
||||
function parseCommaSeparatedList(value: string | undefined): string[] {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return value.split(',').map((item) => item.trim());
|
||||
}
|
||||
|
||||
function isMongoDB(): boolean {
|
||||
return !!process.env.WHATSAPP_SESSIONS_MONGO_URL;
|
||||
}
|
||||
|
||||
function buildAppEnv() {
|
||||
const enabled = process.env.WAHA_APPS_ENABLED;
|
||||
const on = process.env.WAHA_APPS_ON;
|
||||
const off = process.env.WAHA_APPS_OFF;
|
||||
|
||||
// Default if not mongodb
|
||||
const isUserConfigured = enabled !== undefined || on !== undefined;
|
||||
if (!isUserConfigured && !isMongoDB()) {
|
||||
return {
|
||||
enabled: true,
|
||||
on: IN_MEMORY_APPS,
|
||||
off: parseCommaSeparatedList(off),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: parseBool(enabled),
|
||||
on: parseCommaSeparatedList(on),
|
||||
off: parseCommaSeparatedList(off),
|
||||
};
|
||||
}
|
||||
|
||||
export const AppEnv = buildAppEnv();
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
|
||||
export function JestLogger(): ILogger {
|
||||
return {
|
||||
fatal: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { Knex } from 'knex';
|
||||
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
|
||||
|
||||
function migrateSDK(knex: Knex): Promise<any> {
|
||||
const config = {
|
||||
directory: `${__dirname}/migrations`,
|
||||
tableName: 'apps_migrations',
|
||||
loadExtensions: ['.js'],
|
||||
};
|
||||
return knex.migrate.latest(config);
|
||||
}
|
||||
|
||||
function migrateApp(knex: Knex, app: string): Promise<any> {
|
||||
// one level upp
|
||||
const appsDirectory = path.join(__dirname, '..', '..', 'apps');
|
||||
const directory = path.join(appsDirectory, app, 'migrations');
|
||||
const config = {
|
||||
directory: directory,
|
||||
tableName: `app_${app}_migrations`,
|
||||
loadExtensions: ['.js'],
|
||||
};
|
||||
return knex.migrate.latest(config);
|
||||
}
|
||||
|
||||
export async function migrate(knex: Knex) {
|
||||
await migrateSDK(knex);
|
||||
for (const app of AppRuntimeConfig.GetAppsWithMigration()) {
|
||||
await migrateApp(knex, app.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
exports.up = function (knex: Knex) {
|
||||
return knex.schema
|
||||
.createTable('apps', function (table) {
|
||||
table.increments('pk');
|
||||
table.string('id', 64);
|
||||
table.string('session', 64);
|
||||
table.string('app', 64);
|
||||
table.json('config');
|
||||
})
|
||||
.table('apps', function (table) {
|
||||
table.unique(['pk']);
|
||||
table.index(['session']);
|
||||
table.index(['id']);
|
||||
table.unique(['id']);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex: Knex) {
|
||||
return knex.schema.dropTable('apps');
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
exports.up = async function (knex: Knex) {
|
||||
// Add a boolean column 'enabled' defaulting to true and not nullable.
|
||||
const hasColumn = await knex.schema.hasColumn('apps', 'enabled');
|
||||
if (!hasColumn) {
|
||||
await knex.schema.alterTable('apps', (table) => {
|
||||
table.boolean('enabled').defaultTo(true);
|
||||
});
|
||||
// Ensure existing rows have enabled=true
|
||||
await knex('apps').update({ enabled: true });
|
||||
// Make the column NOT NULL
|
||||
await knex.schema.alterTable('apps', (table) => {
|
||||
table.boolean('enabled').notNullable().defaultTo(true).alter();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function (knex: Knex) {
|
||||
const hasColumn = await knex.schema.hasColumn('apps', 'enabled');
|
||||
if (hasColumn) {
|
||||
await knex.schema.alterTable('apps', (table) => {
|
||||
table.dropColumn('enabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
|
||||
import { IAppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { DataStore } from '@waha/core/abc/DataStore';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
import { App } from '../dto/app.dto';
|
||||
|
||||
class AppsIsDisabledError extends UnprocessableEntityException {
|
||||
constructor() {
|
||||
super(
|
||||
"Apps are disabled. Enable it by setting 'WAHA_APPS_ENABLED=True' environment variable.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppsDisabledService implements IAppsService {
|
||||
async list(manager: SessionManager, session: string): Promise<App[]> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async get(manager: SessionManager, appId: string): Promise<App | null> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async create(manager: SessionManager, app: App): Promise<App> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async upsert(manager: SessionManager, app: App): Promise<App> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async update(manager: SessionManager, app: App): Promise<App> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async delete(manager: SessionManager, appId: string): Promise<App> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async removeBySession(manager: SessionManager, session: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
migrate(knex: Knex<any, any[]>): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async beforeSessionStart(session: WhatsappSession, store: DataStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
async afterSessionStart(session: WhatsappSession, store: DataStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
async syncSessionApps(
|
||||
manager: SessionManager,
|
||||
sessionName: string,
|
||||
apps?: App[] | null,
|
||||
): Promise<void> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
Optional,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { migrate } from '@waha/apps/app_sdk/migrations';
|
||||
import { IAppService } from '@waha/apps/app_sdk/services/IAppService';
|
||||
import { IAppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
|
||||
import { CallsAppService } from '@waha/apps/calls/services/CallsAppService';
|
||||
import { DataStore } from '@waha/core/abc/DataStore';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { Knex } from 'knex';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { App } from '../dto/app.dto';
|
||||
import { AppRepository } from '../storage/AppRepository';
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
|
||||
|
||||
export class AppDisableError extends UnprocessableEntityException {
|
||||
constructor(app: string) {
|
||||
super(
|
||||
`App '${app}' is disabled in runtime configuration - adjust WAHA_APPS_ON / WAHA_APPS_OFF environment variables to enable it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppsEnabledService implements IAppsService {
|
||||
constructor(
|
||||
@InjectPinoLogger('AppsService')
|
||||
protected logger: PinoLogger,
|
||||
@Optional() protected readonly chatwootService: ChatWootAppService,
|
||||
@Optional() protected readonly callsAppService: CallsAppService,
|
||||
) {}
|
||||
|
||||
async list(manager: SessionManager, session: string): Promise<App[]> {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const apps = await repo.getAllBySession(session);
|
||||
apps.forEach((app) => {
|
||||
delete app.pk;
|
||||
});
|
||||
return apps;
|
||||
}
|
||||
|
||||
async create(manager: SessionManager, app: App): Promise<App> {
|
||||
await this.checkSessionExists(manager, app.session);
|
||||
app.id = app.id || generatePrefixedId('app');
|
||||
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
|
||||
const existingApp = await repo.getById(app.id);
|
||||
if (existingApp) {
|
||||
throw new Error(`App with ID '${app.id}' already exists.`);
|
||||
}
|
||||
|
||||
let existingApps: App[] = [];
|
||||
if (app.app === AppName.chatwoot || app.app === AppName.calls) {
|
||||
existingApps = await repo.getAllBySession(app.session);
|
||||
}
|
||||
// Validate only one Chatwoot app per session
|
||||
if (app.app === AppName.chatwoot) {
|
||||
const existingChatwootApp = existingApps.find(
|
||||
(existingApp) => existingApp.app === AppName.chatwoot,
|
||||
);
|
||||
|
||||
if (existingChatwootApp) {
|
||||
throw new Error(
|
||||
`Only one Chatwoot app is allowed per session. Session '${app.session}' already has a Chatwoot app with ID '${existingChatwootApp.id}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Validate only one Calls app per session
|
||||
if (app.app === AppName.calls) {
|
||||
const existingCallsApp = existingApps.find(
|
||||
(existingApp) => existingApp.app === AppName.calls,
|
||||
);
|
||||
|
||||
if (existingCallsApp) {
|
||||
throw new Error(
|
||||
`Only one Calls app is allowed per session. Session '${app.session}' already has a Calls app with ID '${existingCallsApp.id}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const service = this.getAppService(app);
|
||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||
throw new AppDisableError(app.app);
|
||||
}
|
||||
service.validate(app);
|
||||
// Only run beforeCreated when app is enabled (default true if omitted)
|
||||
if (app.enabled !== false) {
|
||||
await service.beforeCreated(app);
|
||||
}
|
||||
|
||||
const result = await repo.save(app);
|
||||
delete result.pk;
|
||||
return result;
|
||||
}
|
||||
|
||||
async get(manager: SessionManager, appId: string): Promise<App | null> {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const app = await repo.getById(appId);
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
delete (app as any).pk;
|
||||
return app;
|
||||
}
|
||||
|
||||
async upsert(manager: SessionManager, app: App) {
|
||||
return await this.update(manager, app, true);
|
||||
}
|
||||
|
||||
async update(
|
||||
manager: SessionManager,
|
||||
app: App,
|
||||
upsert: boolean = false,
|
||||
): Promise<App> {
|
||||
await this.checkSessionExists(manager, app.session);
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const savedApp = await repo.getById(app.id);
|
||||
if (!savedApp && upsert) {
|
||||
return this.create(manager, app);
|
||||
}
|
||||
|
||||
if (!savedApp) {
|
||||
throw new NotFoundException(`App '${app.id}' not found`);
|
||||
}
|
||||
if (savedApp.app != app.app) {
|
||||
throw new Error(
|
||||
`Can not change app type. Delete and create a new app. Before type: '${savedApp.app}' After type: '${app.app}'`,
|
||||
);
|
||||
}
|
||||
if (savedApp.session != app.session) {
|
||||
throw new Error(
|
||||
`Can not change session. Delete and create a new app. Before session: '${savedApp.session}' After session: '${app.session}'`,
|
||||
);
|
||||
}
|
||||
|
||||
const service = this.getAppService(app);
|
||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||
throw new AppDisableError(app.app);
|
||||
}
|
||||
service.validate(app);
|
||||
|
||||
const hasEnabledChange = savedApp.enabled !== app.enabled;
|
||||
|
||||
if (hasEnabledChange) {
|
||||
if (app.enabled) {
|
||||
await service.beforeEnabled(savedApp, app);
|
||||
} else {
|
||||
await service.beforeDisabled(savedApp, app);
|
||||
}
|
||||
} else {
|
||||
await service.beforeUpdated(savedApp, app);
|
||||
}
|
||||
await repo.update(app.id, app);
|
||||
const updated = await repo.getById(app.id);
|
||||
delete (updated as any)?.pk;
|
||||
return updated!;
|
||||
}
|
||||
|
||||
async delete(manager: SessionManager, appId: string): Promise<App> {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const app = await repo.getById(appId);
|
||||
if (!app) {
|
||||
throw new NotFoundException(`App '${appId}' not found`);
|
||||
}
|
||||
const service = this.getAppService(app);
|
||||
await service?.beforeDeleted(app);
|
||||
await repo.delete(app.id);
|
||||
delete app.pk;
|
||||
return app;
|
||||
}
|
||||
|
||||
async removeBySession(manager: SessionManager, session: string) {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
await repo.deleteBySession(session);
|
||||
}
|
||||
|
||||
async beforeSessionStart(session: WhatsappSession, store: DataStore) {
|
||||
const knex = store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const apps = await repo.getEnabledBySession(session.name);
|
||||
for (const app of apps) {
|
||||
const service = this.getAppService(app);
|
||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||
throw new AppDisableError(app.app);
|
||||
}
|
||||
service.beforeSessionStart(app, session);
|
||||
}
|
||||
}
|
||||
|
||||
async afterSessionStart(session: WhatsappSession, store: DataStore) {
|
||||
const knex = store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const apps = await repo.getEnabledBySession(session.name);
|
||||
for (const app of apps) {
|
||||
const service = this.getAppService(app);
|
||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||
throw new AppDisableError(app.app);
|
||||
}
|
||||
service.afterSessionStart(app, session);
|
||||
}
|
||||
}
|
||||
|
||||
async syncSessionApps(
|
||||
manager: SessionManager,
|
||||
session: string,
|
||||
apps: App[],
|
||||
): Promise<void> {
|
||||
const existing = await this.list(manager, session);
|
||||
const ids = new Set<string>();
|
||||
|
||||
// Upsert provided apps
|
||||
for (const app of apps) {
|
||||
if (!app.id) {
|
||||
// Try to find the app by type
|
||||
const found = existing.find((a) => a.app === app.app);
|
||||
if (found) {
|
||||
app.id = found.id;
|
||||
}
|
||||
}
|
||||
// Force session
|
||||
app.session = session;
|
||||
await this.upsert(manager, app);
|
||||
ids.add(app.id);
|
||||
}
|
||||
|
||||
// Remove apps that are not in the provided list
|
||||
for (const app of existing) {
|
||||
if (ids.has(app.id)) {
|
||||
continue;
|
||||
}
|
||||
await this.delete(manager, app.id);
|
||||
}
|
||||
}
|
||||
|
||||
async migrate(knex: Knex): Promise<void> {
|
||||
await migrate(knex);
|
||||
}
|
||||
|
||||
private getAppService(app: App): IAppService | null {
|
||||
switch (app.app) {
|
||||
case AppName.chatwoot:
|
||||
return this.chatwootService;
|
||||
case AppName.calls:
|
||||
return this.callsAppService;
|
||||
default:
|
||||
throw new Error(`App '${app.app}' not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkSessionExists(
|
||||
manager: SessionManager,
|
||||
sessionName: string,
|
||||
) {
|
||||
const session = await manager.exists(sessionName);
|
||||
if (session === null) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
|
||||
/**
|
||||
* Exact App service
|
||||
*/
|
||||
export interface IAppService {
|
||||
validate(app: App): void;
|
||||
|
||||
beforeCreated(app: App): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called only when the app transitions from disabled -> enabled.
|
||||
*/
|
||||
beforeEnabled(savedApp: App, newApp: App): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called only when the app transitions from enabled -> disabled.
|
||||
*/
|
||||
beforeDisabled(savedApp: App, newApp: App): Promise<void>;
|
||||
|
||||
beforeUpdated(savedApp: App, newApp: App): Promise<void>;
|
||||
|
||||
beforeDeleted(app: App): Promise<void>;
|
||||
|
||||
beforeSessionStart(app: App, session: WhatsappSession): void;
|
||||
|
||||
afterSessionStart(app: App, session: WhatsappSession): void;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { DataStore } from '@waha/core/abc/DataStore';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export interface IAppsService {
|
||||
list(manager: SessionManager, session: string): Promise<App[]>;
|
||||
|
||||
get(manager: SessionManager, appId: string): Promise<App | null>;
|
||||
|
||||
create(manager: SessionManager, app: App): Promise<App>;
|
||||
|
||||
upsert(manager: SessionManager, app: App): Promise<App>;
|
||||
|
||||
update(manager: SessionManager, app: App): Promise<App>;
|
||||
|
||||
delete(manager: SessionManager, appId: string): Promise<App>;
|
||||
|
||||
removeBySession(manager: SessionManager, session: string): Promise<void>;
|
||||
|
||||
beforeSessionStart(session: WhatsappSession, store: DataStore): Promise<void>;
|
||||
|
||||
afterSessionStart(session: WhatsappSession, store: DataStore): Promise<void>;
|
||||
|
||||
syncSessionApps(
|
||||
manager: SessionManager,
|
||||
sessionName: string,
|
||||
apps?: App[] | null,
|
||||
): Promise<void>;
|
||||
|
||||
migrate(knex: Knex): Promise<void>;
|
||||
}
|
||||
|
||||
export const AppsService = Symbol('AppsService');
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
import { App } from '../dto/app.dto';
|
||||
import { AppDB } from './types';
|
||||
|
||||
export class AppRepository {
|
||||
static tableName = 'apps';
|
||||
|
||||
constructor(private readonly knex: Knex) {}
|
||||
|
||||
get tableName() {
|
||||
return AppRepository.tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an app to the database
|
||||
*/
|
||||
async save(app: Omit<App, 'pk'>): Promise<AppDB> {
|
||||
const appToSave: any = { ...app };
|
||||
if (appToSave.config && typeof appToSave.config === 'object') {
|
||||
appToSave.config = JSON.stringify(appToSave.config);
|
||||
}
|
||||
const [pk] = await this.knex(this.tableName)
|
||||
.insert(appToSave)
|
||||
.returning('pk');
|
||||
return { ...app, pk: pk };
|
||||
}
|
||||
|
||||
private deserialize(app: AppDB): AppDB {
|
||||
const parsedConfig =
|
||||
typeof app.config === 'string' ? JSON.parse(app.config) : app.config;
|
||||
|
||||
// Ensure 'enabled' is boolean (SQLite may return 0/1)
|
||||
let enabled: boolean;
|
||||
switch (app.enabled as any) {
|
||||
case undefined:
|
||||
case true:
|
||||
case 1:
|
||||
case '1':
|
||||
enabled = true;
|
||||
break;
|
||||
case false:
|
||||
case 0:
|
||||
enabled = false;
|
||||
break;
|
||||
default:
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
return { ...(app as any), enabled: enabled, config: parsedConfig } as AppDB;
|
||||
}
|
||||
|
||||
private serialize<T extends Partial<App>>(app: T): T {
|
||||
const appCopy = { ...app };
|
||||
if (appCopy.config && typeof appCopy.config === 'object') {
|
||||
appCopy.config = JSON.stringify(appCopy.config);
|
||||
}
|
||||
return appCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an app by its id
|
||||
*/
|
||||
async getById(id: string): Promise<AppDB | null> {
|
||||
const app = await this.knex(this.tableName).where('id', id).first();
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
return this.deserialize(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an enabled app by id, or null if not found or disabled.
|
||||
*/
|
||||
async findEnabledAppById(id: string): Promise<AppDB | null> {
|
||||
const app = await this.knex(this.tableName)
|
||||
.where('id', id)
|
||||
.andWhere('enabled', true)
|
||||
.first();
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
return this.deserialize(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all apps
|
||||
*/
|
||||
async getAllBySession(session: string): Promise<AppDB[]> {
|
||||
return this.knex(this.tableName)
|
||||
.select('*')
|
||||
.where('session', session)
|
||||
.orderBy('id', 'asc')
|
||||
.then((apps) => apps.map((app) => this.deserialize(app)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets only enabled apps for a session.
|
||||
*/
|
||||
async getEnabledBySession(session: string): Promise<AppDB[]> {
|
||||
return this.knex(this.tableName)
|
||||
.select('*')
|
||||
.where('session', session)
|
||||
.andWhere('enabled', true)
|
||||
.orderBy('id', 'asc')
|
||||
.then((apps) => apps.map((app) => this.deserialize(app)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an app
|
||||
*/
|
||||
async update(id: string, app: Partial<Omit<App, 'id'>>): Promise<void> {
|
||||
const appToUpdate: any = this.serialize(app);
|
||||
if (appToUpdate.enabled === undefined) {
|
||||
delete appToUpdate.enabled; // keep current value
|
||||
}
|
||||
await this.knex(this.tableName).where('id', id).update(appToUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an app
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.knex(this.tableName).where('id', id).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all apps for a session
|
||||
*/
|
||||
async deleteBySession(session: string): Promise<void> {
|
||||
await this.knex(this.tableName).where('session', session).delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
|
||||
export interface AppDB extends App {
|
||||
// Internal database number
|
||||
pk: number;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
type Call<Record> = (processed: number) => Promise<Record[]>;
|
||||
|
||||
export interface PaginatorParams {
|
||||
processed?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
const DefaultParams: PaginatorParams = {
|
||||
processed: 0,
|
||||
max: Infinity,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic paginator for APIs that return arrays of records with limits and offsets.
|
||||
*/
|
||||
export class ArrayPaginator<Record> {
|
||||
private readonly params: PaginatorParams;
|
||||
|
||||
constructor(params: PaginatorParams = DefaultParams) {
|
||||
this.params = lodash.merge({}, DefaultParams, params) as PaginatorParams;
|
||||
}
|
||||
|
||||
async *iterate(call: Call<Record>): AsyncGenerator<Record> {
|
||||
let processed = this.params.processed;
|
||||
let records: Record[] = [];
|
||||
|
||||
while (true) {
|
||||
records = await call(processed);
|
||||
if (records.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const record of records) {
|
||||
yield record;
|
||||
processed += 1;
|
||||
if (processed >= this.params.max) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
import { Channel } from '@waha/structures/channels.dto';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import {
|
||||
ChatRequest,
|
||||
MessageFileRequest,
|
||||
MessageImageRequest,
|
||||
MessageTextRequest,
|
||||
MessageVideoRequest,
|
||||
MessageVoiceRequest,
|
||||
WANumberExistResult,
|
||||
} from '@waha/structures/chatting.dto';
|
||||
import { SessionInfo } from '@waha/structures/sessions.dto';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { Auth } from '@waha/core/auth/config';
|
||||
import { ContactSortField } from '@waha/structures/contacts.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface RequestOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export class WAHASelf {
|
||||
public client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
// Set 'X-Api-Key'
|
||||
const key = Auth.keyplain.value;
|
||||
const port =
|
||||
parseInt(process.env.PORT) ||
|
||||
parseInt(process.env.WHATSAPP_API_PORT) ||
|
||||
3000;
|
||||
const url = `http://localhost:${port}`;
|
||||
this.client = axios.create({
|
||||
baseURL: url,
|
||||
headers: {
|
||||
'X-Api-Key': key,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetch(url: string, opts?: RequestOptions): Promise<Buffer> {
|
||||
const response = await this.client.get(url, {
|
||||
responseType: 'arraybuffer',
|
||||
signal: opts?.signal,
|
||||
});
|
||||
return Buffer.from(response.data);
|
||||
}
|
||||
|
||||
async qr(session: string, opts?: RequestOptions): Promise<Buffer> {
|
||||
const url = `/api/${session}/auth/qr`;
|
||||
return await this.fetch(url, opts);
|
||||
}
|
||||
|
||||
async screenshot(session: string, opts?: RequestOptions): Promise<Buffer> {
|
||||
const url = `/api/screenshot?session=${session}`;
|
||||
return await this.fetch(url, opts);
|
||||
}
|
||||
|
||||
async restart(session: string, opts?: RequestOptions): Promise<any> {
|
||||
const url = `/api/sessions/${session}/restart`;
|
||||
return await this.client.post(url, undefined, { signal: opts?.signal });
|
||||
}
|
||||
|
||||
async logout(session: string, opts?: RequestOptions): Promise<any> {
|
||||
const url = `/api/sessions/${session}/logout`;
|
||||
return await this.client.post(url, undefined, { signal: opts?.signal });
|
||||
}
|
||||
|
||||
async stop(session: string, opts?: RequestOptions): Promise<any> {
|
||||
const url = `/api/sessions/${session}/stop`;
|
||||
return await this.client.post(url, undefined, { signal: opts?.signal });
|
||||
}
|
||||
|
||||
async get(session: string, opts?: RequestOptions): Promise<SessionInfo> {
|
||||
const url = `/api/sessions/${session}/`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getChats(
|
||||
session: string,
|
||||
page: PaginationParams,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
const url = `/api/${session}/chats`;
|
||||
const params = {
|
||||
...page,
|
||||
};
|
||||
return await this.client
|
||||
.get(url, { params: params, signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getContacts(
|
||||
session: string,
|
||||
page: PaginationParams,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
const url = `/api/contacts/all`;
|
||||
const params = {
|
||||
session: session,
|
||||
...page,
|
||||
sortBy: ContactSortField.ID,
|
||||
sortOrder: ContactSortField.ID,
|
||||
};
|
||||
return await this.client
|
||||
.get(url, { params: params, signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getContact(session: string, contactId: string, opts?: RequestOptions) {
|
||||
const url = `/api/contacts`;
|
||||
const params = {
|
||||
session: session,
|
||||
contactId: contactId,
|
||||
};
|
||||
return await this.client
|
||||
.get(url, { params: params, signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async contactCheckExists(
|
||||
session: string,
|
||||
phone: string,
|
||||
opts?: RequestOptions,
|
||||
): Promise<WANumberExistResult> {
|
||||
const url = `/api/contacts/check-exists`;
|
||||
const params = {
|
||||
phone: phone,
|
||||
session: session,
|
||||
};
|
||||
return await this.client
|
||||
.get(url, { params: params, signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getGroup(session: string, groupId: string, opts?: RequestOptions) {
|
||||
const url = `/api/${session}/groups/${groupId}`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getGroupParticipants(
|
||||
session: string,
|
||||
groupId: string,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
const url = `/api/${session}/groups/${groupId}/participants/v2`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getChannel(
|
||||
session: string,
|
||||
channelId: string,
|
||||
opts?: RequestOptions,
|
||||
): Promise<Channel> {
|
||||
const url = `/api/${session}/channels/${channelId}`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getChatPicture(
|
||||
session: string,
|
||||
chatId: string,
|
||||
opts?: RequestOptions,
|
||||
): Promise<string | null> {
|
||||
const url = `/api/${session}/chats/${chatId}/picture`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data?.url);
|
||||
}
|
||||
|
||||
async sendText(
|
||||
body: MessageTextRequest,
|
||||
opts?: RequestOptions,
|
||||
): Promise<any> {
|
||||
const url = `/api/sendText`;
|
||||
return await this.client
|
||||
.post(url, body, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async sendImage(
|
||||
body: MessageImageRequest,
|
||||
opts?: RequestOptions,
|
||||
): Promise<any> {
|
||||
const url = `/api/sendImage`;
|
||||
return await this.client
|
||||
.post(url, body, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async sendVideo(
|
||||
body: MessageVideoRequest,
|
||||
opts?: RequestOptions,
|
||||
): Promise<any> {
|
||||
const url = `/api/sendVideo`;
|
||||
return await this.client
|
||||
.post(url, body, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async sendVoice(
|
||||
body: MessageVoiceRequest,
|
||||
opts?: RequestOptions,
|
||||
): Promise<any> {
|
||||
const url = `/api/sendVoice`;
|
||||
return await this.client
|
||||
.post(url, body, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async sendFile(
|
||||
body: MessageFileRequest,
|
||||
opts?: RequestOptions,
|
||||
): Promise<any> {
|
||||
const url = `/api/sendFile`;
|
||||
return await this.client
|
||||
.post(url, body, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async deleteMessage(
|
||||
session: string,
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
const url = `/api/${session}/chats/${chatId}/messages/${messageId}`;
|
||||
return await this.client
|
||||
.delete(url, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async startTyping(body: ChatRequest, opts?: RequestOptions) {
|
||||
const url = `/api/startTyping`;
|
||||
return await this.client
|
||||
.post(url, body, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async stopTyping(body: ChatRequest, opts?: RequestOptions) {
|
||||
const url = `/api/stopTyping`;
|
||||
return await this.client.post(url, body, { signal: opts?.signal });
|
||||
}
|
||||
|
||||
async readMessages(session: string, chatId: string, opts?: RequestOptions) {
|
||||
const url = `/api/${session}/chats/${chatId}/messages/read`;
|
||||
return await this.client
|
||||
.post(url, undefined, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getMessages(
|
||||
session: string,
|
||||
chatId: string,
|
||||
query: GetChatMessagesQuery,
|
||||
filter: GetChatMessagesFilter,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
const url = `/api/${session}/chats/${chatId}/messages`;
|
||||
const params = {
|
||||
...query,
|
||||
...filter,
|
||||
};
|
||||
return await this.client
|
||||
.get(url, { params: params, signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getMessageById(
|
||||
session: string,
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
media: boolean,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
const url = `/api/${session}/chats/${chatId}/messages/${messageId}`;
|
||||
const params = {
|
||||
downloadMedia: media,
|
||||
};
|
||||
return await this.client
|
||||
.get(url, { params: params, signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async findPNByLid(
|
||||
session: string,
|
||||
lid: string,
|
||||
opts?: RequestOptions,
|
||||
): Promise<string | null> {
|
||||
const url = `/api/${session}/lids/${lid}`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data.pn);
|
||||
}
|
||||
|
||||
async findLIDByPN(
|
||||
session: string,
|
||||
pn: string,
|
||||
opts?: RequestOptions,
|
||||
): Promise<string | null> {
|
||||
const url = `/api/${session}/lids/pn/${pn}`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data.lid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server version
|
||||
* @returns Server version information
|
||||
*/
|
||||
async serverVersion(opts?: RequestOptions): Promise<any> {
|
||||
const url = `/api/server/version`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server status
|
||||
* @returns Server status information
|
||||
*/
|
||||
async serverStatus(opts?: RequestOptions): Promise<any> {
|
||||
const url = `/api/server/status`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reboot the server
|
||||
* @param force Whether to force reboot (true) or gracefully reboot (false)
|
||||
* @returns Server stop response
|
||||
*/
|
||||
async serverReboot(
|
||||
force: boolean = false,
|
||||
opts?: RequestOptions,
|
||||
): Promise<any> {
|
||||
const url = `/api/server/stop`;
|
||||
return await this.client
|
||||
.post(url, { force }, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
}
|
||||
|
||||
export class WAHASessionAPI {
|
||||
constructor(
|
||||
private session: string,
|
||||
private api: WAHASelf,
|
||||
) {}
|
||||
|
||||
getChats(page: PaginationParams, opts?: RequestOptions) {
|
||||
return this.api.getChats(this.session, page, opts);
|
||||
}
|
||||
|
||||
getContacts(page: PaginationParams, opts?: RequestOptions): Promise<any> {
|
||||
return this.api.getContacts(this.session, page, opts);
|
||||
}
|
||||
|
||||
getContact(contactId: string, opts?: RequestOptions): Promise<any> {
|
||||
return this.api.getContact(this.session, contactId, opts);
|
||||
}
|
||||
|
||||
contactCheckExists(
|
||||
phone: string,
|
||||
opts?: RequestOptions,
|
||||
): Promise<WANumberExistResult> {
|
||||
return this.api.contactCheckExists(this.session, phone, opts);
|
||||
}
|
||||
|
||||
getGroup(groupId: string, opts?: RequestOptions): Promise<any> {
|
||||
return this.api.getGroup(this.session, groupId, opts);
|
||||
}
|
||||
|
||||
getGroupParticipants(groupId: string, opts?: RequestOptions): Promise<any> {
|
||||
return this.api.getGroupParticipants(this.session, groupId, opts);
|
||||
}
|
||||
|
||||
getChannel(channelId: string, opts?: RequestOptions): Promise<Channel> {
|
||||
return this.api.getChannel(this.session, channelId, opts);
|
||||
}
|
||||
|
||||
getChatPicture(
|
||||
chatId: string,
|
||||
opts?: RequestOptions,
|
||||
): Promise<string | null> {
|
||||
return this.api.getChatPicture(this.session, chatId, opts);
|
||||
}
|
||||
|
||||
sendText(body: MessageTextRequest, opts?: RequestOptions): Promise<any> {
|
||||
body.session = this.session;
|
||||
return this.api.sendText(body, opts);
|
||||
}
|
||||
|
||||
sendImage(body: MessageImageRequest, opts?: RequestOptions): Promise<any> {
|
||||
body.session = this.session;
|
||||
return this.api.sendImage(body, opts);
|
||||
}
|
||||
|
||||
sendVideo(body: MessageVideoRequest, opts?: RequestOptions): Promise<any> {
|
||||
body.session = this.session;
|
||||
return this.api.sendVideo(body, opts);
|
||||
}
|
||||
|
||||
sendVoice(body: MessageVoiceRequest, opts?: RequestOptions): Promise<any> {
|
||||
body.session = this.session;
|
||||
return this.api.sendVoice(body, opts);
|
||||
}
|
||||
|
||||
sendFile(body: MessageFileRequest, opts?: RequestOptions): Promise<any> {
|
||||
body.session = this.session;
|
||||
return this.api.sendFile(body, opts);
|
||||
}
|
||||
|
||||
deleteMessage(chatId: string, messageId: string, opts?: RequestOptions) {
|
||||
return this.api.deleteMessage(this.session, chatId, messageId, opts);
|
||||
}
|
||||
|
||||
startTyping(body: ChatRequest, opts?: RequestOptions) {
|
||||
body.session = this.session;
|
||||
return this.api.startTyping(body, opts);
|
||||
}
|
||||
|
||||
stopTyping(body: ChatRequest, opts?: RequestOptions) {
|
||||
body.session = this.session;
|
||||
return this.api.stopTyping(body, opts);
|
||||
}
|
||||
|
||||
readMessages(chatId: string, opts?: RequestOptions) {
|
||||
return this.api.readMessages(this.session, chatId, opts);
|
||||
}
|
||||
|
||||
async getMessages(
|
||||
chatId: string,
|
||||
query: GetChatMessagesQuery,
|
||||
filter: GetChatMessagesFilter,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
return this.api.getMessages(this.session, chatId, query, filter, opts);
|
||||
}
|
||||
|
||||
async getMessageById(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
media: boolean,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
return this.api.getMessageById(
|
||||
this.session,
|
||||
chatId,
|
||||
messageId,
|
||||
media,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Lids
|
||||
//
|
||||
findPNByLid(lid: string, opts?: RequestOptions) {
|
||||
return this.api.findPNByLid(this.session, lid, opts);
|
||||
}
|
||||
|
||||
findLIDByPN(pn: string, opts?: RequestOptions) {
|
||||
return this.api.findLIDByPN(this.session, pn, opts);
|
||||
}
|
||||
|
||||
getSessionInfo(opts?: RequestOptions) {
|
||||
return this.api.get(this.session, opts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { AppsDisabledService } from '@waha/apps/app_sdk/services/AppsDisabledService';
|
||||
import { AppsController } from '@waha/apps/app_sdk/api/apps.controller';
|
||||
import { ChatwootLocalesController } from '@waha/apps/chatwoot/api/chatwoot.locales.controller';
|
||||
|
||||
export const AppsDisabled = {
|
||||
providers: [
|
||||
{
|
||||
provide: AppsService,
|
||||
useClass: AppsDisabledService,
|
||||
},
|
||||
],
|
||||
imports: [],
|
||||
controllers: [AppsController, ChatwootLocalesController],
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { RedisModule } from '@liaoliaots/nestjs-redis';
|
||||
import { RMutexModule } from '@waha/modules/rmutex';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { ExpressAdapter } from '@bull-board/express';
|
||||
import { BullAuthMiddleware } from '@waha/apps/app_sdk/auth';
|
||||
import { ChatWootExports } from '@waha/apps/chatwoot/chatwoot.module';
|
||||
import { AppsController } from '@waha/apps/app_sdk/api/apps.controller';
|
||||
import { CallsAppExports } from '@waha/apps/calls/calls.module';
|
||||
import { AppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { AppsEnabledService } from '@waha/apps/app_sdk/services/AppsEnabledService';
|
||||
import { Auth } from '@waha/core/auth/config';
|
||||
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
|
||||
const QUEUES_IMPORTS_REQUIRED = [
|
||||
BullModule.forRoot({
|
||||
connection: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
maxRetriesPerRequest: null,
|
||||
},
|
||||
prefix: `waha-${process.env.WAHA_WORKER_ID}`,
|
||||
}),
|
||||
RedisModule.forRoot({
|
||||
closeClient: true,
|
||||
config: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
onClientCreated: async (client) => {
|
||||
try {
|
||||
await client.ping();
|
||||
} catch (err) {
|
||||
console.error('[Redis] Connection failed:', err);
|
||||
process.exit(1); // Stop the app if Redis is unavailable
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
RMutexModule,
|
||||
BullBoardModule.forRoot({
|
||||
route: '/jobs',
|
||||
adapter: ExpressAdapter,
|
||||
middleware: BullAuthMiddleware(),
|
||||
boardOptions: {
|
||||
uiConfig: {
|
||||
boardTitle: 'Jobs | WAHA',
|
||||
boardLogo: {
|
||||
path: '/dashboard/layout/images/logo-white.svg',
|
||||
width: 35,
|
||||
height: 35,
|
||||
},
|
||||
favIcon: {
|
||||
default: '/dashboard/favicon.ico',
|
||||
alternative: '/dashboard/favicon.ico',
|
||||
},
|
||||
miscLinks: [
|
||||
{
|
||||
text: '📊 Dashboard',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
text: '📚 Swagger (OpenAPI)',
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
const QUEUES_IMPORTS = AppRuntimeConfig.HasAppsRequiringQueue()
|
||||
? QUEUES_IMPORTS_REQUIRED
|
||||
: [];
|
||||
|
||||
function getAppModule(name: AppName) {
|
||||
if (!AppRuntimeConfig.HasApp(name)) {
|
||||
return {
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
};
|
||||
}
|
||||
switch (name) {
|
||||
case AppName.calls:
|
||||
return CallsAppExports;
|
||||
case AppName.chatwoot:
|
||||
return ChatWootExports;
|
||||
default:
|
||||
throw Error(`App module not found for ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const AppsEnabled = {
|
||||
imports: [
|
||||
...QUEUES_IMPORTS,
|
||||
...getAppModule(AppName.chatwoot).imports,
|
||||
...getAppModule(AppName.calls).imports,
|
||||
],
|
||||
controllers: [
|
||||
AppsController,
|
||||
...getAppModule(AppName.chatwoot).controllers,
|
||||
...getAppModule(AppName.calls).controllers,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: AppsService,
|
||||
useClass: AppsEnabledService,
|
||||
},
|
||||
...getAppModule(AppName.calls).providers,
|
||||
...getAppModule(AppName.chatwoot).providers,
|
||||
],
|
||||
};
|
||||
|
||||
function checkApiKey() {
|
||||
const key = Auth.key.value;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const plain = Auth.keyplain.value;
|
||||
if (!plain) {
|
||||
throw Error(
|
||||
'WAHA_API_KEY set, please provide WAHA_API_KEY_PLAIN when WAHA_APPS_ENABLED',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (AppRuntimeConfig.HasAppsRequiringPlainKey()) {
|
||||
checkApiKey();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
|
||||
import { AppsDisabled } from '@waha/apps/apps.module.disabled';
|
||||
import { AppsEnabled } from '@waha/apps/apps.module.enabled';
|
||||
|
||||
export const AppsModuleExports = AppRuntimeConfig.Enabled()
|
||||
? AppsEnabled
|
||||
: AppsDisabled;
|
||||
Loaded 100 of 502 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user