usofn8n commited on
Commit
80f4e3d
·
1 Parent(s): fb0b1e5

Add Evolution API files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +59 -0
  2. manager/dist/assets/index-CO3NSIFj.js +0 -0
  3. manager/dist/assets/index-DsIrum0U.css +1 -0
  4. manager/dist/index.html +14 -0
  5. package.json +156 -0
  6. src/@types/express.d.ts +9 -0
  7. src/api/abstract/abstract.cache.ts +19 -0
  8. src/api/abstract/abstract.repository.ts +66 -0
  9. src/api/abstract/abstract.router.ts +226 -0
  10. src/api/controllers/business.controller.ts +15 -0
  11. src/api/controllers/call.controller.ts +11 -0
  12. src/api/controllers/chat.controller.ts +116 -0
  13. src/api/controllers/group.controller.ts +84 -0
  14. src/api/controllers/instance.controller.ts +445 -0
  15. src/api/controllers/label.controller.ts +15 -0
  16. src/api/controllers/proxy.controller.ts +74 -0
  17. src/api/controllers/sendMessage.controller.ts +107 -0
  18. src/api/controllers/settings.controller.ts +16 -0
  19. src/api/controllers/template.controller.ts +15 -0
  20. src/api/dto/business.dto.ts +14 -0
  21. src/api/dto/call.dto.ts +8 -0
  22. src/api/dto/chat.dto.ts +129 -0
  23. src/api/dto/chatbot.dto.ts +12 -0
  24. src/api/dto/group.dto.ts +56 -0
  25. src/api/dto/instance.dto.ts +58 -0
  26. src/api/dto/label.dto.ts +12 -0
  27. src/api/dto/proxy.dto.ts +8 -0
  28. src/api/dto/sendMessage.dto.ts +169 -0
  29. src/api/dto/settings.dto.ts +10 -0
  30. src/api/dto/template.dto.ts +8 -0
  31. src/api/guards/auth.guard.ts +53 -0
  32. src/api/guards/instance.guard.ts +55 -0
  33. src/api/guards/telemetry.guard.ts +12 -0
  34. src/api/integrations/channel/channel.controller.ts +95 -0
  35. src/api/integrations/channel/channel.router.ts +17 -0
  36. src/api/integrations/channel/evolution/evolution.channel.service.ts +888 -0
  37. src/api/integrations/channel/evolution/evolution.controller.ts +39 -0
  38. src/api/integrations/channel/evolution/evolution.router.ts +18 -0
  39. src/api/integrations/channel/meta/meta.controller.ts +72 -0
  40. src/api/integrations/channel/meta/meta.router.ts +24 -0
  41. src/api/integrations/channel/meta/whatsapp.business.service.ts +1755 -0
  42. src/api/integrations/channel/whatsapp/baileys.controller.ts +60 -0
  43. src/api/integrations/channel/whatsapp/baileys.router.ts +105 -0
  44. src/api/integrations/channel/whatsapp/baileysMessage.processor.ts +59 -0
  45. src/api/integrations/channel/whatsapp/voiceCalls/transport.type.ts +78 -0
  46. src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts +181 -0
  47. src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +0 -0
  48. src/api/integrations/chatbot/base-chatbot.controller.ts +950 -0
  49. src/api/integrations/chatbot/base-chatbot.dto.ts +42 -0
  50. src/api/integrations/chatbot/base-chatbot.service.ts +419 -0
Dockerfile ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage
2
+ FROM node:20-alpine AS builder
3
+
4
+ # Install build tools
5
+ RUN apk update && apk add --no-cache git bash openssl ffmpeg tzdata dos2unix
6
+
7
+ WORKDIR /evolution
8
+
9
+ # Copy project files
10
+ COPY package*.json ./
11
+ COPY tsconfig.json ./
12
+ COPY tsup.config.ts ./
13
+ RUN npm ci --silent
14
+
15
+ COPY src ./src
16
+ COPY public ./public
17
+ COPY prisma ./prisma
18
+ COPY manager ./manager
19
+ COPY .env.example ./.env
20
+ COPY runWithProvider.js ./
21
+ COPY Docker ./Docker
22
+
23
+ RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/*
24
+
25
+ # Generate SQLite database schema (instead of Postgres)
26
+ RUN ./Docker/scripts/generate_database.sh
27
+
28
+ # Build project
29
+ RUN npm run build
30
+
31
+ # Final stage
32
+ FROM node:20-alpine AS final
33
+
34
+ RUN apk update && apk add --no-cache tzdata ffmpeg bash openssl
35
+
36
+ WORKDIR /evolution
37
+
38
+ COPY --from=builder /evolution/package.json ./package.json
39
+ COPY --from=builder /evolution/package-lock.json ./package-lock.json
40
+ COPY --from=builder /evolution/node_modules ./node_modules
41
+ COPY --from=builder /evolution/dist ./dist
42
+ COPY --from=builder /evolution/prisma ./prisma
43
+ COPY --from=builder /evolution/manager ./manager
44
+ COPY --from=builder /evolution/public ./public
45
+ COPY --from=builder /evolution/.env ./.env
46
+ COPY --from=builder /evolution/Docker ./Docker
47
+ COPY --from=builder /evolution/runWithProvider.js ./runWithProvider.js
48
+ COPY --from=builder /evolution/tsup.config.ts ./tsup.config.ts
49
+
50
+ # Hugging Face Spaces requires binding to $PORT
51
+ ENV HOST=0.0.0.0
52
+ ENV PORT=7860
53
+ ENV TZ=Africa/Cairo
54
+ ENV DOCKER_ENV=true
55
+
56
+ EXPOSE 7860
57
+
58
+ # Start API with SQLite (no Redis/Postgres)
59
+ ENTRYPOINT ["/bin/bash", "-c", "npm run start:prod"]
manager/dist/assets/index-CO3NSIFj.js ADDED
The diff for this file is too large to render. See raw diff
 
manager/dist/assets/index-DsIrum0U.css ADDED
@@ -0,0 +1 @@
 
 
1
+ @import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap";*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}:root{--gradient: #093028;--background: 178 98.4% 98.22%;--foreground: 178 6.800000000000001% .44%;--muted: 178 6.800000000000001% 91.1%;--muted-foreground: 178 3.4000000000000004% 41.1%;--popover: 178 35.599999999999994% 91.1%;--popover-foreground: 178 6.800000000000001% .55%;--card: 178 35.599999999999994% 91.1%;--card-foreground: 178 6.800000000000001% .55%;--border: 178 11.8% 89.44%;--input: 178 11.8% 89.44%;--primary: 178 68% 11%;--primary-foreground: 178 1.36% 91.1%;--secondary: 178 3.4000000000000004% 95.55%;--secondary-foreground: 178 5.08% 11.1%;--accent: 178 3.4000000000000004% 95.55%;--accent-foreground: 178 5.08% 11.1%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--ring: 178 68% 11%;--radius: .5rem}.dark{--gradient: #189d68;--background: 166 47.449999999999996% 2.88%;--foreground: 166 7.3% 96.8%;--muted: 166 36.5% 10.799999999999999%;--muted-foreground: 166 7.3% 53.6%;--popover: 166 50.4% 4.68%;--popover-foreground: 166 7.3% 96.8%;--card: 166 50.4% 4.68%;--card-foreground: 166 7.3% 96.8%;--border: 166 36.5% 10.799999999999999%;--input: 166 36.5% 10.799999999999999%;--primary: 166 73% 36%;--primary-foreground: 166 7.3% 96.8%;--secondary: 166 36.5% 10.799999999999999%;--secondary-foreground: 166 7.3% 96.8%;--accent: 166 36.5% 10.799999999999999%;--accent-foreground: 166 7.3% 96.8%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 166 7.3% 96.8%;--ring: 166 73% 36%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground));font-family:Inter,sans-serif;scrollbar-width:thin;scrollbar-color:transparent transparent}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%;margin-right:auto;margin-left:auto;padding-right:2rem;padding-left:2rem}@media (min-width: 1400px){.container{max-width:1400px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-bottom-4{bottom:-1rem}.bottom-0{bottom:0}.left-0{left:0}.left-2{left:.5rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-4{top:1rem}.z-10{z-index:10}.z-50{z-index:50}.-m-2{margin:-.5rem}.m-4{margin:1rem}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-16{margin-right:4rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-auto{margin-top:auto}.box-border{box-sizing:border-box}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.aspect-video{aspect-ratio:16 / 9}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-24{height:6rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[1\.2rem\]{height:1.2rem}.h-\[1px\]{height:1px}.h-\[calc\(100vh-160px\)\]{height:calc(100vh - 160px)}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-32{max-height:8rem}.max-h-96{max-height:24rem}.max-h-\[200px\]{max-height:200px}.max-h-\[300px\]{max-height:300px}.min-h-0{min-height:0px}.min-h-\[80px\]{min-height:80px}.min-h-\[calc\(100vh_-_56px\)\]{min-height:calc(100vh - 56px)}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-1{width:.25rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[1\.2rem\]{width:1.2rem}.w-\[1px\]{width:1px}.w-\[300px\]{width:300px}.w-\[350px\]{width:350px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[280px\]{min-width:280px}.min-w-\[80px\]{min-width:80px}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-2xl{max-width:42rem}.max-w-32{max-width:8rem}.max-w-40{max-width:10rem}.max-w-4xl{max-width:56rem}.max-w-\[300px\]{max-width:300px}.max-w-\[320px\]{max-width:320px}.max-w-\[60\%\]{max-width:60%}.max-w-\[64rem\]{max-width:64rem}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.caption-bottom{caption-side:bottom}.translate-y-1{--tw-translate-y: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-0{--tw-rotate: 0deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-0{--tw-scale-x: 0;--tw-scale-y: 0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.grid-cols-\[repeat\(auto-fit\,_minmax\(15rem\,_1fr\)\)\]{grid-template-columns:repeat(auto-fit,minmax(15rem,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-x>:not([hidden])~:not([hidden]){--tw-divide-x-reverse: 0;border-right-width:calc(1px * var(--tw-divide-x-reverse));border-left-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.self-end{align-self:flex-end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-ellipsis,.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-wrap{text-wrap:wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[16px\]{border-radius:16px}.rounded-\[2px\]{border-radius:2px}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-none{border-radius:0}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:.75rem}.rounded-l-lg{border-top-left-radius:var(--radius);border-bottom-left-radius:var(--radius)}.border{border-width:1px}.border-2{border-width:2px}.border-\[1\.5px\]{border-width:1.5px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-\[--color-border\]{border-color:var(--color-border)}.border-amber-500\/20{border-color:#f59e0b33}.border-black{--tw-border-opacity: 1;border-color:rgb(0 0 0 / var(--tw-border-opacity))}.border-black\/10{border-color:#0000001a}.border-border{border-color:hsl(var(--border))}.border-border\/50{border-color:hsl(var(--border) / .5)}.border-emerald-500\/20{border-color:#10b98133}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.border-gray-600\/50{border-color:#4b556380}.border-input{border-color:hsl(var(--input))}.border-muted{border-color:hsl(var(--muted))}.border-red-500\/20{border-color:#ef444433}.border-sky-500\/20{border-color:#0ea5e933}.border-slate-600{--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-zinc-500\/20{border-color:#71717a33}.border-l-transparent{border-left-color:transparent}.border-t-transparent{border-top-color:transparent}.bg-\[\#b2ece0\]{--tw-bg-opacity: 1;background-color:rgb(178 236 224 / var(--tw-bg-opacity))}.bg-\[\#c8fff2\]{--tw-bg-opacity: 1;background-color:rgb(200 255 242 / var(--tw-bg-opacity))}.bg-\[\#d2e2e2\]{--tw-bg-opacity: 1;background-color:rgb(210 226 226 / var(--tw-bg-opacity))}.bg-\[\#e0f0f0\]{--tw-bg-opacity: 1;background-color:rgb(224 240 240 / var(--tw-bg-opacity))}.bg-\[--color-bg\]{background-color:var(--color-bg)}.bg-amber-50\/50{background-color:#fffbeb80}.bg-amber-600{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity))}.bg-background{background-color:hsl(var(--background))}.bg-background\/80{background-color:hsl(var(--background) / .8)}.bg-black\/10{background-color:#0000001a}.bg-black\/5{background-color:#0000000d}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity))}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-emerald-50\/50{background-color:#ecfdf580}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/50{background-color:hsl(var(--muted) / .5)}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/20{background-color:hsl(var(--primary) / .2)}.bg-primary\/30{background-color:hsl(var(--primary) / .3)}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-red-50\/50{background-color:#fef2f280}.bg-secondary{background-color:hsl(var(--secondary))}.bg-sky-50\/50{background-color:#f0f9ff80}.bg-slate-700{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity))}.bg-zinc-50\/50{background-color:#fafafa80}.fill-current{fill:currentColor}.fill-gray-100{fill:#f3f4f6}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.p-\[0\.375rem_1rem_0_1rem\]{padding:.375rem 1rem 0}.p-\[1px\]{padding:1px}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-3{padding-bottom:.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-5{padding-top:1.25rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-widest{letter-spacing:.1em}.text-\[\#008069\]{--tw-text-opacity: 1;color:rgb(0 128 105 / var(--tw-text-opacity))}.text-\[\#b03f3f\]{--tw-text-opacity: 1;color:rgb(176 63 63 / var(--tw-text-opacity))}.text-amber-100{--tw-text-opacity: 1;color:rgb(254 243 199 / var(--tw-text-opacity))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-900{--tw-text-opacity: 1;color:rgb(6 78 59 / var(--tw-text-opacity))}.text-foreground{color:hsl(var(--foreground))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/80{color:hsl(var(--muted-foreground) / .8)}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity))}.text-rose-600{--tw-text-opacity: 1;color:rgb(225 29 72 / var(--tw-text-opacity))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sky-900{--tw-text-opacity: 1;color:rgb(12 74 110 / var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-zinc-900{--tw-text-opacity: 1;color:rgb(24 24 27 / var(--tw-text-opacity))}.underline-offset-4{text-underline-offset:4px}.caret-transparent{caret-color:transparent}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-muted-foreground{--tw-ring-color: hsl(var(--muted-foreground))}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.duration-200{animation-duration:.2s}.duration-300{animation-duration:.3s}.paused{animation-play-state:paused}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-y-0:after{content:var(--tw-content);top:0;bottom:0}.after\:bottom-\[12px\]:after{content:var(--tw-content);bottom:12px}.after\:left-1\/2:after{content:var(--tw-content);left:50%}.after\:w-1:after{content:var(--tw-content);width:.25rem}.after\:-translate-x-1\/2:after{content:var(--tw-content);--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.after\:border-\[8px\]:after{content:var(--tw-content);border-width:8px}.after\:border-solid:after{content:var(--tw-content);border-style:solid}.after\:bg-border:after{content:var(--tw-content);background-color:hsl(var(--border))}.hover\:bg-\[\#a4ecde\]:hover{--tw-bg-opacity: 1;background-color:rgb(164 236 222 / var(--tw-bg-opacity))}.hover\:bg-\[\#b2ece0\]:hover{--tw-bg-opacity: 1;background-color:rgb(178 236 224 / var(--tw-bg-opacity))}.hover\:bg-\[\#c2d2d2\]:hover{--tw-bg-opacity: 1;background-color:rgb(194 210 210 / var(--tw-bg-opacity))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-amber-600\/80:hover{background-color:#d97706cc}.hover\:bg-amber-600\/90:hover{background-color:#d97706e6}.hover\:bg-black\/10:hover{background-color:#0000001a}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive) / .8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary) / .8)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:stroke-destructive:hover{stroke:hsl(var(--destructive))}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-0:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-transparent:focus-visible{--tw-ring-color: transparent}.focus-visible\:ring-offset-0:focus-visible{--tw-ring-offset-width: 0px}.focus-visible\:ring-offset-1:focus-visible{--tw-ring-offset-width: 1px}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.focus-visible\:ring-offset-transparent:focus-visible{--tw-ring-offset-color: transparent}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:visible{visibility:visible}.group:hover .group-hover\:opacity-100{opacity:1}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.aria-selected\:bg-accent[aria-selected=true]{background-color:hsl(var(--accent))}.aria-selected\:text-accent-foreground[aria-selected=true]{color:hsl(var(--accent-foreground))}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[panel-group-direction\=vertical\]\:h-px[data-panel-group-direction=vertical]{height:1px}.data-\[panel-group-direction\=vertical\]\:w-full[data-panel-group-direction=vertical]{width:100%}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=checked\]\:translate-x-5[data-state=checked]{--tw-translate-x: 1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[panel-group-direction\=vertical\]\:flex-col[data-panel-group-direction=vertical]{flex-direction:column}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=active\]\:bg-primary[data-state=active],.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:hsl(var(--accent))}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:hsl(var(--muted))}.data-\[state\=unchecked\]\:bg-slate-400[data-state=unchecked]{--tw-bg-opacity: 1;background-color:rgb(148 163 184 / var(--tw-bg-opacity))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=active\]\:text-primary-foreground[data-state=active]{color:hsl(var(--primary-foreground))}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:hsl(var(--muted-foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale: .95}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale: .95}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y: -.5rem}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x: .5rem}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x: -.5rem}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y: .5rem}.data-\[state\=closed\]\:slide-out-to-left-1\/2[data-state=closed]{--tw-exit-translate-x: -50%}.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y: -48%}.data-\[state\=open\]\:slide-in-from-left-1\/2[data-state=open]{--tw-enter-translate-x: -50%}.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y: -48%}.data-\[panel-group-direction\=vertical\]\:after\:left-0[data-panel-group-direction=vertical]:after{content:var(--tw-content);left:0}.data-\[panel-group-direction\=vertical\]\:after\:h-1[data-panel-group-direction=vertical]:after{content:var(--tw-content);height:.25rem}.data-\[panel-group-direction\=vertical\]\:after\:w-full[data-panel-group-direction=vertical]:after{content:var(--tw-content);width:100%}.data-\[panel-group-direction\=vertical\]\:after\:-translate-y-1\/2[data-panel-group-direction=vertical]:after{content:var(--tw-content);--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[panel-group-direction\=vertical\]\:after\:translate-x-0[data-panel-group-direction=vertical]:after{content:var(--tw-content);--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:-rotate-90:is(.dark *){--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:rotate-0:is(.dark *){--tw-rotate: 0deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:scale-0:is(.dark *){--tw-scale-x: 0;--tw-scale-y: 0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:scale-100:is(.dark *){--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:border-amber-500\/30:is(.dark *){border-color:#f59e0b4d}.dark\:border-emerald-500\/30:is(.dark *){border-color:#10b9814d}.dark\:border-gray-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:border-red-500\/30:is(.dark *){border-color:#ef44444d}.dark\:border-sky-500\/30:is(.dark *){border-color:#0ea5e94d}.dark\:border-white\/10:is(.dark *){border-color:#ffffff1a}.dark\:border-zinc-500\/30:is(.dark *){border-color:#71717a4d}.dark\:bg-\[\#082720\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(8 39 32 / var(--tw-bg-opacity))}.dark\:bg-\[\#0b332a\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(11 51 42 / var(--tw-bg-opacity))}.dark\:bg-\[\#0f1413\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 20 19 / var(--tw-bg-opacity))}.dark\:bg-\[\#1d2724\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(29 39 36 / var(--tw-bg-opacity))}.dark\:bg-amber-500\/10:is(.dark *){background-color:#f59e0b1a}.dark\:bg-blue-300:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(147 197 253 / var(--tw-bg-opacity))}.dark\:bg-emerald-500\/10:is(.dark *){background-color:#10b9811a}.dark\:bg-gray-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.dark\:bg-gray-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.dark\:bg-red-500\/10:is(.dark *){background-color:#ef44441a}.dark\:bg-red-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.dark\:bg-sky-500\/10:is(.dark *){background-color:#0ea5e91a}.dark\:bg-white\/10:is(.dark *){background-color:#ffffff1a}.dark\:bg-white\/5:is(.dark *){background-color:#ffffff0d}.dark\:bg-yellow-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(66 32 6 / var(--tw-bg-opacity))}.dark\:bg-zinc-500\/10:is(.dark *){background-color:#71717a1a}.dark\:fill-gray-800:is(.dark *){fill:#1f2937}.dark\:text-\[\#00a884\]:is(.dark *){--tw-text-opacity: 1;color:rgb(0 168 132 / var(--tw-text-opacity))}.dark\:text-amber-200:is(.dark *){--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity))}.dark\:text-blue-300:is(.dark *){--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity))}.dark\:text-emerald-200:is(.dark *){--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity))}.dark\:text-gray-100:is(.dark *){--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity))}.dark\:text-gray-300:is(.dark *){--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.dark\:text-gray-400:is(.dark *){--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.dark\:text-red-200:is(.dark *){--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}.dark\:text-sky-200:is(.dark *){--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity))}.dark\:text-white:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\:text-zinc-300:is(.dark *){--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity))}.dark\:hover\:bg-\[\#071f19\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(7 31 25 / var(--tw-bg-opacity))}.dark\:hover\:bg-\[\#082720\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(8 39 32 / var(--tw-bg-opacity))}.dark\:hover\:bg-\[\#141a18\]:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 26 24 / var(--tw-bg-opacity))}.dark\:hover\:bg-white\/10:hover:is(.dark *){background-color:#ffffff1a}@media (min-width: 640px){.sm\:m-4{margin:1rem}.sm\:inline{display:inline}.sm\:max-h-\[600px\]{max-height:600px}.sm\:max-w-\[650px\]{max-width:650px}.sm\:max-w-\[740px\]{max-width:740px}.sm\:max-w-\[950px\]{max-width:950px}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-\[10rem_1fr_10rem\]{grid-template-columns:10rem 1fr 10rem}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media (min-width: 768px){.md\:inline{display:inline}.md\:flex{display:flex}.md\:w-64{width:16rem}.md\:w-full{width:100%}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:gap-8{gap:2rem}}@media (min-width: 1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:0}.\[\&\>\*\]\:p-4>*{padding:1rem}.\[\&\>\*\]\:px-4>*{padding-left:1rem;padding-right:1rem}.\[\&\>\*\]\:py-2>*{padding-top:.5rem;padding-bottom:.5rem}.\[\&\>div\[style\]\]\:\!block>div[style]{display:block!important}.\[\&\>div\[style\]\]\:h-full>div[style]{height:100%}.\[\&\>span\]\:line-clamp-1>span{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1}.\[\&\>svg\+div\]\:translate-y-\[-3px\]>svg+div{--tw-translate-y: -3px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\[\&\>svg\]\:absolute>svg{position:absolute}.\[\&\>svg\]\:left-4>svg{left:1rem}.\[\&\>svg\]\:top-4>svg{top:1rem}.\[\&\>svg\]\:h-2\.5>svg{height:.625rem}.\[\&\>svg\]\:h-3>svg{height:.75rem}.\[\&\>svg\]\:w-2\.5>svg{width:.625rem}.\[\&\>svg\]\:w-3>svg{width:.75rem}.\[\&\>svg\]\:fill-rose-600>svg{fill:#e11d48}.\[\&\>svg\]\:text-amber-500>svg{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity))}.\[\&\>svg\]\:text-emerald-600>svg{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity))}.\[\&\>svg\]\:text-foreground>svg{color:hsl(var(--foreground))}.\[\&\>svg\]\:text-muted-foreground>svg{color:hsl(var(--muted-foreground))}.\[\&\>svg\]\:text-red-600>svg{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.\[\&\>svg\]\:text-sky-500>svg{--tw-text-opacity: 1;color:rgb(14 165 233 / var(--tw-text-opacity))}.\[\&\>svg\]\:text-zinc-400>svg{--tw-text-opacity: 1;color:rgb(161 161 170 / var(--tw-text-opacity))}.hover\:\[\&\>svg\]\:fill-rose-700>svg:hover{fill:#be123c}.dark\:\[\&\>svg\]\:text-emerald-400\/80>svg:is(.dark *){color:#34d399cc}.dark\:\[\&\>svg\]\:text-red-400\/80>svg:is(.dark *){color:#f87171cc}.dark\:\[\&\>svg\]\:text-zinc-300>svg:is(.dark *){--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity))}.\[\&\>svg\~\*\]\:pl-7>svg~*{padding-left:1.75rem}.\[\&\>tr\]\:last\:border-b-0:last-child>tr{border-bottom-width:0px}.\[\&\[data-panel-group-direction\=vertical\]\>div\]\:rotate-90[data-panel-group-direction=vertical]>div{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\[\&_\.recharts-cartesian-axis-tick_text\]\:fill-muted-foreground .recharts-cartesian-axis-tick text{fill:hsl(var(--muted-foreground))}.\[\&_\.recharts-cartesian-grid_line\[stroke\=\'\#ccc\'\]\]\:stroke-border\/50 .recharts-cartesian-grid line[stroke="#ccc"]{stroke:hsl(var(--border) / .5)}.\[\&_\.recharts-curve\.recharts-tooltip-cursor\]\:stroke-border .recharts-curve.recharts-tooltip-cursor{stroke:hsl(var(--border))}.\[\&_\.recharts-dot\[stroke\=\'\#fff\'\]\]\:stroke-transparent .recharts-dot[stroke="#fff"]{stroke:transparent}.\[\&_\.recharts-layer\]\:outline-none .recharts-layer{outline:2px solid transparent;outline-offset:2px}.\[\&_\.recharts-polar-grid_\[stroke\=\'\#ccc\'\]\]\:stroke-border .recharts-polar-grid [stroke="#ccc"]{stroke:hsl(var(--border))}.\[\&_\.recharts-radial-bar-background-sector\]\:fill-muted .recharts-radial-bar-background-sector,.\[\&_\.recharts-rectangle\.recharts-tooltip-cursor\]\:fill-muted .recharts-rectangle.recharts-tooltip-cursor{fill:hsl(var(--muted))}.\[\&_\.recharts-reference-line_\[stroke\=\'\#ccc\'\]\]\:stroke-border .recharts-reference-line [stroke="#ccc"]{stroke:hsl(var(--border))}.\[\&_\.recharts-sector\[stroke\=\'\#fff\'\]\]\:stroke-transparent .recharts-sector[stroke="#fff"]{stroke:transparent}.\[\&_\.recharts-sector\]\:outline-none .recharts-sector,.\[\&_\.recharts-surface\]\:outline-none .recharts-surface{outline:2px solid transparent;outline-offset:2px}.\[\&_\[cmdk-group-heading\]\]\:px-2 [cmdk-group-heading]{padding-left:.5rem;padding-right:.5rem}.\[\&_\[cmdk-group-heading\]\]\:py-1\.5 [cmdk-group-heading]{padding-top:.375rem;padding-bottom:.375rem}.\[\&_\[cmdk-group-heading\]\]\:text-xs [cmdk-group-heading]{font-size:.75rem;line-height:1rem}.\[\&_\[cmdk-group-heading\]\]\:font-medium [cmdk-group-heading]{font-weight:500}.\[\&_\[cmdk-group-heading\]\]\:text-muted-foreground [cmdk-group-heading]{color:hsl(var(--muted-foreground))}.\[\&_\[cmdk-group\]\:not\(\[hidden\]\)_\~\[cmdk-group\]\]\:pt-0 [cmdk-group]:not([hidden])~[cmdk-group]{padding-top:0}.\[\&_\[cmdk-group\]\]\:px-2 [cmdk-group]{padding-left:.5rem;padding-right:.5rem}.\[\&_\[cmdk-input-wrapper\]_svg\]\:h-5 [cmdk-input-wrapper] svg{height:1.25rem}.\[\&_\[cmdk-input-wrapper\]_svg\]\:w-5 [cmdk-input-wrapper] svg{width:1.25rem}.\[\&_\[cmdk-input\]\]\:h-12 [cmdk-input]{height:3rem}.\[\&_\[cmdk-item\]\]\:px-2 [cmdk-item]{padding-left:.5rem;padding-right:.5rem}.\[\&_\[cmdk-item\]\]\:py-3 [cmdk-item]{padding-top:.75rem;padding-bottom:.75rem}.\[\&_\[cmdk-item\]_svg\]\:h-5 [cmdk-item] svg{height:1.25rem}.\[\&_\[cmdk-item\]_svg\]\:w-5 [cmdk-item] svg{width:1.25rem}.\[\&_p\]\:leading-relaxed p{line-height:1.625}.\[\&_strong\]\:text-foreground strong{color:hsl(var(--foreground))}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}:root{--toastify-color-light: #fff;--toastify-color-dark: #121212;--toastify-color-info: #3498db;--toastify-color-success: #07bc0c;--toastify-color-warning: #f1c40f;--toastify-color-error: #e74c3c;--toastify-color-transparent: rgba(255, 255, 255, .7);--toastify-icon-color-info: var(--toastify-color-info);--toastify-icon-color-success: var(--toastify-color-success);--toastify-icon-color-warning: var(--toastify-color-warning);--toastify-icon-color-error: var(--toastify-color-error);--toastify-toast-width: 320px;--toastify-toast-offset: 16px;--toastify-toast-top: max(var(--toastify-toast-offset), env(safe-area-inset-top));--toastify-toast-right: max(var(--toastify-toast-offset), env(safe-area-inset-right));--toastify-toast-left: max(var(--toastify-toast-offset), env(safe-area-inset-left));--toastify-toast-bottom: max(var(--toastify-toast-offset), env(safe-area-inset-bottom));--toastify-toast-background: #fff;--toastify-toast-min-height: 64px;--toastify-toast-max-height: 800px;--toastify-toast-bd-radius: 6px;--toastify-font-family: sans-serif;--toastify-z-index: 9999;--toastify-text-color-light: #757575;--toastify-text-color-dark: #fff;--toastify-text-color-info: #fff;--toastify-text-color-success: #fff;--toastify-text-color-warning: #fff;--toastify-text-color-error: #fff;--toastify-spinner-color: #616161;--toastify-spinner-color-empty-area: #e0e0e0;--toastify-color-progress-light: linear-gradient( to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55 );--toastify-color-progress-dark: #bb86fc;--toastify-color-progress-info: var(--toastify-color-info);--toastify-color-progress-success: var(--toastify-color-success);--toastify-color-progress-warning: var(--toastify-color-warning);--toastify-color-progress-error: var(--toastify-color-error);--toastify-color-progress-bgo: .2}.Toastify__toast-container{z-index:var(--toastify-z-index);-webkit-transform:translate3d(0,0,var(--toastify-z-index));position:fixed;padding:4px;width:var(--toastify-toast-width);box-sizing:border-box;color:#fff}.Toastify__toast-container--top-left{top:var(--toastify-toast-top);left:var(--toastify-toast-left)}.Toastify__toast-container--top-center{top:var(--toastify-toast-top);left:50%;transform:translate(-50%)}.Toastify__toast-container--top-right{top:var(--toastify-toast-top);right:var(--toastify-toast-right)}.Toastify__toast-container--bottom-left{bottom:var(--toastify-toast-bottom);left:var(--toastify-toast-left)}.Toastify__toast-container--bottom-center{bottom:var(--toastify-toast-bottom);left:50%;transform:translate(-50%)}.Toastify__toast-container--bottom-right{bottom:var(--toastify-toast-bottom);right:var(--toastify-toast-right)}@media only screen and (max-width : 480px){.Toastify__toast-container{width:100vw;padding:0;left:env(safe-area-inset-left);margin:0}.Toastify__toast-container--top-left,.Toastify__toast-container--top-center,.Toastify__toast-container--top-right{top:env(safe-area-inset-top);transform:translate(0)}.Toastify__toast-container--bottom-left,.Toastify__toast-container--bottom-center,.Toastify__toast-container--bottom-right{bottom:env(safe-area-inset-bottom);transform:translate(0)}.Toastify__toast-container--rtl{right:env(safe-area-inset-right);left:initial}}.Toastify__toast{--y: 0;position:relative;touch-action:none;min-height:var(--toastify-toast-min-height);box-sizing:border-box;margin-bottom:1rem;padding:8px;border-radius:var(--toastify-toast-bd-radius);box-shadow:0 4px 12px #0000001a;display:flex;justify-content:space-between;max-height:var(--toastify-toast-max-height);font-family:var(--toastify-font-family);cursor:default;direction:ltr;z-index:0;overflow:hidden}.Toastify__toast--stacked{position:absolute;width:100%;transform:translate3d(0,var(--y),0) scale(var(--s));transition:transform .3s}.Toastify__toast--stacked[data-collapsed] .Toastify__toast-body,.Toastify__toast--stacked[data-collapsed] .Toastify__close-button{transition:opacity .1s}.Toastify__toast--stacked[data-collapsed=false]{overflow:visible}.Toastify__toast--stacked[data-collapsed=true]:not(:last-child)>*{opacity:0}.Toastify__toast--stacked:after{content:"";position:absolute;left:0;right:0;height:calc(var(--g) * 1px);bottom:100%}.Toastify__toast--stacked[data-pos=top]{top:0}.Toastify__toast--stacked[data-pos=bot]{bottom:0}.Toastify__toast--stacked[data-pos=bot].Toastify__toast--stacked:before{transform-origin:top}.Toastify__toast--stacked[data-pos=top].Toastify__toast--stacked:before{transform-origin:bottom}.Toastify__toast--stacked:before{content:"";position:absolute;left:0;right:0;bottom:0;height:100%;transform:scaleY(3);z-index:-1}.Toastify__toast--rtl{direction:rtl}.Toastify__toast--close-on-click{cursor:pointer}.Toastify__toast-body{margin:auto 0;flex:1 1 auto;padding:6px;display:flex;align-items:center}.Toastify__toast-body>div:last-child{word-break:break-word;flex:1}.Toastify__toast-icon{margin-inline-end:10px;width:20px;flex-shrink:0;display:flex}.Toastify--animate{animation-fill-mode:both;animation-duration:.5s}.Toastify--animate-icon{animation-fill-mode:both;animation-duration:.3s}@media only screen and (max-width : 480px){.Toastify__toast{margin-bottom:0;border-radius:0}}.Toastify__toast-theme--dark{background:var(--toastify-color-dark);color:var(--toastify-text-color-dark)}.Toastify__toast-theme--light,.Toastify__toast-theme--colored.Toastify__toast--default{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--info{color:var(--toastify-text-color-info);background:var(--toastify-color-info)}.Toastify__toast-theme--colored.Toastify__toast--success{color:var(--toastify-text-color-success);background:var(--toastify-color-success)}.Toastify__toast-theme--colored.Toastify__toast--warning{color:var(--toastify-text-color-warning);background:var(--toastify-color-warning)}.Toastify__toast-theme--colored.Toastify__toast--error{color:var(--toastify-text-color-error);background:var(--toastify-color-error)}.Toastify__progress-bar-theme--light{background:var(--toastify-color-progress-light)}.Toastify__progress-bar-theme--dark{background:var(--toastify-color-progress-dark)}.Toastify__progress-bar--info{background:var(--toastify-color-progress-info)}.Toastify__progress-bar--success{background:var(--toastify-color-progress-success)}.Toastify__progress-bar--warning{background:var(--toastify-color-progress-warning)}.Toastify__progress-bar--error{background:var(--toastify-color-progress-error)}.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--error{background:var(--toastify-color-transparent)}.Toastify__close-button{color:#fff;background:transparent;outline:none;border:none;padding:0;cursor:pointer;opacity:.7;transition:.3s ease;align-self:flex-start;z-index:1}.Toastify__close-button--light{color:#000;opacity:.3}.Toastify__close-button>svg{fill:currentColor;height:16px;width:14px}.Toastify__close-button:hover,.Toastify__close-button:focus{opacity:1}@keyframes Toastify__trackProgress{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.Toastify__progress-bar{position:absolute;bottom:0;left:0;width:100%;height:100%;z-index:var(--toastify-z-index);opacity:.7;transform-origin:left;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--animated{animation:Toastify__trackProgress linear 1 forwards}.Toastify__progress-bar--controlled{transition:transform .2s}.Toastify__progress-bar--rtl{right:0;left:initial;transform-origin:right;border-bottom-left-radius:initial;border-bottom-right-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp{position:absolute;bottom:0;left:0;width:100%;height:5px;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp[data-hidden=true]{opacity:0}.Toastify__progress-bar--bg{opacity:var(--toastify-color-progress-bgo);width:100%;height:100%}.Toastify__spinner{width:20px;height:20px;box-sizing:border-box;border:2px solid;border-radius:100%;border-color:var(--toastify-spinner-color-empty-area);border-right-color:var(--toastify-spinner-color);animation:Toastify__spin .65s linear infinite}@keyframes Toastify__bounceInRight{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(3000px,0,0)}60%{opacity:1;transform:translate3d(-25px,0,0)}75%{transform:translate3d(10px,0,0)}90%{transform:translate3d(-5px,0,0)}to{transform:none}}@keyframes Toastify__bounceOutRight{20%{opacity:1;transform:translate3d(-20px,var(--y),0)}to{opacity:0;transform:translate3d(2000px,var(--y),0)}}@keyframes Toastify__bounceInLeft{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(-3000px,0,0)}60%{opacity:1;transform:translate3d(25px,0,0)}75%{transform:translate3d(-10px,0,0)}90%{transform:translate3d(5px,0,0)}to{transform:none}}@keyframes Toastify__bounceOutLeft{20%{opacity:1;transform:translate3d(20px,var(--y),0)}to{opacity:0;transform:translate3d(-2000px,var(--y),0)}}@keyframes Toastify__bounceInUp{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(0,3000px,0)}60%{opacity:1;transform:translate3d(0,-20px,0)}75%{transform:translate3d(0,10px,0)}90%{transform:translate3d(0,-5px,0)}to{transform:translateZ(0)}}@keyframes Toastify__bounceOutUp{20%{transform:translate3d(0,calc(var(--y) - 10px),0)}40%,45%{opacity:1;transform:translate3d(0,calc(var(--y) + 20px),0)}to{opacity:0;transform:translate3d(0,-2000px,0)}}@keyframes Toastify__bounceInDown{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(0,-3000px,0)}60%{opacity:1;transform:translate3d(0,25px,0)}75%{transform:translate3d(0,-10px,0)}90%{transform:translate3d(0,5px,0)}to{transform:none}}@keyframes Toastify__bounceOutDown{20%{transform:translate3d(0,calc(var(--y) - 10px),0)}40%,45%{opacity:1;transform:translate3d(0,calc(var(--y) + 20px),0)}to{opacity:0;transform:translate3d(0,2000px,0)}}.Toastify__bounce-enter--top-left,.Toastify__bounce-enter--bottom-left{animation-name:Toastify__bounceInLeft}.Toastify__bounce-enter--top-right,.Toastify__bounce-enter--bottom-right{animation-name:Toastify__bounceInRight}.Toastify__bounce-enter--top-center{animation-name:Toastify__bounceInDown}.Toastify__bounce-enter--bottom-center{animation-name:Toastify__bounceInUp}.Toastify__bounce-exit--top-left,.Toastify__bounce-exit--bottom-left{animation-name:Toastify__bounceOutLeft}.Toastify__bounce-exit--top-right,.Toastify__bounce-exit--bottom-right{animation-name:Toastify__bounceOutRight}.Toastify__bounce-exit--top-center{animation-name:Toastify__bounceOutUp}.Toastify__bounce-exit--bottom-center{animation-name:Toastify__bounceOutDown}@keyframes Toastify__zoomIn{0%{opacity:0;transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes Toastify__zoomOut{0%{opacity:1}50%{opacity:0;transform:translate3d(0,var(--y),0) scale3d(.3,.3,.3)}to{opacity:0}}.Toastify__zoom-enter{animation-name:Toastify__zoomIn}.Toastify__zoom-exit{animation-name:Toastify__zoomOut}@keyframes Toastify__flipIn{0%{transform:perspective(400px) rotateX(90deg);animation-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotateX(-20deg);animation-timing-function:ease-in}60%{transform:perspective(400px) rotateX(10deg);opacity:1}80%{transform:perspective(400px) rotateX(-5deg)}to{transform:perspective(400px)}}@keyframes Toastify__flipOut{0%{transform:translate3d(0,var(--y),0) perspective(400px)}30%{transform:translate3d(0,var(--y),0) perspective(400px) rotateX(-20deg);opacity:1}to{transform:translate3d(0,var(--y),0) perspective(400px) rotateX(90deg);opacity:0}}.Toastify__flip-enter{animation-name:Toastify__flipIn}.Toastify__flip-exit{animation-name:Toastify__flipOut}@keyframes Toastify__slideInRight{0%{transform:translate3d(110%,0,0);visibility:visible}to{transform:translate3d(0,var(--y),0)}}@keyframes Toastify__slideInLeft{0%{transform:translate3d(-110%,0,0);visibility:visible}to{transform:translate3d(0,var(--y),0)}}@keyframes Toastify__slideInUp{0%{transform:translate3d(0,110%,0);visibility:visible}to{transform:translate3d(0,var(--y),0)}}@keyframes Toastify__slideInDown{0%{transform:translate3d(0,-110%,0);visibility:visible}to{transform:translate3d(0,var(--y),0)}}@keyframes Toastify__slideOutRight{0%{transform:translate3d(0,var(--y),0)}to{visibility:hidden;transform:translate3d(110%,var(--y),0)}}@keyframes Toastify__slideOutLeft{0%{transform:translate3d(0,var(--y),0)}to{visibility:hidden;transform:translate3d(-110%,var(--y),0)}}@keyframes Toastify__slideOutDown{0%{transform:translate3d(0,var(--y),0)}to{visibility:hidden;transform:translate3d(0,500px,0)}}@keyframes Toastify__slideOutUp{0%{transform:translate3d(0,var(--y),0)}to{visibility:hidden;transform:translate3d(0,-500px,0)}}.Toastify__slide-enter--top-left,.Toastify__slide-enter--bottom-left{animation-name:Toastify__slideInLeft}.Toastify__slide-enter--top-right,.Toastify__slide-enter--bottom-right{animation-name:Toastify__slideInRight}.Toastify__slide-enter--top-center{animation-name:Toastify__slideInDown}.Toastify__slide-enter--bottom-center{animation-name:Toastify__slideInUp}.Toastify__slide-exit--top-left,.Toastify__slide-exit--bottom-left{animation-name:Toastify__slideOutLeft;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-right,.Toastify__slide-exit--bottom-right{animation-name:Toastify__slideOutRight;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-center{animation-name:Toastify__slideOutUp;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--bottom-center{animation-name:Toastify__slideOutDown;animation-timing-function:ease-in;animation-duration:.3s}@keyframes Toastify__spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.chat-item{display:flex;padding:10px;cursor:pointer;background-color:hsl(var(--background))}.chat-item:hover,.chat-item.active{background-color:#2f2f2f}.bubble{border-radius:16px;padding:12px;word-wrap:break-word;max-width:100%;overflow:hidden}.bubble-right .bubble{background-color:#0a0a0a;max-width:100%}.bubble-right .bubble>span{text-align:right;display:block}.bubble-left .bubble{background-color:#1b1b1b;max-width:100%}.bubble-right{align-self:flex-end;display:flex;justify-content:flex-end;width:80%}.bubble-left{align-self:flex-start;display:flex;justify-content:flex-start;width:80%}.input-message textarea{background-color:#2f2f2f;padding-left:48px}.input-message textarea:focus{outline:none;border:none;box-shadow:none}.message-container{flex:1;overflow-y:auto;max-height:calc(100vh - 110px);padding-top:50px}.tabs-chat{background-color:transparent;width:100%;border-radius:0}.contacts-container{height:calc(100vh - 180px);overflow-y:auto;display:flex;flex-direction:column}.chat-item{display:flex;padding:10px;cursor:pointer}.custom-scrollbar{scrollbar-width:none}.custom-scrollbar::-webkit-scrollbar{display:none}.input-container{position:sticky;bottom:0;display:flex;flex-direction:column;gap:.375rem;background-color:transparent;padding:.375rem 1rem;width:100%;max-width:48rem;margin:0 auto;box-sizing:border-box}.formatted-message{white-space:pre-wrap}.formatted-message p{margin-bottom:1em}.formatted-message strong{font-weight:700}.formatted-message em{font-style:italic}.formatted-message del{text-decoration:line-through}.formatted-message a{color:#170c96!important;text-decoration:underline!important}.highlight-quoted{animation:highlight 2s ease-out}@keyframes highlight{0%{background-color:#3b82f633}to{background-color:transparent}}
manager/dist/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="https://evolution-api.com/files/evo/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Evolution Manager</title>
8
+ <script type="module" crossorigin src="/assets/index-CO3NSIFj.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DsIrum0U.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
package.json ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "evolution-api",
3
+ "version": "2.3.6",
4
+ "description": "Rest api for communication with WhatsApp",
5
+ "main": "./dist/main.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "build": "tsc --noEmit && tsup",
9
+ "start": "tsx ./src/main.ts",
10
+ "start:prod": "node dist/main",
11
+ "dev:server": "tsx watch ./src/main.ts",
12
+ "test": "tsx watch ./test/all.test.ts",
13
+ "lint": "eslint --fix --ext .ts src",
14
+ "lint:check": "eslint --ext .ts src",
15
+ "commit": "cz",
16
+ "commitlint": "commitlint --edit",
17
+ "db:generate": "node runWithProvider.js \"npx prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
18
+ "db:deploy": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate deploy --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
19
+ "db:deploy:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate deploy --schema prisma\\DATABASE_PROVIDER-schema.prisma\"",
20
+ "db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
21
+ "db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"",
22
+ "db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"",
23
+ "prepare": "husky"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/EvolutionAPI/evolution-api.git"
28
+ },
29
+ "keywords": [
30
+ "chat",
31
+ "communication",
32
+ "message",
33
+ "send message",
34
+ "whatsapp",
35
+ "js-whatsapp",
36
+ "whatsapp-api",
37
+ "whatsapp-web",
38
+ "whatsapp",
39
+ "whatsapp-chat",
40
+ "whatsapp-group",
41
+ "automation",
42
+ "multi-device",
43
+ "bot"
44
+ ],
45
+ "author": {
46
+ "name": "Davidson Gomes",
47
+ "email": "contato@evolution-api.com"
48
+ },
49
+ "license": "Apache-2.0",
50
+ "bugs": {
51
+ "url": "https://github.com/EvolutionAPI/evolution-api/issues"
52
+ },
53
+ "homepage": "https://github.com/EvolutionAPI/evolution-api#readme",
54
+ "lint-staged": {
55
+ "src/**/*.{ts,js}": [
56
+ "eslint --fix"
57
+ ],
58
+ "src/**/*.ts": [
59
+ "sh -c 'tsc --noEmit'"
60
+ ]
61
+ },
62
+ "config": {
63
+ "commitizen": {
64
+ "path": "cz-conventional-changelog"
65
+ }
66
+ },
67
+ "dependencies": {
68
+ "@adiwajshing/keyed-db": "^0.2.4",
69
+ "@aws-sdk/client-sqs": "^3.891.0",
70
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
71
+ "@figuro/chatwoot-sdk": "^1.1.16",
72
+ "@hapi/boom": "^10.0.1",
73
+ "@paralleldrive/cuid2": "^2.2.2",
74
+ "@prisma/client": "^6.16.2",
75
+ "@sentry/node": "^10.12.0",
76
+ "@types/uuid": "^10.0.0",
77
+ "amqplib": "^0.10.5",
78
+ "audio-decode": "^2.2.3",
79
+ "axios": "^1.7.9",
80
+ "baileys": "7.0.0-rc.6",
81
+ "class-validator": "^0.14.1",
82
+ "compression": "^1.7.5",
83
+ "cors": "^2.8.5",
84
+ "dayjs": "^1.11.13",
85
+ "dotenv": "^16.4.7",
86
+ "emoji-regex": "^10.4.0",
87
+ "eventemitter2": "^6.4.9",
88
+ "express": "^4.21.2",
89
+ "express-async-errors": "^3.1.1",
90
+ "fluent-ffmpeg": "^2.1.3",
91
+ "form-data": "^4.0.1",
92
+ "https-proxy-agent": "^7.0.6",
93
+ "i18next": "^23.7.19",
94
+ "jimp": "^1.6.0",
95
+ "json-schema": "^0.4.0",
96
+ "jsonschema": "^1.4.1",
97
+ "jsonwebtoken": "^9.0.2",
98
+ "kafkajs": "^2.2.4",
99
+ "link-preview-js": "^3.0.13",
100
+ "long": "^5.2.3",
101
+ "mediainfo.js": "^0.3.4",
102
+ "mime": "^4.0.0",
103
+ "mime-types": "^2.1.35",
104
+ "minio": "^8.0.3",
105
+ "multer": "^2.0.2",
106
+ "nats": "^2.29.1",
107
+ "node-cache": "^5.1.2",
108
+ "node-cron": "^3.0.3",
109
+ "openai": "^4.77.3",
110
+ "pg": "^8.13.1",
111
+ "pino": "^9.10.0",
112
+ "prisma": "^6.1.0",
113
+ "pusher": "^5.2.0",
114
+ "qrcode": "^1.5.4",
115
+ "qrcode-terminal": "^0.12.0",
116
+ "redis": "^4.7.0",
117
+ "rxjs": "^7.8.2",
118
+ "sharp": "^0.34.2",
119
+ "socket.io": "^4.8.1",
120
+ "socket.io-client": "^4.8.1",
121
+ "socks-proxy-agent": "^8.0.5",
122
+ "swagger-ui-express": "^5.0.1",
123
+ "tsup": "^8.3.5",
124
+ "undici": "^7.16.0",
125
+ "uuid": "^13.0.0"
126
+ },
127
+ "devDependencies": {
128
+ "@commitlint/cli": "^19.8.1",
129
+ "@commitlint/config-conventional": "^19.8.1",
130
+ "@types/compression": "^1.7.5",
131
+ "@types/cors": "^2.8.17",
132
+ "@types/express": "^4.17.18",
133
+ "@types/json-schema": "^7.0.15",
134
+ "@types/mime": "^4.0.0",
135
+ "@types/mime-types": "^2.1.4",
136
+ "@types/node": "^24.5.2",
137
+ "@types/node-cron": "^3.0.11",
138
+ "@types/qrcode": "^1.5.5",
139
+ "@types/qrcode-terminal": "^0.12.2",
140
+ "@typescript-eslint/eslint-plugin": "^8.44.0",
141
+ "@typescript-eslint/parser": "^8.44.0",
142
+ "commitizen": "^4.3.1",
143
+ "cz-conventional-changelog": "^3.3.0",
144
+ "eslint": "^8.45.0",
145
+ "eslint-config-prettier": "^10.1.8",
146
+ "eslint-plugin-import": "^2.31.0",
147
+ "eslint-plugin-prettier": "^5.2.1",
148
+ "eslint-plugin-simple-import-sort": "^12.1.1",
149
+ "husky": "^9.1.7",
150
+ "lint-staged": "^16.1.6",
151
+ "prettier": "^3.4.2",
152
+ "tsconfig-paths": "^4.2.0",
153
+ "tsx": "^4.20.5",
154
+ "typescript": "^5.7.2"
155
+ }
156
+ }
src/@types/express.d.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { Multer } from 'multer';
2
+
3
+ declare global {
4
+ namespace Express {
5
+ interface Request {
6
+ file?: Multer.File;
7
+ }
8
+ }
9
+ }
src/api/abstract/abstract.cache.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ICache {
2
+ get(key: string): Promise<any>;
3
+
4
+ hGet(key: string, field: string): Promise<any>;
5
+
6
+ set(key: string, value: any, ttl?: number): void;
7
+
8
+ hSet(key: string, field: string, value: any): Promise<void>;
9
+
10
+ has(key: string): Promise<boolean>;
11
+
12
+ keys(appendCriteria?: string): Promise<string[]>;
13
+
14
+ delete(key: string | string[]): Promise<number>;
15
+
16
+ hDelete(key: string, field: string): Promise<any>;
17
+
18
+ deleteAll(appendCriteria?: string): Promise<number>;
19
+ }
src/api/abstract/abstract.repository.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ConfigService, Database } from '@config/env.config';
2
+ import { ROOT_DIR } from '@config/path.config';
3
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ export type IInsert = { insertCount: number };
7
+
8
+ export interface IRepository {
9
+ insert(data: any, instanceName: string, saveDb?: boolean): Promise<IInsert>;
10
+ update(data: any, instanceName: string, saveDb?: boolean): Promise<IInsert>;
11
+ find(query: any): Promise<any>;
12
+ delete(query: any, force?: boolean): Promise<any>;
13
+
14
+ dbSettings: Database;
15
+ readonly storePath: string;
16
+ }
17
+
18
+ type WriteStore<U> = {
19
+ path: string;
20
+ fileName: string;
21
+ data: U;
22
+ };
23
+
24
+ export abstract class Repository implements IRepository {
25
+ constructor(configService: ConfigService) {
26
+ this.dbSettings = configService.get<Database>('DATABASE');
27
+ }
28
+
29
+ dbSettings: Database;
30
+ readonly storePath = join(ROOT_DIR, 'store');
31
+
32
+ public writeStore = <T = any>(create: WriteStore<T>) => {
33
+ if (!existsSync(create.path)) {
34
+ mkdirSync(create.path, { recursive: true });
35
+ }
36
+ try {
37
+ writeFileSync(join(create.path, create.fileName + '.json'), JSON.stringify({ ...create.data }), {
38
+ encoding: 'utf-8',
39
+ });
40
+
41
+ return { message: 'create - success' };
42
+ } finally {
43
+ create.data = undefined;
44
+ }
45
+ };
46
+
47
+ // eslint-disable-next-line
48
+ public insert(data: any, instanceName: string, saveDb = false): Promise<IInsert> {
49
+ throw new Error('Method not implemented.');
50
+ }
51
+
52
+ // eslint-disable-next-line
53
+ public update(data: any, instanceName: string, saveDb = false): Promise<IInsert> {
54
+ throw new Error('Method not implemented.');
55
+ }
56
+
57
+ // eslint-disable-next-line
58
+ public find(query: any): Promise<any> {
59
+ throw new Error('Method not implemented.');
60
+ }
61
+
62
+ // eslint-disable-next-line
63
+ delete(query: any, force?: boolean): Promise<any> {
64
+ throw new Error('Method not implemented.');
65
+ }
66
+ }
src/api/abstract/abstract.router.ts ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'express-async-errors';
2
+
3
+ import { GetParticipant, GroupInvite } from '@api/dto/group.dto';
4
+ import { InstanceDto } from '@api/dto/instance.dto';
5
+ import { Logger } from '@config/logger.config';
6
+ import { BadRequestException } from '@exceptions';
7
+ import { Request } from 'express';
8
+ import { JSONSchema7 } from 'json-schema';
9
+ import { validate } from 'jsonschema';
10
+
11
+ type DataValidate<T> = {
12
+ request: Request;
13
+ schema: JSONSchema7;
14
+ ClassRef: any;
15
+ execute: (instance: InstanceDto, data: T) => Promise<any>;
16
+ };
17
+
18
+ const logger = new Logger('Validate');
19
+
20
+ export abstract class RouterBroker {
21
+ constructor() {}
22
+ public routerPath(path: string, param = true) {
23
+ let route = '/' + path;
24
+ param ? (route += '/:instanceName') : null;
25
+
26
+ return route;
27
+ }
28
+
29
+ public async dataValidate<T>(args: DataValidate<T>) {
30
+ const { request, schema, ClassRef, execute } = args;
31
+
32
+ const ref = new ClassRef();
33
+ const body = request.body;
34
+ const instance = request.params as unknown as InstanceDto;
35
+
36
+ if (request?.query && Object.keys(request.query).length > 0) {
37
+ Object.assign(instance, request.query);
38
+ }
39
+
40
+ if (request.originalUrl.includes('/instance/create')) {
41
+ Object.assign(instance, body);
42
+ }
43
+
44
+ Object.assign(ref, body);
45
+
46
+ const v = schema ? validate(ref, schema) : { valid: true, errors: [] };
47
+
48
+ if (!v.valid) {
49
+ const message: any[] = v.errors.map(({ stack, schema }) => {
50
+ let message: string;
51
+ if (schema['description']) {
52
+ message = schema['description'];
53
+ } else {
54
+ message = stack.replace('instance.', '');
55
+ }
56
+ return message;
57
+ });
58
+ logger.error(message);
59
+ throw new BadRequestException(message);
60
+ }
61
+
62
+ return await execute(instance, ref);
63
+ }
64
+
65
+ public async groupNoValidate<T>(args: DataValidate<T>) {
66
+ const { request, ClassRef, schema, execute } = args;
67
+
68
+ const instance = request.params as unknown as InstanceDto;
69
+
70
+ const ref = new ClassRef();
71
+
72
+ Object.assign(ref, request.body);
73
+
74
+ const v = validate(ref, schema);
75
+
76
+ if (!v.valid) {
77
+ const message: any[] = v.errors.map(({ property, stack, schema }) => {
78
+ let message: string;
79
+ if (schema['description']) {
80
+ message = schema['description'];
81
+ } else {
82
+ message = stack.replace('instance.', '');
83
+ }
84
+ return {
85
+ property: property.replace('instance.', ''),
86
+ message,
87
+ };
88
+ });
89
+ logger.error([...message]);
90
+ throw new BadRequestException(...message);
91
+ }
92
+
93
+ return await execute(instance, ref);
94
+ }
95
+
96
+ public async groupValidate<T>(args: DataValidate<T>) {
97
+ const { request, ClassRef, schema, execute } = args;
98
+
99
+ const instance = request.params as unknown as InstanceDto;
100
+ const body = request.body;
101
+
102
+ let groupJid = body?.groupJid;
103
+
104
+ if (!groupJid) {
105
+ if (request.query?.groupJid) {
106
+ groupJid = request.query.groupJid;
107
+ } else {
108
+ throw new BadRequestException('The group id needs to be informed in the query', 'ex: "groupJid=120362@g.us"');
109
+ }
110
+ }
111
+
112
+ if (!groupJid.endsWith('@g.us')) {
113
+ groupJid = groupJid + '@g.us';
114
+ }
115
+
116
+ Object.assign(body, {
117
+ groupJid: groupJid,
118
+ });
119
+
120
+ const ref = new ClassRef();
121
+
122
+ Object.assign(ref, body);
123
+
124
+ const v = validate(ref, schema);
125
+
126
+ if (!v.valid) {
127
+ const message: any[] = v.errors.map(({ property, stack, schema }) => {
128
+ let message: string;
129
+ if (schema['description']) {
130
+ message = schema['description'];
131
+ } else {
132
+ message = stack.replace('instance.', '');
133
+ }
134
+ return {
135
+ property: property.replace('instance.', ''),
136
+ message,
137
+ };
138
+ });
139
+ logger.error([...message]);
140
+ throw new BadRequestException(...message);
141
+ }
142
+
143
+ return await execute(instance, ref);
144
+ }
145
+
146
+ public async inviteCodeValidate<T>(args: DataValidate<T>) {
147
+ const { request, ClassRef, schema, execute } = args;
148
+
149
+ const inviteCode = request.query as unknown as GroupInvite;
150
+
151
+ if (!inviteCode?.inviteCode) {
152
+ throw new BadRequestException(
153
+ 'The group invite code id needs to be informed in the query',
154
+ 'ex: "inviteCode=F1EX5QZxO181L3TMVP31gY" (Obtained from group join link)',
155
+ );
156
+ }
157
+
158
+ const instance = request.params as unknown as InstanceDto;
159
+ const body = request.body;
160
+
161
+ const ref = new ClassRef();
162
+
163
+ Object.assign(body, inviteCode);
164
+ Object.assign(ref, body);
165
+
166
+ const v = validate(ref, schema);
167
+
168
+ if (!v.valid) {
169
+ const message: any[] = v.errors.map(({ property, stack, schema }) => {
170
+ let message: string;
171
+ if (schema['description']) {
172
+ message = schema['description'];
173
+ } else {
174
+ message = stack.replace('instance.', '');
175
+ }
176
+ return {
177
+ property: property.replace('instance.', ''),
178
+ message,
179
+ };
180
+ });
181
+ logger.error([...message]);
182
+ throw new BadRequestException(...message);
183
+ }
184
+
185
+ return await execute(instance, ref);
186
+ }
187
+
188
+ public async getParticipantsValidate<T>(args: DataValidate<T>) {
189
+ const { request, ClassRef, schema, execute } = args;
190
+
191
+ const getParticipants = request.query as unknown as GetParticipant;
192
+
193
+ if (!getParticipants?.getParticipants) {
194
+ throw new BadRequestException('The getParticipants needs to be informed in the query');
195
+ }
196
+
197
+ const instance = request.params as unknown as InstanceDto;
198
+ const body = request.body;
199
+
200
+ const ref = new ClassRef();
201
+
202
+ Object.assign(body, getParticipants);
203
+ Object.assign(ref, body);
204
+
205
+ const v = validate(ref, schema);
206
+
207
+ if (!v.valid) {
208
+ const message: any[] = v.errors.map(({ property, stack, schema }) => {
209
+ let message: string;
210
+ if (schema['description']) {
211
+ message = schema['description'];
212
+ } else {
213
+ message = stack.replace('instance.', '');
214
+ }
215
+ return {
216
+ property: property.replace('instance.', ''),
217
+ message,
218
+ };
219
+ });
220
+ logger.error([...message]);
221
+ throw new BadRequestException(...message);
222
+ }
223
+
224
+ return await execute(instance, ref);
225
+ }
226
+ }
src/api/controllers/business.controller.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto';
2
+ import { InstanceDto } from '@api/dto/instance.dto';
3
+ import { WAMonitoringService } from '@api/services/monitor.service';
4
+
5
+ export class BusinessController {
6
+ constructor(private readonly waMonitor: WAMonitoringService) {}
7
+
8
+ public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) {
9
+ return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data);
10
+ }
11
+
12
+ public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) {
13
+ return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data);
14
+ }
15
+ }
src/api/controllers/call.controller.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { OfferCallDto } from '@api/dto/call.dto';
2
+ import { InstanceDto } from '@api/dto/instance.dto';
3
+ import { WAMonitoringService } from '@api/services/monitor.service';
4
+
5
+ export class CallController {
6
+ constructor(private readonly waMonitor: WAMonitoringService) {}
7
+
8
+ public async offerCall({ instanceName }: InstanceDto, data: OfferCallDto) {
9
+ return await this.waMonitor.waInstances[instanceName].offerCall(data);
10
+ }
11
+ }
src/api/controllers/chat.controller.ts ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ ArchiveChatDto,
3
+ BlockUserDto,
4
+ DeleteMessage,
5
+ getBase64FromMediaMessageDto,
6
+ MarkChatUnreadDto,
7
+ NumberDto,
8
+ PrivacySettingDto,
9
+ ProfileNameDto,
10
+ ProfilePictureDto,
11
+ ProfileStatusDto,
12
+ ReadMessageDto,
13
+ SendPresenceDto,
14
+ UpdateMessageDto,
15
+ WhatsAppNumberDto,
16
+ } from '@api/dto/chat.dto';
17
+ import { InstanceDto } from '@api/dto/instance.dto';
18
+ import { Query } from '@api/repository/repository.service';
19
+ import { WAMonitoringService } from '@api/services/monitor.service';
20
+ import { Contact, Message, MessageUpdate } from '@prisma/client';
21
+
22
+ export class ChatController {
23
+ constructor(private readonly waMonitor: WAMonitoringService) {}
24
+
25
+ public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) {
26
+ return await this.waMonitor.waInstances[instanceName].whatsappNumber(data);
27
+ }
28
+
29
+ public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) {
30
+ return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data);
31
+ }
32
+
33
+ public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) {
34
+ return await this.waMonitor.waInstances[instanceName].archiveChat(data);
35
+ }
36
+
37
+ public async markChatUnread({ instanceName }: InstanceDto, data: MarkChatUnreadDto) {
38
+ return await this.waMonitor.waInstances[instanceName].markChatUnread(data);
39
+ }
40
+
41
+ public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) {
42
+ return await this.waMonitor.waInstances[instanceName].deleteMessage(data);
43
+ }
44
+
45
+ public async fetchProfilePicture({ instanceName }: InstanceDto, data: NumberDto) {
46
+ return await this.waMonitor.waInstances[instanceName].profilePicture(data.number);
47
+ }
48
+
49
+ public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) {
50
+ return await this.waMonitor.waInstances[instanceName].fetchProfile(instanceName, data.number);
51
+ }
52
+
53
+ public async fetchContacts({ instanceName }: InstanceDto, query: Query<Contact>) {
54
+ return await this.waMonitor.waInstances[instanceName].fetchContacts(query);
55
+ }
56
+
57
+ public async getBase64FromMediaMessage({ instanceName }: InstanceDto, data: getBase64FromMediaMessageDto) {
58
+ return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(data);
59
+ }
60
+
61
+ public async fetchMessages({ instanceName }: InstanceDto, query: Query<Message>) {
62
+ return await this.waMonitor.waInstances[instanceName].fetchMessages(query);
63
+ }
64
+
65
+ public async fetchStatusMessage({ instanceName }: InstanceDto, query: Query<MessageUpdate>) {
66
+ return await this.waMonitor.waInstances[instanceName].fetchStatusMessage(query);
67
+ }
68
+
69
+ public async fetchChats({ instanceName }: InstanceDto, query: Query<Contact>) {
70
+ return await this.waMonitor.waInstances[instanceName].fetchChats(query);
71
+ }
72
+
73
+ public async findChatByRemoteJid({ instanceName }: InstanceDto, remoteJid: string) {
74
+ return await this.waMonitor.waInstances[instanceName].findChatByRemoteJid(remoteJid);
75
+ }
76
+
77
+ public async sendPresence({ instanceName }: InstanceDto, data: SendPresenceDto) {
78
+ return await this.waMonitor.waInstances[instanceName].sendPresence(data);
79
+ }
80
+
81
+ public async fetchPrivacySettings({ instanceName }: InstanceDto) {
82
+ return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings();
83
+ }
84
+
85
+ public async updatePrivacySettings({ instanceName }: InstanceDto, data: PrivacySettingDto) {
86
+ return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data);
87
+ }
88
+
89
+ public async fetchBusinessProfile({ instanceName }: InstanceDto, data: ProfilePictureDto) {
90
+ return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile(data.number);
91
+ }
92
+
93
+ public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) {
94
+ return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name);
95
+ }
96
+
97
+ public async updateProfileStatus({ instanceName }: InstanceDto, data: ProfileStatusDto) {
98
+ return await this.waMonitor.waInstances[instanceName].updateProfileStatus(data.status);
99
+ }
100
+
101
+ public async updateProfilePicture({ instanceName }: InstanceDto, data: ProfilePictureDto) {
102
+ return await this.waMonitor.waInstances[instanceName].updateProfilePicture(data.picture);
103
+ }
104
+
105
+ public async removeProfilePicture({ instanceName }: InstanceDto) {
106
+ return await this.waMonitor.waInstances[instanceName].removeProfilePicture();
107
+ }
108
+
109
+ public async updateMessage({ instanceName }: InstanceDto, data: UpdateMessageDto) {
110
+ return await this.waMonitor.waInstances[instanceName].updateMessage(data);
111
+ }
112
+
113
+ public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) {
114
+ return await this.waMonitor.waInstances[instanceName].blockUser(data);
115
+ }
116
+ }
src/api/controllers/group.controller.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ AcceptGroupInvite,
3
+ CreateGroupDto,
4
+ GetParticipant,
5
+ GroupDescriptionDto,
6
+ GroupInvite,
7
+ GroupJid,
8
+ GroupPictureDto,
9
+ GroupSendInvite,
10
+ GroupSubjectDto,
11
+ GroupToggleEphemeralDto,
12
+ GroupUpdateParticipantDto,
13
+ GroupUpdateSettingDto,
14
+ } from '@api/dto/group.dto';
15
+ import { InstanceDto } from '@api/dto/instance.dto';
16
+ import { WAMonitoringService } from '@api/services/monitor.service';
17
+
18
+ export class GroupController {
19
+ constructor(private readonly waMonitor: WAMonitoringService) {}
20
+
21
+ public async createGroup(instance: InstanceDto, create: CreateGroupDto) {
22
+ return await this.waMonitor.waInstances[instance.instanceName].createGroup(create);
23
+ }
24
+
25
+ public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) {
26
+ return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture(update);
27
+ }
28
+
29
+ public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) {
30
+ return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject(update);
31
+ }
32
+
33
+ public async updateGroupDescription(instance: InstanceDto, update: GroupDescriptionDto) {
34
+ return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription(update);
35
+ }
36
+
37
+ public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) {
38
+ return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid);
39
+ }
40
+
41
+ public async fetchAllGroups(instance: InstanceDto, getPaticipants: GetParticipant) {
42
+ return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups(getPaticipants);
43
+ }
44
+
45
+ public async inviteCode(instance: InstanceDto, groupJid: GroupJid) {
46
+ return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid);
47
+ }
48
+
49
+ public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) {
50
+ return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode);
51
+ }
52
+
53
+ public async sendInvite(instance: InstanceDto, data: GroupSendInvite) {
54
+ return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data);
55
+ }
56
+
57
+ public async acceptInviteCode(instance: InstanceDto, inviteCode: AcceptGroupInvite) {
58
+ return await this.waMonitor.waInstances[instance.instanceName].acceptInviteCode(inviteCode);
59
+ }
60
+
61
+ public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) {
62
+ return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid);
63
+ }
64
+
65
+ public async findParticipants(instance: InstanceDto, groupJid: GroupJid) {
66
+ return await this.waMonitor.waInstances[instance.instanceName].findParticipants(groupJid);
67
+ }
68
+
69
+ public async updateGParticipate(instance: InstanceDto, update: GroupUpdateParticipantDto) {
70
+ return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant(update);
71
+ }
72
+
73
+ public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) {
74
+ return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update);
75
+ }
76
+
77
+ public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) {
78
+ return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(update);
79
+ }
80
+
81
+ public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) {
82
+ return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid);
83
+ }
84
+ }
src/api/controllers/instance.controller.ts ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto';
2
+ import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
3
+ import { ProviderFiles } from '@api/provider/sessions';
4
+ import { PrismaRepository } from '@api/repository/repository.service';
5
+ import { channelController, eventManager } from '@api/server.module';
6
+ import { CacheService } from '@api/services/cache.service';
7
+ import { WAMonitoringService } from '@api/services/monitor.service';
8
+ import { SettingsService } from '@api/services/settings.service';
9
+ import { Events, Integration, wa } from '@api/types/wa.types';
10
+ import { Auth, Chatwoot, ConfigService, HttpServer, WaBusiness } from '@config/env.config';
11
+ import { Logger } from '@config/logger.config';
12
+ import { BadRequestException, InternalServerErrorException, UnauthorizedException } from '@exceptions';
13
+ import { delay } from 'baileys';
14
+ import { isArray, isURL } from 'class-validator';
15
+ import EventEmitter2 from 'eventemitter2';
16
+ import { v4 } from 'uuid';
17
+
18
+ import { ProxyController } from './proxy.controller';
19
+
20
+ export class InstanceController {
21
+ constructor(
22
+ private readonly waMonitor: WAMonitoringService,
23
+ private readonly configService: ConfigService,
24
+ private readonly prismaRepository: PrismaRepository,
25
+ private readonly eventEmitter: EventEmitter2,
26
+ private readonly chatwootService: ChatwootService,
27
+ private readonly settingsService: SettingsService,
28
+ private readonly proxyService: ProxyController,
29
+ private readonly cache: CacheService,
30
+ private readonly chatwootCache: CacheService,
31
+ private readonly baileysCache: CacheService,
32
+ private readonly providerFiles: ProviderFiles,
33
+ ) {}
34
+
35
+ private readonly logger = new Logger('InstanceController');
36
+
37
+ public async createInstance(instanceData: InstanceDto) {
38
+ try {
39
+ const instance = channelController.init(instanceData, {
40
+ configService: this.configService,
41
+ eventEmitter: this.eventEmitter,
42
+ prismaRepository: this.prismaRepository,
43
+ cache: this.cache,
44
+ chatwootCache: this.chatwootCache,
45
+ baileysCache: this.baileysCache,
46
+ providerFiles: this.providerFiles,
47
+ });
48
+
49
+ if (!instance) {
50
+ throw new BadRequestException('Invalid integration');
51
+ }
52
+
53
+ const instanceId = v4();
54
+
55
+ instanceData.instanceId = instanceId;
56
+
57
+ let hash: string;
58
+
59
+ if (!instanceData.token) hash = v4().toUpperCase();
60
+ else hash = instanceData.token;
61
+
62
+ await this.waMonitor.saveInstance({
63
+ instanceId,
64
+ integration: instanceData.integration,
65
+ instanceName: instanceData.instanceName,
66
+ ownerJid: instanceData.ownerJid,
67
+ profileName: instanceData.profileName,
68
+ profilePicUrl: instanceData.profilePicUrl,
69
+ hash,
70
+ number: instanceData.number,
71
+ businessId: instanceData.businessId,
72
+ status: instanceData.status,
73
+ });
74
+
75
+ instance.setInstance({
76
+ instanceName: instanceData.instanceName,
77
+ instanceId,
78
+ integration: instanceData.integration,
79
+ token: hash,
80
+ number: instanceData.number,
81
+ businessId: instanceData.businessId,
82
+ });
83
+
84
+ this.waMonitor.waInstances[instance.instanceName] = instance;
85
+ this.waMonitor.delInstanceTime(instance.instanceName);
86
+
87
+ // set events
88
+ await eventManager.setInstance(instance.instanceName, instanceData);
89
+
90
+ instance.sendDataWebhook(Events.INSTANCE_CREATE, {
91
+ instanceName: instanceData.instanceName,
92
+ instanceId: instanceId,
93
+ });
94
+
95
+ if (instanceData.proxyHost && instanceData.proxyPort && instanceData.proxyProtocol) {
96
+ const testProxy = await this.proxyService.testProxy({
97
+ host: instanceData.proxyHost,
98
+ port: instanceData.proxyPort,
99
+ protocol: instanceData.proxyProtocol,
100
+ username: instanceData.proxyUsername,
101
+ password: instanceData.proxyPassword,
102
+ });
103
+ if (!testProxy) {
104
+ throw new BadRequestException('Invalid proxy');
105
+ }
106
+
107
+ await this.proxyService.createProxy(instance, {
108
+ enabled: true,
109
+ host: instanceData.proxyHost,
110
+ port: instanceData.proxyPort,
111
+ protocol: instanceData.proxyProtocol,
112
+ username: instanceData.proxyUsername,
113
+ password: instanceData.proxyPassword,
114
+ });
115
+ }
116
+
117
+ const settings: wa.LocalSettings = {
118
+ rejectCall: instanceData.rejectCall === true,
119
+ msgCall: instanceData.msgCall || '',
120
+ groupsIgnore: instanceData.groupsIgnore === true,
121
+ alwaysOnline: instanceData.alwaysOnline === true,
122
+ readMessages: instanceData.readMessages === true,
123
+ readStatus: instanceData.readStatus === true,
124
+ syncFullHistory: instanceData.syncFullHistory === true,
125
+ wavoipToken: instanceData.wavoipToken || '',
126
+ };
127
+
128
+ await this.settingsService.create(instance, settings);
129
+
130
+ let webhookWaBusiness = null,
131
+ accessTokenWaBusiness = '';
132
+
133
+ if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
134
+ if (!instanceData.number) {
135
+ throw new BadRequestException('number is required');
136
+ }
137
+ const urlServer = this.configService.get<HttpServer>('SERVER').URL;
138
+ webhookWaBusiness = `${urlServer}/webhook/meta`;
139
+ accessTokenWaBusiness = this.configService.get<WaBusiness>('WA_BUSINESS').TOKEN_WEBHOOK;
140
+ }
141
+
142
+ if (!instanceData.chatwootAccountId || !instanceData.chatwootToken || !instanceData.chatwootUrl) {
143
+ let getQrcode: wa.QrCode;
144
+
145
+ if (instanceData.qrcode && instanceData.integration === Integration.WHATSAPP_BAILEYS) {
146
+ await instance.connectToWhatsapp(instanceData.number);
147
+ await delay(5000);
148
+ getQrcode = instance.qrCode;
149
+ }
150
+
151
+ const result = {
152
+ instance: {
153
+ instanceName: instance.instanceName,
154
+ instanceId: instanceId,
155
+ integration: instanceData.integration,
156
+ webhookWaBusiness,
157
+ accessTokenWaBusiness,
158
+ status: instance.connectionStatus.state,
159
+ },
160
+ hash,
161
+ webhook: {
162
+ webhookUrl: instanceData?.webhook?.url,
163
+ webhookHeaders: instanceData?.webhook?.headers,
164
+ webhookByEvents: instanceData?.webhook?.byEvents,
165
+ webhookBase64: instanceData?.webhook?.base64,
166
+ },
167
+ websocket: {
168
+ enabled: instanceData?.websocket?.enabled,
169
+ },
170
+ rabbitmq: {
171
+ enabled: instanceData?.rabbitmq?.enabled,
172
+ },
173
+ nats: {
174
+ enabled: instanceData?.nats?.enabled,
175
+ },
176
+ sqs: {
177
+ enabled: instanceData?.sqs?.enabled,
178
+ },
179
+ settings,
180
+ qrcode: getQrcode,
181
+ };
182
+
183
+ return result;
184
+ }
185
+
186
+ if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED)
187
+ throw new BadRequestException('Chatwoot is not enabled');
188
+
189
+ if (!instanceData.chatwootAccountId) {
190
+ throw new BadRequestException('accountId is required');
191
+ }
192
+
193
+ if (!instanceData.chatwootToken) {
194
+ throw new BadRequestException('token is required');
195
+ }
196
+
197
+ if (!instanceData.chatwootUrl) {
198
+ throw new BadRequestException('url is required');
199
+ }
200
+
201
+ if (!isURL(instanceData.chatwootUrl, { require_tld: false })) {
202
+ throw new BadRequestException('Invalid "url" property in chatwoot');
203
+ }
204
+
205
+ if (instanceData.chatwootSignMsg !== true && instanceData.chatwootSignMsg !== false) {
206
+ throw new BadRequestException('signMsg is required');
207
+ }
208
+
209
+ if (instanceData.chatwootReopenConversation !== true && instanceData.chatwootReopenConversation !== false) {
210
+ throw new BadRequestException('reopenConversation is required');
211
+ }
212
+
213
+ if (instanceData.chatwootConversationPending !== true && instanceData.chatwootConversationPending !== false) {
214
+ throw new BadRequestException('conversationPending is required');
215
+ }
216
+
217
+ const urlServer = this.configService.get<HttpServer>('SERVER').URL;
218
+
219
+ try {
220
+ this.chatwootService.create(instance, {
221
+ enabled: true,
222
+ accountId: instanceData.chatwootAccountId,
223
+ token: instanceData.chatwootToken,
224
+ url: instanceData.chatwootUrl,
225
+ signMsg: instanceData.chatwootSignMsg || false,
226
+ nameInbox: instanceData.chatwootNameInbox ?? instance.instanceName.split('-cwId-')[0],
227
+ number: instanceData.number,
228
+ reopenConversation: instanceData.chatwootReopenConversation || false,
229
+ conversationPending: instanceData.chatwootConversationPending || false,
230
+ importContacts: instanceData.chatwootImportContacts ?? true,
231
+ mergeBrazilContacts: instanceData.chatwootMergeBrazilContacts ?? false,
232
+ importMessages: instanceData.chatwootImportMessages ?? true,
233
+ daysLimitImportMessages: instanceData.chatwootDaysLimitImportMessages ?? 60,
234
+ organization: instanceData.chatwootOrganization,
235
+ logo: instanceData.chatwootLogo,
236
+ autoCreate: instanceData.chatwootAutoCreate !== false,
237
+ });
238
+ } catch (error) {
239
+ this.logger.log(error);
240
+ }
241
+
242
+ return {
243
+ instance: {
244
+ instanceName: instance.instanceName,
245
+ instanceId: instanceId,
246
+ integration: instanceData.integration,
247
+ webhookWaBusiness,
248
+ accessTokenWaBusiness,
249
+ status: instance.connectionStatus.state,
250
+ },
251
+ hash,
252
+ webhook: {
253
+ webhookUrl: instanceData?.webhook?.url,
254
+ webhookHeaders: instanceData?.webhook?.headers,
255
+ webhookByEvents: instanceData?.webhook?.byEvents,
256
+ webhookBase64: instanceData?.webhook?.base64,
257
+ },
258
+ websocket: {
259
+ enabled: instanceData?.websocket?.enabled,
260
+ },
261
+ rabbitmq: {
262
+ enabled: instanceData?.rabbitmq?.enabled,
263
+ },
264
+ nats: {
265
+ enabled: instanceData?.nats?.enabled,
266
+ },
267
+ sqs: {
268
+ enabled: instanceData?.sqs?.enabled,
269
+ },
270
+ settings,
271
+ chatwoot: {
272
+ enabled: true,
273
+ accountId: instanceData.chatwootAccountId,
274
+ token: instanceData.chatwootToken,
275
+ url: instanceData.chatwootUrl,
276
+ signMsg: instanceData.chatwootSignMsg || false,
277
+ reopenConversation: instanceData.chatwootReopenConversation || false,
278
+ conversationPending: instanceData.chatwootConversationPending || false,
279
+ mergeBrazilContacts: instanceData.chatwootMergeBrazilContacts ?? false,
280
+ importContacts: instanceData.chatwootImportContacts ?? true,
281
+ importMessages: instanceData.chatwootImportMessages ?? true,
282
+ daysLimitImportMessages: instanceData.chatwootDaysLimitImportMessages || 60,
283
+ number: instanceData.number,
284
+ nameInbox: instanceData.chatwootNameInbox ?? instance.instanceName,
285
+ webhookUrl: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
286
+ },
287
+ };
288
+ } catch (error) {
289
+ this.waMonitor.deleteInstance(instanceData.instanceName);
290
+ this.logger.error(isArray(error.message) ? error.message[0] : error.message);
291
+ throw new BadRequestException(isArray(error.message) ? error.message[0] : error.message);
292
+ }
293
+ }
294
+
295
+ public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) {
296
+ try {
297
+ const instance = this.waMonitor.waInstances[instanceName];
298
+ const state = instance?.connectionStatus?.state;
299
+
300
+ if (!state) {
301
+ throw new BadRequestException('The "' + instanceName + '" instance does not exist');
302
+ }
303
+
304
+ if (state == 'open') {
305
+ return await this.connectionState({ instanceName });
306
+ }
307
+
308
+ if (state == 'connecting') {
309
+ return instance.qrCode;
310
+ }
311
+
312
+ if (state == 'close') {
313
+ await instance.connectToWhatsapp(number);
314
+
315
+ await delay(2000);
316
+ return instance.qrCode;
317
+ }
318
+
319
+ return {
320
+ instance: {
321
+ instanceName: instanceName,
322
+ status: state,
323
+ },
324
+ qrcode: instance?.qrCode,
325
+ };
326
+ } catch (error) {
327
+ this.logger.error(error);
328
+ return { error: true, message: error.toString() };
329
+ }
330
+ }
331
+
332
+ public async restartInstance({ instanceName }: InstanceDto) {
333
+ try {
334
+ const instance = this.waMonitor.waInstances[instanceName];
335
+ const state = instance?.connectionStatus?.state;
336
+
337
+ if (!state) {
338
+ throw new BadRequestException('The "' + instanceName + '" instance does not exist');
339
+ }
340
+
341
+ if (state == 'close') {
342
+ throw new BadRequestException('The "' + instanceName + '" instance is not connected');
343
+ } else if (state == 'open') {
344
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) instance.clearCacheChatwoot();
345
+ this.logger.info('restarting instance' + instanceName);
346
+
347
+ instance.client?.ws?.close();
348
+ instance.client?.end(new Error('restart'));
349
+ return await this.connectToWhatsapp({ instanceName });
350
+ } else if (state == 'connecting') {
351
+ instance.client?.ws?.close();
352
+ instance.client?.end(new Error('restart'));
353
+ return await this.connectToWhatsapp({ instanceName });
354
+ }
355
+ } catch (error) {
356
+ this.logger.error(error);
357
+ return { error: true, message: error.toString() };
358
+ }
359
+ }
360
+
361
+ public async connectionState({ instanceName }: InstanceDto) {
362
+ return {
363
+ instance: {
364
+ instanceName: instanceName,
365
+ state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state,
366
+ },
367
+ };
368
+ }
369
+
370
+ public async fetchInstances({ instanceName, instanceId, number }: InstanceDto, key: string) {
371
+ const env = this.configService.get<Auth>('AUTHENTICATION').API_KEY;
372
+
373
+ if (env.KEY !== key) {
374
+ const instancesByKey = await this.prismaRepository.instance.findMany({
375
+ where: {
376
+ token: key,
377
+ name: instanceName || undefined,
378
+ id: instanceId || undefined,
379
+ },
380
+ });
381
+
382
+ if (instancesByKey.length > 0) {
383
+ const names = instancesByKey.map((instance) => instance.name);
384
+
385
+ return this.waMonitor.instanceInfo(names);
386
+ } else {
387
+ throw new UnauthorizedException();
388
+ }
389
+ }
390
+
391
+ if (instanceId || number) {
392
+ return this.waMonitor.instanceInfoById(instanceId, number);
393
+ }
394
+
395
+ const instanceNames = instanceName ? [instanceName] : null;
396
+
397
+ return this.waMonitor.instanceInfo(instanceNames);
398
+ }
399
+
400
+ public async setPresence({ instanceName }: InstanceDto, data: SetPresenceDto) {
401
+ return await this.waMonitor.waInstances[instanceName].setPresence(data);
402
+ }
403
+
404
+ public async logout({ instanceName }: InstanceDto) {
405
+ const { instance } = await this.connectionState({ instanceName });
406
+
407
+ if (instance.state === 'close') {
408
+ throw new BadRequestException('The "' + instanceName + '" instance is not connected');
409
+ }
410
+
411
+ try {
412
+ this.waMonitor.waInstances[instanceName]?.logoutInstance();
413
+
414
+ return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } };
415
+ } catch (error) {
416
+ throw new InternalServerErrorException(error.toString());
417
+ }
418
+ }
419
+
420
+ public async deleteInstance({ instanceName }: InstanceDto) {
421
+ const { instance } = await this.connectionState({ instanceName });
422
+ try {
423
+ const waInstances = this.waMonitor.waInstances[instanceName];
424
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) waInstances?.clearCacheChatwoot();
425
+
426
+ if (instance.state === 'connecting' || instance.state === 'open') {
427
+ await this.logout({ instanceName });
428
+ }
429
+
430
+ try {
431
+ waInstances?.sendDataWebhook(Events.INSTANCE_DELETE, {
432
+ instanceName,
433
+ instanceId: waInstances.instanceId,
434
+ });
435
+ } catch (error) {
436
+ this.logger.error(error);
437
+ }
438
+
439
+ this.eventEmitter.emit('remove.instance', instanceName, 'inner');
440
+ return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } };
441
+ } catch (error) {
442
+ throw new BadRequestException(error.toString());
443
+ }
444
+ }
445
+ }
src/api/controllers/label.controller.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { HandleLabelDto } from '@api/dto/label.dto';
3
+ import { WAMonitoringService } from '@api/services/monitor.service';
4
+
5
+ export class LabelController {
6
+ constructor(private readonly waMonitor: WAMonitoringService) {}
7
+
8
+ public async fetchLabels({ instanceName }: InstanceDto) {
9
+ return await this.waMonitor.waInstances[instanceName].fetchLabels();
10
+ }
11
+
12
+ public async handleLabel({ instanceName }: InstanceDto, data: HandleLabelDto) {
13
+ return await this.waMonitor.waInstances[instanceName].handleLabel(data);
14
+ }
15
+ }
src/api/controllers/proxy.controller.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { ProxyDto } from '@api/dto/proxy.dto';
3
+ import { WAMonitoringService } from '@api/services/monitor.service';
4
+ import { ProxyService } from '@api/services/proxy.service';
5
+ import { Logger } from '@config/logger.config';
6
+ import { BadRequestException, NotFoundException } from '@exceptions';
7
+ import { makeProxyAgent } from '@utils/makeProxyAgent';
8
+ import axios from 'axios';
9
+
10
+ const logger = new Logger('ProxyController');
11
+
12
+ export class ProxyController {
13
+ constructor(
14
+ private readonly proxyService: ProxyService,
15
+ private readonly waMonitor: WAMonitoringService,
16
+ ) {}
17
+
18
+ public async createProxy(instance: InstanceDto, data: ProxyDto) {
19
+ if (!this.waMonitor.waInstances[instance.instanceName]) {
20
+ throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`);
21
+ }
22
+
23
+ if (!data?.enabled) {
24
+ data.host = '';
25
+ data.port = '';
26
+ data.protocol = '';
27
+ data.username = '';
28
+ data.password = '';
29
+ }
30
+
31
+ if (data.host) {
32
+ const testProxy = await this.testProxy(data);
33
+ if (!testProxy) {
34
+ throw new BadRequestException('Invalid proxy');
35
+ }
36
+ }
37
+
38
+ return this.proxyService.create(instance, data);
39
+ }
40
+
41
+ public async findProxy(instance: InstanceDto) {
42
+ if (!this.waMonitor.waInstances[instance.instanceName]) {
43
+ throw new NotFoundException(`The "${instance.instanceName}" instance does not exist`);
44
+ }
45
+
46
+ return this.proxyService.find(instance);
47
+ }
48
+
49
+ public async testProxy(proxy: ProxyDto) {
50
+ try {
51
+ const serverIp = await axios.get('https://icanhazip.com/');
52
+ const response = await axios.get('https://icanhazip.com/', {
53
+ httpsAgent: makeProxyAgent(proxy),
54
+ });
55
+
56
+ const result = response?.data !== serverIp?.data;
57
+ if (result) {
58
+ logger.info('testProxy: proxy connection successful');
59
+ } else {
60
+ logger.warn("testProxy: proxy connection doesn't change the origin IP");
61
+ }
62
+
63
+ return result;
64
+ } catch (error) {
65
+ if (axios.isAxiosError(error)) {
66
+ logger.error('testProxy error: axios error: ' + error.message);
67
+ } else {
68
+ logger.error('testProxy error: unexpected error: ' + error);
69
+ }
70
+
71
+ return false;
72
+ }
73
+ }
74
+ }
src/api/controllers/sendMessage.controller.ts ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import {
3
+ SendAudioDto,
4
+ SendButtonsDto,
5
+ SendContactDto,
6
+ SendListDto,
7
+ SendLocationDto,
8
+ SendMediaDto,
9
+ SendPollDto,
10
+ SendPtvDto,
11
+ SendReactionDto,
12
+ SendStatusDto,
13
+ SendStickerDto,
14
+ SendTemplateDto,
15
+ SendTextDto,
16
+ } from '@api/dto/sendMessage.dto';
17
+ import { WAMonitoringService } from '@api/services/monitor.service';
18
+ import { BadRequestException } from '@exceptions';
19
+ import { isBase64, isURL } from 'class-validator';
20
+ import emojiRegex from 'emoji-regex';
21
+
22
+ const regex = emojiRegex();
23
+
24
+ function isEmoji(str: string) {
25
+ if (str === '') return true;
26
+
27
+ const match = str.match(regex);
28
+ return match?.length === 1 && match[0] === str;
29
+ }
30
+
31
+ export class SendMessageController {
32
+ constructor(private readonly waMonitor: WAMonitoringService) {}
33
+
34
+ public async sendTemplate({ instanceName }: InstanceDto, data: SendTemplateDto) {
35
+ return await this.waMonitor.waInstances[instanceName].templateMessage(data);
36
+ }
37
+
38
+ public async sendText({ instanceName }: InstanceDto, data: SendTextDto) {
39
+ return await this.waMonitor.waInstances[instanceName].textMessage(data);
40
+ }
41
+
42
+ public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto, file?: any) {
43
+ if (isBase64(data?.media) && !data?.fileName && data?.mediatype === 'document') {
44
+ throw new BadRequestException('For base64 the file name must be informed.');
45
+ }
46
+
47
+ if (file || isURL(data?.media) || isBase64(data?.media)) {
48
+ return await this.waMonitor.waInstances[instanceName].mediaMessage(data, file);
49
+ }
50
+ throw new BadRequestException('Owned media must be a url or base64');
51
+ }
52
+
53
+ public async sendPtv({ instanceName }: InstanceDto, data: SendPtvDto, file?: any) {
54
+ if (file || isURL(data?.video) || isBase64(data?.video)) {
55
+ return await this.waMonitor.waInstances[instanceName].ptvMessage(data, file);
56
+ }
57
+ throw new BadRequestException('Owned media must be a url or base64');
58
+ }
59
+
60
+ public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto, file?: any) {
61
+ if (file || isURL(data.sticker) || isBase64(data.sticker)) {
62
+ return await this.waMonitor.waInstances[instanceName].mediaSticker(data, file);
63
+ }
64
+ throw new BadRequestException('Owned media must be a url or base64');
65
+ }
66
+
67
+ public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto, file?: any) {
68
+ if (file?.buffer || isURL(data.audio) || isBase64(data.audio)) {
69
+ // Si file existe y tiene buffer, o si es una URL o Base64, continúa
70
+ return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data, file);
71
+ } else {
72
+ console.error('El archivo no tiene buffer o el audio no es una URL o Base64 válida');
73
+ throw new BadRequestException('Owned media must be a url, base64, or valid file with buffer');
74
+ }
75
+ }
76
+
77
+ public async sendButtons({ instanceName }: InstanceDto, data: SendButtonsDto) {
78
+ return await this.waMonitor.waInstances[instanceName].buttonMessage(data);
79
+ }
80
+
81
+ public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) {
82
+ return await this.waMonitor.waInstances[instanceName].locationMessage(data);
83
+ }
84
+
85
+ public async sendList({ instanceName }: InstanceDto, data: SendListDto) {
86
+ return await this.waMonitor.waInstances[instanceName].listMessage(data);
87
+ }
88
+
89
+ public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) {
90
+ return await this.waMonitor.waInstances[instanceName].contactMessage(data);
91
+ }
92
+
93
+ public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
94
+ if (!isEmoji(data.reaction)) {
95
+ throw new BadRequestException('Reaction must be a single emoji or empty string');
96
+ }
97
+ return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
98
+ }
99
+
100
+ public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) {
101
+ return await this.waMonitor.waInstances[instanceName].pollMessage(data);
102
+ }
103
+
104
+ public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto, file?: any) {
105
+ return await this.waMonitor.waInstances[instanceName].statusMessage(data, file);
106
+ }
107
+ }
src/api/controllers/settings.controller.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { SettingsDto } from '@api/dto/settings.dto';
3
+ import { SettingsService } from '@api/services/settings.service';
4
+
5
+ export class SettingsController {
6
+ constructor(private readonly settingsService: SettingsService) {}
7
+
8
+ public async createSettings(instance: InstanceDto, data: SettingsDto) {
9
+ return this.settingsService.create(instance, data);
10
+ }
11
+
12
+ public async findSettings(instance: InstanceDto) {
13
+ const settings = this.settingsService.find(instance);
14
+ return settings;
15
+ }
16
+ }
src/api/controllers/template.controller.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { TemplateDto } from '@api/dto/template.dto';
3
+ import { TemplateService } from '@api/services/template.service';
4
+
5
+ export class TemplateController {
6
+ constructor(private readonly templateService: TemplateService) {}
7
+
8
+ public async createTemplate(instance: InstanceDto, data: TemplateDto) {
9
+ return this.templateService.create(instance, data);
10
+ }
11
+
12
+ public async findTemplate(instance: InstanceDto) {
13
+ return this.templateService.find(instance);
14
+ }
15
+ }
src/api/dto/business.dto.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class NumberDto {
2
+ number: string;
3
+ }
4
+
5
+ export class getCatalogDto {
6
+ number?: string;
7
+ limit?: number;
8
+ cursor?: string;
9
+ }
10
+
11
+ export class getCollectionsDto {
12
+ number?: string;
13
+ limit?: number;
14
+ }
src/api/dto/call.dto.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export class Metadata {
2
+ number: string;
3
+ }
4
+
5
+ export class OfferCallDto extends Metadata {
6
+ isVideo?: boolean;
7
+ callDuration?: number;
8
+ }
src/api/dto/chat.dto.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ proto,
3
+ WAPresence,
4
+ WAPrivacyGroupAddValue,
5
+ WAPrivacyOnlineValue,
6
+ WAPrivacyValue,
7
+ WAReadReceiptsValue,
8
+ } from 'baileys';
9
+
10
+ export class OnWhatsAppDto {
11
+ constructor(
12
+ public readonly jid: string,
13
+ public readonly exists: boolean,
14
+ public readonly number: string,
15
+ public readonly name?: string,
16
+ public readonly lid?: string,
17
+ ) {}
18
+ }
19
+
20
+ export class getBase64FromMediaMessageDto {
21
+ message: proto.WebMessageInfo;
22
+ convertToMp4?: boolean;
23
+ }
24
+
25
+ export class WhatsAppNumberDto {
26
+ numbers: string[];
27
+ }
28
+
29
+ export class NumberDto {
30
+ number: string;
31
+ }
32
+
33
+ export class NumberBusiness {
34
+ wid?: string;
35
+ jid?: string;
36
+ exists?: boolean;
37
+ isBusiness: boolean;
38
+ name?: string;
39
+ message?: string;
40
+ description?: string;
41
+ email?: string;
42
+ websites?: string[];
43
+ website?: string[];
44
+ address?: string;
45
+ about?: string;
46
+ vertical?: string;
47
+ profilehandle?: string;
48
+ }
49
+
50
+ export class ProfileNameDto {
51
+ name: string;
52
+ }
53
+
54
+ export class ProfileStatusDto {
55
+ status: string;
56
+ }
57
+
58
+ export class ProfilePictureDto {
59
+ number?: string;
60
+ // url or base64
61
+ picture?: string;
62
+ }
63
+
64
+ class Key {
65
+ id: string;
66
+ fromMe: boolean;
67
+ remoteJid: string;
68
+ }
69
+ export class ReadMessageDto {
70
+ readMessages: Key[];
71
+ }
72
+
73
+ export class LastMessage {
74
+ key: Key;
75
+ messageTimestamp?: number;
76
+ }
77
+
78
+ export class ArchiveChatDto {
79
+ lastMessage?: LastMessage;
80
+ chat?: string;
81
+ archive: boolean;
82
+ }
83
+
84
+ export class MarkChatUnreadDto {
85
+ lastMessage?: LastMessage;
86
+ chat?: string;
87
+ }
88
+
89
+ export class PrivacySettingDto {
90
+ readreceipts: WAReadReceiptsValue;
91
+ profile: WAPrivacyValue;
92
+ status: WAPrivacyValue;
93
+ online: WAPrivacyOnlineValue;
94
+ last: WAPrivacyValue;
95
+ groupadd: WAPrivacyGroupAddValue;
96
+ }
97
+
98
+ export class DeleteMessage {
99
+ id: string;
100
+ fromMe: boolean;
101
+ remoteJid: string;
102
+ participant?: string;
103
+ }
104
+ export class Options {
105
+ delay?: number;
106
+ presence?: WAPresence;
107
+ }
108
+ class OptionsMessage {
109
+ options: Options;
110
+ }
111
+ export class Metadata extends OptionsMessage {
112
+ number: string;
113
+ }
114
+
115
+ export class SendPresenceDto extends Metadata {
116
+ presence: WAPresence;
117
+ delay: number;
118
+ }
119
+
120
+ export class UpdateMessageDto extends Metadata {
121
+ number: string;
122
+ key: proto.IMessageKey;
123
+ text: string;
124
+ }
125
+
126
+ export class BlockUserDto {
127
+ number: string;
128
+ status: 'block' | 'unblock';
129
+ }
src/api/dto/chatbot.dto.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class Session {
2
+ remoteJid?: string;
3
+ sessionId?: string;
4
+ status?: string;
5
+ createdAt?: number;
6
+ updateAt?: number;
7
+ }
8
+
9
+ export class IgnoreJidDto {
10
+ remoteJid?: string;
11
+ action?: string;
12
+ }
src/api/dto/group.dto.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class CreateGroupDto {
2
+ subject: string;
3
+ participants: string[];
4
+ description?: string;
5
+ promoteParticipants?: boolean;
6
+ }
7
+
8
+ export class GroupPictureDto {
9
+ groupJid: string;
10
+ image: string;
11
+ }
12
+
13
+ export class GroupSubjectDto {
14
+ groupJid: string;
15
+ subject: string;
16
+ }
17
+
18
+ export class GroupDescriptionDto {
19
+ groupJid: string;
20
+ description: string;
21
+ }
22
+
23
+ export class GroupJid {
24
+ groupJid: string;
25
+ }
26
+
27
+ export class GetParticipant {
28
+ getParticipants: string;
29
+ }
30
+
31
+ export class GroupInvite {
32
+ inviteCode: string;
33
+ }
34
+
35
+ export class AcceptGroupInvite {
36
+ inviteCode: string;
37
+ }
38
+
39
+ export class GroupSendInvite {
40
+ groupJid: string;
41
+ description: string;
42
+ numbers: string[];
43
+ }
44
+
45
+ export class GroupUpdateParticipantDto extends GroupJid {
46
+ action: 'add' | 'remove' | 'promote' | 'demote';
47
+ participants: string[];
48
+ }
49
+
50
+ export class GroupUpdateSettingDto extends GroupJid {
51
+ action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked';
52
+ }
53
+
54
+ export class GroupToggleEphemeralDto extends GroupJid {
55
+ expiration: 0 | 86400 | 604800 | 7776000;
56
+ }
src/api/dto/instance.dto.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IntegrationDto } from '@api/integrations/integration.dto';
2
+ import { JsonValue } from '@prisma/client/runtime/library';
3
+ import { WAPresence } from 'baileys';
4
+
5
+ export class InstanceDto extends IntegrationDto {
6
+ instanceName: string;
7
+ instanceId?: string;
8
+ qrcode?: boolean;
9
+ businessId?: string;
10
+ number?: string;
11
+ integration?: string;
12
+ token?: string;
13
+ status?: string;
14
+ ownerJid?: string;
15
+ profileName?: string;
16
+ profilePicUrl?: string;
17
+ // settings
18
+ rejectCall?: boolean;
19
+ msgCall?: string;
20
+ groupsIgnore?: boolean;
21
+ alwaysOnline?: boolean;
22
+ readMessages?: boolean;
23
+ readStatus?: boolean;
24
+ syncFullHistory?: boolean;
25
+ wavoipToken?: string;
26
+ // proxy
27
+ proxyHost?: string;
28
+ proxyPort?: string;
29
+ proxyProtocol?: string;
30
+ proxyUsername?: string;
31
+ proxyPassword?: string;
32
+ webhook?: {
33
+ enabled?: boolean;
34
+ events?: string[];
35
+ headers?: JsonValue;
36
+ url?: string;
37
+ byEvents?: boolean;
38
+ base64?: boolean;
39
+ };
40
+ chatwootAccountId?: string;
41
+ chatwootConversationPending?: boolean;
42
+ chatwootAutoCreate?: boolean;
43
+ chatwootDaysLimitImportMessages?: number;
44
+ chatwootImportContacts?: boolean;
45
+ chatwootImportMessages?: boolean;
46
+ chatwootLogo?: string;
47
+ chatwootMergeBrazilContacts?: boolean;
48
+ chatwootNameInbox?: string;
49
+ chatwootOrganization?: string;
50
+ chatwootReopenConversation?: boolean;
51
+ chatwootSignMsg?: boolean;
52
+ chatwootToken?: string;
53
+ chatwootUrl?: string;
54
+ }
55
+
56
+ export class SetPresenceDto {
57
+ presence: WAPresence;
58
+ }
src/api/dto/label.dto.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class LabelDto {
2
+ id?: string;
3
+ name: string;
4
+ color: string;
5
+ predefinedId?: string;
6
+ }
7
+
8
+ export class HandleLabelDto {
9
+ number: string;
10
+ labelId: string;
11
+ action: 'add' | 'remove';
12
+ }
src/api/dto/proxy.dto.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export class ProxyDto {
2
+ enabled?: boolean;
3
+ host: string;
4
+ port: string;
5
+ protocol: string;
6
+ username?: string;
7
+ password?: string;
8
+ }
src/api/dto/sendMessage.dto.ts ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { proto, WAPresence } from 'baileys';
2
+
3
+ export class Quoted {
4
+ key: proto.IMessageKey;
5
+ message: proto.IMessage;
6
+ }
7
+
8
+ export class Options {
9
+ delay?: number;
10
+ presence?: WAPresence;
11
+ quoted?: Quoted;
12
+ linkPreview?: boolean;
13
+ encoding?: boolean;
14
+ mentionsEveryOne?: boolean;
15
+ mentioned?: string[];
16
+ webhookUrl?: string;
17
+ }
18
+
19
+ export class MediaMessage {
20
+ mediatype: MediaType;
21
+ mimetype?: string;
22
+ caption?: string;
23
+ // for document
24
+ fileName?: string;
25
+ // url or base64
26
+ media: string;
27
+ }
28
+
29
+ export class StatusMessage {
30
+ type: string;
31
+ content: string;
32
+ statusJidList?: string[];
33
+ allContacts?: boolean;
34
+ caption?: string;
35
+ backgroundColor?: string;
36
+ font?: number;
37
+ }
38
+
39
+ export class Metadata {
40
+ number: string;
41
+ delay?: number;
42
+ quoted?: Quoted;
43
+ linkPreview?: boolean;
44
+ mentionsEveryOne?: boolean;
45
+ mentioned?: string[];
46
+ encoding?: boolean;
47
+ notConvertSticker?: boolean;
48
+ }
49
+
50
+ export class SendTextDto extends Metadata {
51
+ text: string;
52
+ }
53
+ export class SendPresence extends Metadata {
54
+ text: string;
55
+ }
56
+
57
+ export class SendStatusDto extends Metadata {
58
+ type: string;
59
+ content: string;
60
+ statusJidList?: string[];
61
+ allContacts?: boolean;
62
+ caption?: string;
63
+ backgroundColor?: string;
64
+ font?: number;
65
+ }
66
+
67
+ export class SendPollDto extends Metadata {
68
+ name: string;
69
+ selectableCount: number;
70
+ values: string[];
71
+ messageSecret?: Uint8Array;
72
+ }
73
+
74
+ export type MediaType = 'image' | 'document' | 'video' | 'audio' | 'ptv';
75
+
76
+ export class SendMediaDto extends Metadata {
77
+ mediatype: MediaType;
78
+ mimetype?: string;
79
+ caption?: string;
80
+ // for document
81
+ fileName?: string;
82
+ // url or base64
83
+ media: string;
84
+ }
85
+
86
+ export class SendPtvDto extends Metadata {
87
+ video: string;
88
+ }
89
+
90
+ export class SendStickerDto extends Metadata {
91
+ sticker: string;
92
+ }
93
+
94
+ export class SendAudioDto extends Metadata {
95
+ audio: string;
96
+ }
97
+
98
+ export type TypeButton = 'reply' | 'copy' | 'url' | 'call' | 'pix';
99
+
100
+ export type KeyType = 'phone' | 'email' | 'cpf' | 'cnpj' | 'random';
101
+
102
+ export class Button {
103
+ type: TypeButton;
104
+ displayText?: string;
105
+ id?: string;
106
+ url?: string;
107
+ copyCode?: string;
108
+ phoneNumber?: string;
109
+ currency?: string;
110
+ name?: string;
111
+ keyType?: KeyType;
112
+ key?: string;
113
+ }
114
+
115
+ export class SendButtonsDto extends Metadata {
116
+ thumbnailUrl?: string;
117
+ title: string;
118
+ description?: string;
119
+ footer?: string;
120
+ buttons: Button[];
121
+ }
122
+
123
+ export class SendLocationDto extends Metadata {
124
+ latitude: number;
125
+ longitude: number;
126
+ name?: string;
127
+ address?: string;
128
+ }
129
+
130
+ class Row {
131
+ title: string;
132
+ description: string;
133
+ rowId: string;
134
+ }
135
+ class Section {
136
+ title: string;
137
+ rows: Row[];
138
+ }
139
+ export class SendListDto extends Metadata {
140
+ title: string;
141
+ description?: string;
142
+ footerText?: string;
143
+ buttonText: string;
144
+ sections: Section[];
145
+ }
146
+
147
+ export class ContactMessage {
148
+ fullName: string;
149
+ wuid: string;
150
+ phoneNumber: string;
151
+ organization?: string;
152
+ email?: string;
153
+ url?: string;
154
+ }
155
+
156
+ export class SendTemplateDto extends Metadata {
157
+ name: string;
158
+ language: string;
159
+ components: any;
160
+ webhookUrl?: string;
161
+ }
162
+ export class SendContactDto extends Metadata {
163
+ contact: ContactMessage[];
164
+ }
165
+
166
+ export class SendReactionDto {
167
+ key: proto.IMessageKey;
168
+ reaction: string;
169
+ }
src/api/dto/settings.dto.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export class SettingsDto {
2
+ rejectCall?: boolean;
3
+ msgCall?: string;
4
+ groupsIgnore?: boolean;
5
+ alwaysOnline?: boolean;
6
+ readMessages?: boolean;
7
+ readStatus?: boolean;
8
+ syncFullHistory?: boolean;
9
+ wavoipToken?: string;
10
+ }
src/api/dto/template.dto.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export class TemplateDto {
2
+ name: string;
3
+ category: string;
4
+ allowCategoryChange: boolean;
5
+ language: string;
6
+ components: any;
7
+ webhookUrl?: string;
8
+ }
src/api/guards/auth.guard.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { prismaRepository } from '@api/server.module';
3
+ import { Auth, configService, Database } from '@config/env.config';
4
+ import { Logger } from '@config/logger.config';
5
+ import { ForbiddenException, UnauthorizedException } from '@exceptions';
6
+ import { NextFunction, Request, Response } from 'express';
7
+
8
+ const logger = new Logger('GUARD');
9
+
10
+ async function apikey(req: Request, _: Response, next: NextFunction) {
11
+ const env = configService.get<Auth>('AUTHENTICATION').API_KEY;
12
+ const key = req.get('apikey');
13
+ const db = configService.get<Database>('DATABASE');
14
+
15
+ if (!key) {
16
+ throw new UnauthorizedException();
17
+ }
18
+
19
+ if (env.KEY === key) {
20
+ return next();
21
+ }
22
+
23
+ if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) {
24
+ throw new ForbiddenException('Missing global api key', 'The global api key must be set');
25
+ }
26
+ const param = req.params as unknown as InstanceDto;
27
+
28
+ try {
29
+ if (param?.instanceName) {
30
+ const instance = await prismaRepository.instance.findUnique({
31
+ where: { name: param.instanceName },
32
+ });
33
+ if (instance.token === key) {
34
+ return next();
35
+ }
36
+ } else {
37
+ if (req.originalUrl.includes('/instance/fetchInstances') && db.SAVE_DATA.INSTANCE) {
38
+ const instanceByKey = await prismaRepository.instance.findFirst({
39
+ where: { token: key },
40
+ });
41
+ if (instanceByKey) {
42
+ return next();
43
+ }
44
+ }
45
+ }
46
+ } catch (error) {
47
+ logger.error(error);
48
+ }
49
+
50
+ throw new UnauthorizedException();
51
+ }
52
+
53
+ export const authGuard = { apikey };
src/api/guards/instance.guard.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { cache, prismaRepository, waMonitor } from '@api/server.module';
3
+ import { CacheConf, configService } from '@config/env.config';
4
+ import { BadRequestException, ForbiddenException, InternalServerErrorException, NotFoundException } from '@exceptions';
5
+ import { NextFunction, Request, Response } from 'express';
6
+
7
+ async function getInstance(instanceName: string) {
8
+ try {
9
+ const cacheConf = configService.get<CacheConf>('CACHE');
10
+
11
+ const exists = !!waMonitor.waInstances[instanceName];
12
+
13
+ if (cacheConf.REDIS.ENABLED && cacheConf.REDIS.SAVE_INSTANCES) {
14
+ const keyExists = await cache.has(instanceName);
15
+
16
+ return exists || keyExists;
17
+ }
18
+
19
+ return exists || (await prismaRepository.instance.findMany({ where: { name: instanceName } })).length > 0;
20
+ } catch (error) {
21
+ throw new InternalServerErrorException(error?.toString());
22
+ }
23
+ }
24
+
25
+ export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) {
26
+ if (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) {
27
+ return next();
28
+ }
29
+
30
+ const param = req.params as unknown as InstanceDto;
31
+ if (!param?.instanceName) {
32
+ throw new BadRequestException('"instanceName" not provided.');
33
+ }
34
+
35
+ if (!(await getInstance(param.instanceName))) {
36
+ throw new NotFoundException(`The "${param.instanceName}" instance does not exist`);
37
+ }
38
+
39
+ next();
40
+ }
41
+
42
+ export async function instanceLoggedGuard(req: Request, _: Response, next: NextFunction) {
43
+ if (req.originalUrl.includes('/instance/create')) {
44
+ const instance = req.body as InstanceDto;
45
+ if (await getInstance(instance.instanceName)) {
46
+ throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`);
47
+ }
48
+
49
+ if (waMonitor.waInstances[instance.instanceName]) {
50
+ delete waMonitor.waInstances[instance.instanceName];
51
+ }
52
+ }
53
+
54
+ next();
55
+ }
src/api/guards/telemetry.guard.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { sendTelemetry } from '@utils/sendTelemetry';
2
+ import { NextFunction, Request, Response } from 'express';
3
+
4
+ class Telemetry {
5
+ public collectTelemetry(req: Request, res: Response, next: NextFunction): void {
6
+ sendTelemetry(req.path);
7
+
8
+ next();
9
+ }
10
+ }
11
+
12
+ export default Telemetry;
src/api/integrations/channel/channel.controller.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { ProviderFiles } from '@api/provider/sessions';
3
+ import { PrismaRepository } from '@api/repository/repository.service';
4
+ import { CacheService } from '@api/services/cache.service';
5
+ import { WAMonitoringService } from '@api/services/monitor.service';
6
+ import { Integration } from '@api/types/wa.types';
7
+ import { ConfigService } from '@config/env.config';
8
+ import { BadRequestException } from '@exceptions';
9
+ import EventEmitter2 from 'eventemitter2';
10
+
11
+ import { EvolutionStartupService } from './evolution/evolution.channel.service';
12
+ import { BusinessStartupService } from './meta/whatsapp.business.service';
13
+ import { BaileysStartupService } from './whatsapp/whatsapp.baileys.service';
14
+
15
+ type ChannelDataType = {
16
+ configService: ConfigService;
17
+ eventEmitter: EventEmitter2;
18
+ prismaRepository: PrismaRepository;
19
+ cache: CacheService;
20
+ chatwootCache: CacheService;
21
+ baileysCache: CacheService;
22
+ providerFiles: ProviderFiles;
23
+ };
24
+
25
+ export interface ChannelControllerInterface {
26
+ receiveWebhook(data: any): Promise<any>;
27
+ }
28
+
29
+ export class ChannelController {
30
+ public prismaRepository: PrismaRepository;
31
+ public waMonitor: WAMonitoringService;
32
+
33
+ constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
34
+ this.prisma = prismaRepository;
35
+ this.monitor = waMonitor;
36
+ }
37
+
38
+ public set prisma(prisma: PrismaRepository) {
39
+ this.prismaRepository = prisma;
40
+ }
41
+
42
+ public get prisma() {
43
+ return this.prismaRepository;
44
+ }
45
+
46
+ public set monitor(waMonitor: WAMonitoringService) {
47
+ this.waMonitor = waMonitor;
48
+ }
49
+
50
+ public get monitor() {
51
+ return this.waMonitor;
52
+ }
53
+
54
+ public init(instanceData: InstanceDto, data: ChannelDataType) {
55
+ if (!instanceData.token && instanceData.integration === Integration.WHATSAPP_BUSINESS) {
56
+ throw new BadRequestException('token is required');
57
+ }
58
+
59
+ if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
60
+ return new BusinessStartupService(
61
+ data.configService,
62
+ data.eventEmitter,
63
+ data.prismaRepository,
64
+ data.cache,
65
+ data.chatwootCache,
66
+ data.baileysCache,
67
+ data.providerFiles,
68
+ );
69
+ }
70
+
71
+ if (instanceData.integration === Integration.EVOLUTION) {
72
+ return new EvolutionStartupService(
73
+ data.configService,
74
+ data.eventEmitter,
75
+ data.prismaRepository,
76
+ data.cache,
77
+ data.chatwootCache,
78
+ );
79
+ }
80
+
81
+ if (instanceData.integration === Integration.WHATSAPP_BAILEYS) {
82
+ return new BaileysStartupService(
83
+ data.configService,
84
+ data.eventEmitter,
85
+ data.prismaRepository,
86
+ data.cache,
87
+ data.chatwootCache,
88
+ data.baileysCache,
89
+ data.providerFiles,
90
+ );
91
+ }
92
+
93
+ return null;
94
+ }
95
+ }
src/api/integrations/channel/channel.router.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+
3
+ import { EvolutionRouter } from './evolution/evolution.router';
4
+ import { MetaRouter } from './meta/meta.router';
5
+ import { BaileysRouter } from './whatsapp/baileys.router';
6
+
7
+ export class ChannelRouter {
8
+ public readonly router: Router;
9
+
10
+ constructor(configService: any, ...guards: any[]) {
11
+ this.router = Router();
12
+
13
+ this.router.use('/', new EvolutionRouter(configService).router);
14
+ this.router.use('/', new MetaRouter(configService).router);
15
+ this.router.use('/baileys', new BaileysRouter(...guards).router);
16
+ }
17
+ }
src/api/integrations/channel/evolution/evolution.channel.service.ts ADDED
@@ -0,0 +1,888 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import {
3
+ MediaMessage,
4
+ Options,
5
+ SendAudioDto,
6
+ SendButtonsDto,
7
+ SendMediaDto,
8
+ SendTextDto,
9
+ } from '@api/dto/sendMessage.dto';
10
+ import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
11
+ import { PrismaRepository } from '@api/repository/repository.service';
12
+ import { chatbotController } from '@api/server.module';
13
+ import { CacheService } from '@api/services/cache.service';
14
+ import { ChannelStartupService } from '@api/services/channel.service';
15
+ import { Events, wa } from '@api/types/wa.types';
16
+ import { AudioConverter, Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
17
+ import { BadRequestException, InternalServerErrorException } from '@exceptions';
18
+ import { createJid } from '@utils/createJid';
19
+ import { sendTelemetry } from '@utils/sendTelemetry';
20
+ import axios from 'axios';
21
+ import { isBase64, isURL } from 'class-validator';
22
+ import EventEmitter2 from 'eventemitter2';
23
+ import FormData from 'form-data';
24
+ import mimeTypes from 'mime-types';
25
+ import { join } from 'path';
26
+ import { v4 } from 'uuid';
27
+
28
+ export class EvolutionStartupService extends ChannelStartupService {
29
+ constructor(
30
+ public readonly configService: ConfigService,
31
+ public readonly eventEmitter: EventEmitter2,
32
+ public readonly prismaRepository: PrismaRepository,
33
+ public readonly cache: CacheService,
34
+ public readonly chatwootCache: CacheService,
35
+ ) {
36
+ super(configService, eventEmitter, prismaRepository, chatwootCache);
37
+
38
+ this.client = null;
39
+ }
40
+
41
+ public client: any;
42
+
43
+ public stateConnection: wa.StateConnection = { state: 'open' };
44
+
45
+ public phoneNumber: string;
46
+ public mobile: boolean;
47
+
48
+ public get connectionStatus() {
49
+ return this.stateConnection;
50
+ }
51
+
52
+ public async closeClient() {
53
+ this.stateConnection = { state: 'close' };
54
+ }
55
+
56
+ public get qrCode(): wa.QrCode {
57
+ return {
58
+ pairingCode: this.instance.qrcode?.pairingCode,
59
+ code: this.instance.qrcode?.code,
60
+ base64: this.instance.qrcode?.base64,
61
+ count: this.instance.qrcode?.count,
62
+ };
63
+ }
64
+
65
+ public async logoutInstance() {
66
+ await this.closeClient();
67
+ }
68
+
69
+ public setInstance(instance: InstanceDto) {
70
+ this.logger.setInstance(instance.instanceId);
71
+
72
+ this.instance.name = instance.instanceName;
73
+ this.instance.id = instance.instanceId;
74
+ this.instance.integration = instance.integration;
75
+ this.instance.number = instance.number;
76
+ this.instance.token = instance.token;
77
+ this.instance.businessId = instance.businessId;
78
+
79
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
80
+ this.chatwootService.eventWhatsapp(
81
+ Events.STATUS_INSTANCE,
82
+ {
83
+ instanceName: this.instance.name,
84
+ instanceId: this.instance.id,
85
+ integration: instance.integration,
86
+ },
87
+ {
88
+ instance: this.instance.name,
89
+ status: 'created',
90
+ },
91
+ );
92
+ }
93
+ }
94
+
95
+ public async profilePicture(number: string) {
96
+ const jid = createJid(number);
97
+
98
+ return {
99
+ wuid: jid,
100
+ profilePictureUrl: null,
101
+ };
102
+ }
103
+
104
+ public async getProfileName() {
105
+ return null;
106
+ }
107
+
108
+ public async profilePictureUrl() {
109
+ return null;
110
+ }
111
+
112
+ public async getProfileStatus() {
113
+ return null;
114
+ }
115
+
116
+ public async connectToWhatsapp(data?: any): Promise<any> {
117
+ if (!data) {
118
+ this.loadChatwoot();
119
+ return;
120
+ }
121
+
122
+ try {
123
+ this.eventHandler(data);
124
+ } catch (error) {
125
+ this.logger.error(error);
126
+ throw new InternalServerErrorException(error?.toString());
127
+ }
128
+ }
129
+
130
+ protected async eventHandler(received: any) {
131
+ try {
132
+ let messageRaw: any;
133
+
134
+ if (received.message) {
135
+ const key = {
136
+ id: received.key.id || v4(),
137
+ remoteJid: received.key.remoteJid,
138
+ fromMe: received.key.fromMe,
139
+ profilePicUrl: received.profilePicUrl,
140
+ };
141
+ messageRaw = {
142
+ key,
143
+ pushName: received.pushName,
144
+ message: received.message,
145
+ messageType: received.messageType,
146
+ messageTimestamp: Math.round(new Date().getTime() / 1000),
147
+ source: 'unknown',
148
+ instanceId: this.instanceId,
149
+ };
150
+
151
+ const isAudio = received?.message?.audioMessage;
152
+
153
+ if (this.configService.get<Openai>('OPENAI').ENABLED && isAudio) {
154
+ const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
155
+ where: {
156
+ instanceId: this.instanceId,
157
+ },
158
+ include: {
159
+ OpenaiCreds: true,
160
+ },
161
+ });
162
+
163
+ if (
164
+ openAiDefaultSettings &&
165
+ openAiDefaultSettings.openaiCredsId &&
166
+ openAiDefaultSettings.speechToText &&
167
+ received?.message?.audioMessage
168
+ ) {
169
+ messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`;
170
+ }
171
+ }
172
+
173
+ this.logger.log(messageRaw);
174
+
175
+ sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
176
+
177
+ this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
178
+
179
+ await chatbotController.emit({
180
+ instance: { instanceName: this.instance.name, instanceId: this.instanceId },
181
+ remoteJid: messageRaw.key.remoteJid,
182
+ msg: messageRaw,
183
+ pushName: messageRaw.pushName,
184
+ });
185
+
186
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
187
+ const chatwootSentMessage = await this.chatwootService.eventWhatsapp(
188
+ Events.MESSAGES_UPSERT,
189
+ { instanceName: this.instance.name, instanceId: this.instanceId },
190
+ messageRaw,
191
+ );
192
+
193
+ if (chatwootSentMessage?.id) {
194
+ messageRaw.chatwootMessageId = chatwootSentMessage.id;
195
+ messageRaw.chatwootInboxId = chatwootSentMessage.id;
196
+ messageRaw.chatwootConversationId = chatwootSentMessage.id;
197
+ }
198
+ }
199
+
200
+ await this.prismaRepository.message.create({
201
+ data: messageRaw,
202
+ });
203
+
204
+ await this.updateContact({
205
+ remoteJid: messageRaw.key.remoteJid,
206
+ pushName: messageRaw.pushName,
207
+ profilePicUrl: received.profilePicUrl,
208
+ });
209
+ }
210
+ } catch (error) {
211
+ this.logger.error(error);
212
+ }
213
+ }
214
+
215
+ private async updateContact(data: { remoteJid: string; pushName?: string; profilePicUrl?: string }) {
216
+ const contactRaw: any = {
217
+ remoteJid: data.remoteJid,
218
+ pushName: data?.pushName,
219
+ instanceId: this.instanceId,
220
+ profilePicUrl: data?.profilePicUrl,
221
+ };
222
+
223
+ const existingContact = await this.prismaRepository.contact.findFirst({
224
+ where: {
225
+ remoteJid: data.remoteJid,
226
+ instanceId: this.instanceId,
227
+ },
228
+ });
229
+
230
+ if (existingContact) {
231
+ await this.prismaRepository.contact.updateMany({
232
+ where: {
233
+ remoteJid: data.remoteJid,
234
+ instanceId: this.instanceId,
235
+ },
236
+ data: contactRaw,
237
+ });
238
+ } else {
239
+ await this.prismaRepository.contact.create({
240
+ data: contactRaw,
241
+ });
242
+ }
243
+
244
+ this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
245
+
246
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
247
+ await this.chatwootService.eventWhatsapp(
248
+ Events.CONTACTS_UPDATE,
249
+ {
250
+ instanceName: this.instance.name,
251
+ instanceId: this.instanceId,
252
+ integration: this.instance.integration,
253
+ },
254
+ contactRaw,
255
+ );
256
+ }
257
+
258
+ const chat = await this.prismaRepository.chat.findFirst({
259
+ where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
260
+ });
261
+
262
+ if (chat) {
263
+ const chatRaw: any = {
264
+ remoteJid: data.remoteJid,
265
+ instanceId: this.instanceId,
266
+ };
267
+
268
+ this.sendDataWebhook(Events.CHATS_UPDATE, chatRaw);
269
+
270
+ await this.prismaRepository.chat.updateMany({
271
+ where: { remoteJid: chat.remoteJid },
272
+ data: chatRaw,
273
+ });
274
+ }
275
+
276
+ const chatRaw: any = {
277
+ remoteJid: data.remoteJid,
278
+ instanceId: this.instanceId,
279
+ };
280
+
281
+ this.sendDataWebhook(Events.CHATS_UPSERT, chatRaw);
282
+
283
+ await this.prismaRepository.chat.create({
284
+ data: chatRaw,
285
+ });
286
+ }
287
+
288
+ protected async sendMessageWithTyping(
289
+ number: string,
290
+ message: any,
291
+ options?: Options,
292
+ file?: any,
293
+ isIntegration = false,
294
+ ) {
295
+ try {
296
+ let quoted: any;
297
+ let webhookUrl: any;
298
+
299
+ if (options?.quoted) {
300
+ const m = options?.quoted;
301
+
302
+ const msg = m?.key;
303
+
304
+ if (!msg) {
305
+ throw 'Message not found';
306
+ }
307
+
308
+ quoted = msg;
309
+ }
310
+
311
+ if (options.delay) {
312
+ await new Promise((resolve) => setTimeout(resolve, options.delay));
313
+ }
314
+
315
+ if (options?.webhookUrl) {
316
+ webhookUrl = options.webhookUrl;
317
+ }
318
+
319
+ let audioFile;
320
+
321
+ const messageId = v4();
322
+
323
+ let messageRaw: any;
324
+
325
+ if (message?.mediaType === 'image') {
326
+ messageRaw = {
327
+ key: { fromMe: true, id: messageId, remoteJid: number },
328
+ message: {
329
+ base64: isBase64(message.media) ? message.media : null,
330
+ mediaUrl: isURL(message.media) ? message.media : null,
331
+ quoted,
332
+ },
333
+ messageType: 'imageMessage',
334
+ messageTimestamp: Math.round(new Date().getTime() / 1000),
335
+ webhookUrl,
336
+ source: 'unknown',
337
+ instanceId: this.instanceId,
338
+ };
339
+ } else if (message?.mediaType === 'video') {
340
+ messageRaw = {
341
+ key: { fromMe: true, id: messageId, remoteJid: number },
342
+ message: {
343
+ base64: isBase64(message.media) ? message.media : null,
344
+ mediaUrl: isURL(message.media) ? message.media : null,
345
+ quoted,
346
+ },
347
+ messageType: 'videoMessage',
348
+ messageTimestamp: Math.round(new Date().getTime() / 1000),
349
+ webhookUrl,
350
+ source: 'unknown',
351
+ instanceId: this.instanceId,
352
+ };
353
+ } else if (message?.mediaType === 'audio') {
354
+ messageRaw = {
355
+ key: { fromMe: true, id: messageId, remoteJid: number },
356
+ message: {
357
+ base64: isBase64(message.media) ? message.media : null,
358
+ mediaUrl: isURL(message.media) ? message.media : null,
359
+ quoted,
360
+ },
361
+ messageType: 'audioMessage',
362
+ messageTimestamp: Math.round(new Date().getTime() / 1000),
363
+ webhookUrl,
364
+ source: 'unknown',
365
+ instanceId: this.instanceId,
366
+ };
367
+
368
+ const buffer = Buffer.from(message.media, 'base64');
369
+ audioFile = {
370
+ buffer,
371
+ mimetype: 'audio/mp4',
372
+ originalname: `${messageId}.mp4`,
373
+ };
374
+ } else if (message?.mediaType === 'document') {
375
+ messageRaw = {
376
+ key: { fromMe: true, id: messageId, remoteJid: number },
377
+ message: {
378
+ base64: isBase64(message.media) ? message.media : null,
379
+ mediaUrl: isURL(message.media) ? message.media : null,
380
+ quoted,
381
+ },
382
+ messageType: 'documentMessage',
383
+ messageTimestamp: Math.round(new Date().getTime() / 1000),
384
+ webhookUrl,
385
+ source: 'unknown',
386
+ instanceId: this.instanceId,
387
+ };
388
+ } else if (message.buttonMessage) {
389
+ messageRaw = {
390
+ key: { fromMe: true, id: messageId, remoteJid: number },
391
+ message: {
392
+ ...message.buttonMessage,
393
+ buttons: message.buttonMessage.buttons,
394
+ footer: message.buttonMessage.footer,
395
+ body: message.buttonMessage.body,
396
+ quoted,
397
+ },
398
+ messageType: 'buttonMessage',
399
+ messageTimestamp: Math.round(new Date().getTime() / 1000),
400
+ webhookUrl,
401
+ source: 'unknown',
402
+ instanceId: this.instanceId,
403
+ };
404
+ } else if (message.listMessage) {
405
+ messageRaw = {
406
+ key: { fromMe: true, id: messageId, remoteJid: number },
407
+ message: {
408
+ ...message.listMessage,
409
+ quoted,
410
+ },
411
+ messageType: 'listMessage',
412
+ messageTimestamp: Math.round(new Date().getTime() / 1000),
413
+ webhookUrl,
414
+ source: 'unknown',
415
+ instanceId: this.instanceId,
416
+ };
417
+ } else {
418
+ messageRaw = {
419
+ key: { fromMe: true, id: messageId, remoteJid: number },
420
+ message: {
421
+ ...message,
422
+ quoted,
423
+ },
424
+ messageType: 'conversation',
425
+ messageTimestamp: Math.round(new Date().getTime() / 1000),
426
+ webhookUrl,
427
+ source: 'unknown',
428
+ instanceId: this.instanceId,
429
+ };
430
+ }
431
+
432
+ if (messageRaw.message.contextInfo) {
433
+ messageRaw.contextInfo = {
434
+ ...messageRaw.message.contextInfo,
435
+ };
436
+ }
437
+
438
+ if (messageRaw.contextInfo?.stanzaId) {
439
+ const key: any = {
440
+ id: messageRaw.contextInfo.stanzaId,
441
+ };
442
+
443
+ const findMessage = await this.prismaRepository.message.findFirst({
444
+ where: {
445
+ instanceId: this.instanceId,
446
+ key,
447
+ },
448
+ });
449
+
450
+ if (findMessage) {
451
+ messageRaw.contextInfo.quotedMessage = findMessage.message;
452
+ }
453
+ }
454
+
455
+ const { base64 } = messageRaw.message;
456
+ delete messageRaw.message.base64;
457
+
458
+ if (base64 || file || audioFile) {
459
+ if (this.configService.get<S3>('S3').ENABLE) {
460
+ try {
461
+ // Verificação adicional para garantir que há conteúdo de mídia real
462
+ const hasRealMedia = this.hasValidMediaContent(messageRaw);
463
+
464
+ if (!hasRealMedia) {
465
+ this.logger.warn('Message detected as media but contains no valid media content');
466
+ } else {
467
+ const fileBuffer = audioFile?.buffer || file?.buffer;
468
+ const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
469
+
470
+ let mediaType: string;
471
+ let mimetype = audioFile?.mimetype || file.mimetype;
472
+
473
+ if (messageRaw.messageType === 'documentMessage') {
474
+ mediaType = 'document';
475
+ mimetype = !mimetype ? 'application/pdf' : mimetype;
476
+ } else if (messageRaw.messageType === 'imageMessage') {
477
+ mediaType = 'image';
478
+ mimetype = !mimetype ? 'image/png' : mimetype;
479
+ } else if (messageRaw.messageType === 'audioMessage') {
480
+ mediaType = 'audio';
481
+ mimetype = !mimetype ? 'audio/mp4' : mimetype;
482
+ } else if (messageRaw.messageType === 'videoMessage') {
483
+ mediaType = 'video';
484
+ mimetype = !mimetype ? 'video/mp4' : mimetype;
485
+ }
486
+
487
+ const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
488
+
489
+ const size = buffer.byteLength;
490
+
491
+ const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
492
+
493
+ await s3Service.uploadFile(fullName, buffer, size, {
494
+ 'Content-Type': mimetype,
495
+ });
496
+
497
+ const mediaUrl = await s3Service.getObjectUrl(fullName);
498
+
499
+ messageRaw.message.mediaUrl = mediaUrl;
500
+ }
501
+ } catch (error) {
502
+ this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
503
+ }
504
+ }
505
+ }
506
+
507
+ this.logger.log(messageRaw);
508
+
509
+ this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
510
+
511
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) {
512
+ this.chatwootService.eventWhatsapp(
513
+ Events.SEND_MESSAGE,
514
+ { instanceName: this.instance.name, instanceId: this.instanceId },
515
+ messageRaw,
516
+ );
517
+ }
518
+
519
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && isIntegration)
520
+ await chatbotController.emit({
521
+ instance: { instanceName: this.instance.name, instanceId: this.instanceId },
522
+ remoteJid: messageRaw.key.remoteJid,
523
+ msg: messageRaw,
524
+ pushName: messageRaw.pushName,
525
+ });
526
+
527
+ await this.prismaRepository.message.create({
528
+ data: messageRaw,
529
+ });
530
+
531
+ return messageRaw;
532
+ } catch (error) {
533
+ this.logger.error(error);
534
+ throw new BadRequestException(error.toString());
535
+ }
536
+ }
537
+
538
+ public async textMessage(data: SendTextDto, isIntegration = false) {
539
+ const res = await this.sendMessageWithTyping(
540
+ data.number,
541
+ {
542
+ conversation: data.text,
543
+ },
544
+ {
545
+ delay: data?.delay,
546
+ presence: 'composing',
547
+ quoted: data?.quoted,
548
+ linkPreview: data?.linkPreview,
549
+ mentionsEveryOne: data?.mentionsEveryOne,
550
+ mentioned: data?.mentioned,
551
+ },
552
+ null,
553
+ isIntegration,
554
+ );
555
+ return res;
556
+ }
557
+
558
+ protected async prepareMediaMessage(mediaMessage: MediaMessage) {
559
+ try {
560
+ if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) {
561
+ const regex = new RegExp(/.*\/(.+?)\./);
562
+ const arrayMatch = regex.exec(mediaMessage.media);
563
+ mediaMessage.fileName = arrayMatch[1];
564
+ }
565
+
566
+ if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) {
567
+ mediaMessage.fileName = 'image.png';
568
+ }
569
+
570
+ if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) {
571
+ mediaMessage.fileName = 'video.mp4';
572
+ }
573
+
574
+ let mimetype: string | false;
575
+
576
+ const prepareMedia: any = {
577
+ caption: mediaMessage?.caption,
578
+ fileName: mediaMessage.fileName,
579
+ mediaType: mediaMessage.mediatype,
580
+ media: mediaMessage.media,
581
+ gifPlayback: false,
582
+ };
583
+
584
+ if (isURL(mediaMessage.media)) {
585
+ mimetype = mimeTypes.lookup(mediaMessage.media);
586
+ } else {
587
+ mimetype = mimeTypes.lookup(mediaMessage.fileName);
588
+ }
589
+
590
+ prepareMedia.mimetype = mimetype;
591
+
592
+ return prepareMedia;
593
+ } catch (error) {
594
+ this.logger.error(error);
595
+ throw new InternalServerErrorException(error?.toString() || error);
596
+ }
597
+ }
598
+
599
+ public async mediaMessage(data: SendMediaDto, file?: any, isIntegration = false) {
600
+ const mediaData: SendMediaDto = { ...data };
601
+
602
+ if (file) mediaData.media = file.buffer.toString('base64');
603
+
604
+ const message = await this.prepareMediaMessage(mediaData);
605
+
606
+ const mediaSent = await this.sendMessageWithTyping(
607
+ data.number,
608
+ { ...message },
609
+ {
610
+ delay: data?.delay,
611
+ presence: 'composing',
612
+ quoted: data?.quoted,
613
+ linkPreview: data?.linkPreview,
614
+ mentionsEveryOne: data?.mentionsEveryOne,
615
+ mentioned: data?.mentioned,
616
+ },
617
+ file,
618
+ isIntegration,
619
+ );
620
+
621
+ return mediaSent;
622
+ }
623
+
624
+ public async processAudio(audio: string, number: string, file: any) {
625
+ number = number.replace(/\D/g, '');
626
+ const hash = `${number}-${new Date().getTime()}`;
627
+
628
+ const audioConverterConfig = this.configService.get<AudioConverter>('AUDIO_CONVERTER');
629
+ if (audioConverterConfig.API_URL) {
630
+ try {
631
+ this.logger.verbose('Using audio converter API');
632
+ const formData = new FormData();
633
+
634
+ if (file) {
635
+ formData.append('file', file.buffer, {
636
+ filename: file.originalname,
637
+ contentType: file.mimetype,
638
+ });
639
+ } else if (isURL(audio)) {
640
+ formData.append('url', audio);
641
+ } else {
642
+ formData.append('base64', audio);
643
+ }
644
+
645
+ formData.append('format', 'mp4');
646
+
647
+ const response = await axios.post(audioConverterConfig.API_URL, formData, {
648
+ headers: {
649
+ ...formData.getHeaders(),
650
+ apikey: audioConverterConfig.API_KEY,
651
+ },
652
+ });
653
+
654
+ if (!response?.data?.audio) {
655
+ throw new InternalServerErrorException('Failed to convert audio');
656
+ }
657
+
658
+ const prepareMedia: any = {
659
+ fileName: `${hash}.mp4`,
660
+ mediaType: 'audio',
661
+ media: response?.data?.audio,
662
+ mimetype: 'audio/mpeg',
663
+ };
664
+
665
+ return prepareMedia;
666
+ } catch (error) {
667
+ this.logger.error(error?.response?.data || error);
668
+ throw new InternalServerErrorException(error?.response?.data?.message || error?.toString() || error);
669
+ }
670
+ } else {
671
+ let mimetype: string;
672
+
673
+ const prepareMedia: any = {
674
+ fileName: `${hash}.mp3`,
675
+ mediaType: 'audio',
676
+ media: audio,
677
+ mimetype: 'audio/mpeg',
678
+ };
679
+
680
+ if (isURL(audio)) {
681
+ mimetype = mimeTypes.lookup(audio).toString();
682
+ } else {
683
+ mimetype = mimeTypes.lookup(prepareMedia.fileName).toString();
684
+ }
685
+
686
+ prepareMedia.mimetype = mimetype;
687
+
688
+ return prepareMedia;
689
+ }
690
+ }
691
+
692
+ public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
693
+ const mediaData: SendAudioDto = { ...data };
694
+
695
+ if (file?.buffer) {
696
+ mediaData.audio = file.buffer.toString('base64');
697
+ } else {
698
+ console.error('El archivo o buffer no est� definido correctamente.');
699
+ throw new Error('File or buffer is undefined.');
700
+ }
701
+
702
+ const message = await this.processAudio(mediaData.audio, data.number, file);
703
+
704
+ const audioSent = await this.sendMessageWithTyping(
705
+ data.number,
706
+ { ...message },
707
+ {
708
+ delay: data?.delay,
709
+ presence: 'composing',
710
+ quoted: data?.quoted,
711
+ linkPreview: data?.linkPreview,
712
+ mentionsEveryOne: data?.mentionsEveryOne,
713
+ mentioned: data?.mentioned,
714
+ },
715
+ file,
716
+ isIntegration,
717
+ );
718
+
719
+ return audioSent;
720
+ }
721
+
722
+ public async buttonMessage(data: SendButtonsDto, isIntegration = false) {
723
+ return await this.sendMessageWithTyping(
724
+ data.number,
725
+ {
726
+ buttonMessage: {
727
+ title: data.title,
728
+ description: data.description,
729
+ footer: data.footer,
730
+ buttons: data.buttons,
731
+ },
732
+ },
733
+ {
734
+ delay: data?.delay,
735
+ presence: 'composing',
736
+ quoted: data?.quoted,
737
+ mentionsEveryOne: data?.mentionsEveryOne,
738
+ mentioned: data?.mentioned,
739
+ },
740
+ null,
741
+ isIntegration,
742
+ );
743
+ }
744
+ public async locationMessage() {
745
+ throw new BadRequestException('Method not available on Evolution Channel');
746
+ }
747
+ public async listMessage() {
748
+ throw new BadRequestException('Method not available on Evolution Channel');
749
+ }
750
+ public async templateMessage() {
751
+ throw new BadRequestException('Method not available on Evolution Channel');
752
+ }
753
+ public async contactMessage() {
754
+ throw new BadRequestException('Method not available on Evolution Channel');
755
+ }
756
+ public async reactionMessage() {
757
+ throw new BadRequestException('Method not available on Evolution Channel');
758
+ }
759
+ public async getBase64FromMediaMessage() {
760
+ throw new BadRequestException('Method not available on Evolution Channel');
761
+ }
762
+ public async deleteMessage() {
763
+ throw new BadRequestException('Method not available on Evolution Channel');
764
+ }
765
+ public async mediaSticker() {
766
+ throw new BadRequestException('Method not available on Evolution Channel');
767
+ }
768
+ public async pollMessage() {
769
+ throw new BadRequestException('Method not available on Evolution Channel');
770
+ }
771
+ public async statusMessage() {
772
+ throw new BadRequestException('Method not available on Evolution Channel');
773
+ }
774
+ public async reloadConnection() {
775
+ throw new BadRequestException('Method not available on Evolution Channel');
776
+ }
777
+ public async whatsappNumber() {
778
+ throw new BadRequestException('Method not available on Evolution Channel');
779
+ }
780
+ public async markMessageAsRead() {
781
+ throw new BadRequestException('Method not available on Evolution Channel');
782
+ }
783
+ public async archiveChat() {
784
+ throw new BadRequestException('Method not available on Evolution Channel');
785
+ }
786
+ public async markChatUnread() {
787
+ throw new BadRequestException('Method not available on Evolution Channel');
788
+ }
789
+ public async fetchProfile() {
790
+ throw new BadRequestException('Method not available on Evolution Channel');
791
+ }
792
+ public async offerCall() {
793
+ throw new BadRequestException('Method not available on WhatsApp Business API');
794
+ }
795
+ public async sendPresence() {
796
+ throw new BadRequestException('Method not available on Evolution Channel');
797
+ }
798
+ public async setPresence() {
799
+ throw new BadRequestException('Method not available on Evolution Channel');
800
+ }
801
+ public async fetchPrivacySettings() {
802
+ throw new BadRequestException('Method not available on Evolution Channel');
803
+ }
804
+ public async updatePrivacySettings() {
805
+ throw new BadRequestException('Method not available on Evolution Channel');
806
+ }
807
+ public async fetchBusinessProfile() {
808
+ throw new BadRequestException('Method not available on Evolution Channel');
809
+ }
810
+ public async updateProfileName() {
811
+ throw new BadRequestException('Method not available on Evolution Channel');
812
+ }
813
+ public async updateProfileStatus() {
814
+ throw new BadRequestException('Method not available on Evolution Channel');
815
+ }
816
+ public async updateProfilePicture() {
817
+ throw new BadRequestException('Method not available on Evolution Channel');
818
+ }
819
+ public async removeProfilePicture() {
820
+ throw new BadRequestException('Method not available on Evolution Channel');
821
+ }
822
+ public async blockUser() {
823
+ throw new BadRequestException('Method not available on Evolution Channel');
824
+ }
825
+ public async updateMessage() {
826
+ throw new BadRequestException('Method not available on Evolution Channel');
827
+ }
828
+ public async createGroup() {
829
+ throw new BadRequestException('Method not available on Evolution Channel');
830
+ }
831
+ public async updateGroupPicture() {
832
+ throw new BadRequestException('Method not available on Evolution Channel');
833
+ }
834
+ public async updateGroupSubject() {
835
+ throw new BadRequestException('Method not available on Evolution Channel');
836
+ }
837
+ public async updateGroupDescription() {
838
+ throw new BadRequestException('Method not available on Evolution Channel');
839
+ }
840
+ public async findGroup() {
841
+ throw new BadRequestException('Method not available on Evolution Channel');
842
+ }
843
+ public async fetchAllGroups() {
844
+ throw new BadRequestException('Method not available on Evolution Channel');
845
+ }
846
+ public async inviteCode() {
847
+ throw new BadRequestException('Method not available on Evolution Channel');
848
+ }
849
+ public async inviteInfo() {
850
+ throw new BadRequestException('Method not available on Evolution Channel');
851
+ }
852
+ public async sendInvite() {
853
+ throw new BadRequestException('Method not available on Evolution Channel');
854
+ }
855
+ public async acceptInviteCode() {
856
+ throw new BadRequestException('Method not available on Evolution Channel');
857
+ }
858
+ public async revokeInviteCode() {
859
+ throw new BadRequestException('Method not available on Evolution Channel');
860
+ }
861
+ public async findParticipants() {
862
+ throw new BadRequestException('Method not available on Evolution Channel');
863
+ }
864
+ public async updateGParticipant() {
865
+ throw new BadRequestException('Method not available on Evolution Channel');
866
+ }
867
+ public async updateGSetting() {
868
+ throw new BadRequestException('Method not available on Evolution Channel');
869
+ }
870
+ public async toggleEphemeral() {
871
+ throw new BadRequestException('Method not available on Evolution Channel');
872
+ }
873
+ public async leaveGroup() {
874
+ throw new BadRequestException('Method not available on Evolution Channel');
875
+ }
876
+ public async fetchLabels() {
877
+ throw new BadRequestException('Method not available on Evolution Channel');
878
+ }
879
+ public async handleLabel() {
880
+ throw new BadRequestException('Method not available on Evolution Channel');
881
+ }
882
+ public async receiveMobileCode() {
883
+ throw new BadRequestException('Method not available on Evolution Channel');
884
+ }
885
+ public async fakeCall() {
886
+ throw new BadRequestException('Method not available on Evolution Channel');
887
+ }
888
+ }
src/api/integrations/channel/evolution/evolution.controller.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@api/repository/repository.service';
2
+ import { WAMonitoringService } from '@api/services/monitor.service';
3
+ import { Logger } from '@config/logger.config';
4
+
5
+ import { ChannelController, ChannelControllerInterface } from '../channel.controller';
6
+
7
+ export class EvolutionController extends ChannelController implements ChannelControllerInterface {
8
+ private readonly logger = new Logger('EvolutionController');
9
+
10
+ constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
11
+ super(prismaRepository, waMonitor);
12
+ }
13
+
14
+ integrationEnabled: boolean;
15
+
16
+ public async receiveWebhook(data: any) {
17
+ const numberId = data.numberId;
18
+
19
+ if (!numberId) {
20
+ this.logger.error('WebhookService -> receiveWebhookEvolution -> numberId not found');
21
+ return;
22
+ }
23
+
24
+ const instance = await this.prismaRepository.instance.findFirst({
25
+ where: { number: numberId },
26
+ });
27
+
28
+ if (!instance) {
29
+ this.logger.error('WebhookService -> receiveWebhook -> instance not found');
30
+ return;
31
+ }
32
+
33
+ await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data);
34
+
35
+ return {
36
+ status: 'success',
37
+ };
38
+ }
39
+ }
src/api/integrations/channel/evolution/evolution.router.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RouterBroker } from '@api/abstract/abstract.router';
2
+ import { evolutionController } from '@api/server.module';
3
+ import { ConfigService } from '@config/env.config';
4
+ import { Router } from 'express';
5
+
6
+ export class EvolutionRouter extends RouterBroker {
7
+ constructor(readonly configService: ConfigService) {
8
+ super();
9
+ this.router.post(this.routerPath('webhook/evolution', false), async (req, res) => {
10
+ const { body } = req;
11
+ const response = await evolutionController.receiveWebhook(body);
12
+
13
+ return res.status(200).json(response);
14
+ });
15
+ }
16
+
17
+ public readonly router: Router = Router();
18
+ }
src/api/integrations/channel/meta/meta.controller.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaRepository } from '@api/repository/repository.service';
2
+ import { WAMonitoringService } from '@api/services/monitor.service';
3
+ import { Logger } from '@config/logger.config';
4
+ import axios from 'axios';
5
+
6
+ import { ChannelController, ChannelControllerInterface } from '../channel.controller';
7
+
8
+ export class MetaController extends ChannelController implements ChannelControllerInterface {
9
+ private readonly logger = new Logger('MetaController');
10
+
11
+ constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
12
+ super(prismaRepository, waMonitor);
13
+ }
14
+
15
+ integrationEnabled: boolean;
16
+
17
+ public async receiveWebhook(data: any) {
18
+ if (data.object === 'whatsapp_business_account') {
19
+ if (data.entry[0]?.changes[0]?.field === 'message_template_status_update') {
20
+ const template = await this.prismaRepository.template.findFirst({
21
+ where: { templateId: `${data.entry[0].changes[0].value.message_template_id}` },
22
+ });
23
+
24
+ if (!template) {
25
+ console.log('template not found');
26
+ return;
27
+ }
28
+
29
+ const { webhookUrl } = template;
30
+
31
+ await axios.post(webhookUrl, data.entry[0].changes[0].value, {
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ },
35
+ });
36
+ return;
37
+ }
38
+
39
+ data.entry?.forEach(async (entry: any) => {
40
+ const numberId = entry.changes[0].value.metadata.phone_number_id;
41
+
42
+ if (!numberId) {
43
+ this.logger.error('WebhookService -> receiveWebhookMeta -> numberId not found');
44
+ return {
45
+ status: 'success',
46
+ };
47
+ }
48
+
49
+ const instance = await this.prismaRepository.instance.findFirst({
50
+ where: { number: numberId },
51
+ });
52
+
53
+ if (!instance) {
54
+ this.logger.error('WebhookService -> receiveWebhookMeta -> instance not found');
55
+ return {
56
+ status: 'success',
57
+ };
58
+ }
59
+
60
+ await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data);
61
+
62
+ return {
63
+ status: 'success',
64
+ };
65
+ });
66
+ }
67
+
68
+ return {
69
+ status: 'success',
70
+ };
71
+ }
72
+ }
src/api/integrations/channel/meta/meta.router.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RouterBroker } from '@api/abstract/abstract.router';
2
+ import { metaController } from '@api/server.module';
3
+ import { ConfigService, WaBusiness } from '@config/env.config';
4
+ import { Router } from 'express';
5
+
6
+ export class MetaRouter extends RouterBroker {
7
+ constructor(readonly configService: ConfigService) {
8
+ super();
9
+ this.router
10
+ .get(this.routerPath('webhook/meta', false), async (req, res) => {
11
+ if (req.query['hub.verify_token'] === configService.get<WaBusiness>('WA_BUSINESS').TOKEN_WEBHOOK)
12
+ res.send(req.query['hub.challenge']);
13
+ else res.send('Error, wrong validation token');
14
+ })
15
+ .post(this.routerPath('webhook/meta', false), async (req, res) => {
16
+ const { body } = req;
17
+ const response = await metaController.receiveWebhook(body);
18
+
19
+ return res.status(200).json(response);
20
+ });
21
+ }
22
+
23
+ public readonly router: Router = Router();
24
+ }
src/api/integrations/channel/meta/whatsapp.business.service.ts ADDED
@@ -0,0 +1,1755 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NumberBusiness } from '@api/dto/chat.dto';
2
+ import {
3
+ ContactMessage,
4
+ MediaMessage,
5
+ Options,
6
+ SendAudioDto,
7
+ SendButtonsDto,
8
+ SendContactDto,
9
+ SendListDto,
10
+ SendLocationDto,
11
+ SendMediaDto,
12
+ SendReactionDto,
13
+ SendTemplateDto,
14
+ SendTextDto,
15
+ } from '@api/dto/sendMessage.dto';
16
+ import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
17
+ import { ProviderFiles } from '@api/provider/sessions';
18
+ import { PrismaRepository } from '@api/repository/repository.service';
19
+ import { chatbotController } from '@api/server.module';
20
+ import { CacheService } from '@api/services/cache.service';
21
+ import { ChannelStartupService } from '@api/services/channel.service';
22
+ import { Events, wa } from '@api/types/wa.types';
23
+ import { AudioConverter, Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config';
24
+ import { BadRequestException, InternalServerErrorException } from '@exceptions';
25
+ import { createJid } from '@utils/createJid';
26
+ import { status } from '@utils/renderStatus';
27
+ import { sendTelemetry } from '@utils/sendTelemetry';
28
+ import axios from 'axios';
29
+ import { arrayUnique, isURL } from 'class-validator';
30
+ import EventEmitter2 from 'eventemitter2';
31
+ import FormData from 'form-data';
32
+ import mimeTypes from 'mime-types';
33
+ import { join } from 'path';
34
+
35
+ export class BusinessStartupService extends ChannelStartupService {
36
+ constructor(
37
+ public readonly configService: ConfigService,
38
+ public readonly eventEmitter: EventEmitter2,
39
+ public readonly prismaRepository: PrismaRepository,
40
+ public readonly cache: CacheService,
41
+ public readonly chatwootCache: CacheService,
42
+ public readonly baileysCache: CacheService,
43
+ private readonly providerFiles: ProviderFiles,
44
+ ) {
45
+ super(configService, eventEmitter, prismaRepository, chatwootCache);
46
+ }
47
+
48
+ public stateConnection: wa.StateConnection = { state: 'open' };
49
+
50
+ public phoneNumber: string;
51
+ public mobile: boolean;
52
+
53
+ public get connectionStatus() {
54
+ return this.stateConnection;
55
+ }
56
+
57
+ public async closeClient() {
58
+ this.stateConnection = { state: 'close' };
59
+ }
60
+
61
+ public get qrCode(): wa.QrCode {
62
+ return {
63
+ pairingCode: this.instance.qrcode?.pairingCode,
64
+ code: this.instance.qrcode?.code,
65
+ base64: this.instance.qrcode?.base64,
66
+ count: this.instance.qrcode?.count,
67
+ };
68
+ }
69
+
70
+ public async logoutInstance() {
71
+ await this.closeClient();
72
+ }
73
+
74
+ private isMediaMessage(message: any) {
75
+ return message.document || message.image || message.audio || message.video;
76
+ }
77
+
78
+ private async post(message: any, params: string) {
79
+ try {
80
+ let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
81
+ const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
82
+ urlServer = `${urlServer}/${version}/${this.number}/${params}`;
83
+ const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
84
+ const result = await axios.post(urlServer, message, { headers });
85
+ return result.data;
86
+ } catch (e) {
87
+ return e.response?.data?.error;
88
+ }
89
+ }
90
+
91
+ public async profilePicture(number: string) {
92
+ const jid = createJid(number);
93
+
94
+ return {
95
+ wuid: jid,
96
+ profilePictureUrl: null,
97
+ };
98
+ }
99
+
100
+ public async getProfileName() {
101
+ return null;
102
+ }
103
+
104
+ public async profilePictureUrl() {
105
+ return null;
106
+ }
107
+
108
+ public async getProfileStatus() {
109
+ return null;
110
+ }
111
+
112
+ public async setWhatsappBusinessProfile(data: NumberBusiness): Promise<any> {
113
+ const content = {
114
+ messaging_product: 'whatsapp',
115
+ about: data.about,
116
+ address: data.address,
117
+ description: data.description,
118
+ vertical: data.vertical,
119
+ email: data.email,
120
+ websites: data.websites,
121
+ profile_picture_handle: data.profilehandle,
122
+ };
123
+ return await this.post(content, 'whatsapp_business_profile');
124
+ }
125
+
126
+ public async connectToWhatsapp(data?: any): Promise<any> {
127
+ if (!data) return;
128
+
129
+ const content = data.entry[0].changes[0].value;
130
+
131
+ try {
132
+ this.loadChatwoot();
133
+
134
+ this.eventHandler(content);
135
+
136
+ this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id);
137
+ } catch (error) {
138
+ this.logger.error(error);
139
+ throw new InternalServerErrorException(error?.toString());
140
+ }
141
+ }
142
+
143
+ private async downloadMediaMessage(message: any) {
144
+ try {
145
+ const id = message[message.type].id;
146
+ let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
147
+ const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
148
+ urlServer = `${urlServer}/${version}/${id}`;
149
+ const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
150
+
151
+ // Primeiro, obtenha a URL do arquivo
152
+ let result = await axios.get(urlServer, { headers });
153
+
154
+ // Depois, baixe o arquivo usando a URL retornada
155
+ result = await axios.get(result.data.url, {
156
+ headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
157
+ responseType: 'arraybuffer',
158
+ });
159
+
160
+ return result.data;
161
+ } catch (e) {
162
+ this.logger.error(`Error downloading media: ${e}`);
163
+ throw e;
164
+ }
165
+ }
166
+
167
+ private messageMediaJson(received: any) {
168
+ const message = received.messages[0];
169
+ let content: any = message.type + 'Message';
170
+ content = { [content]: message[message.type] };
171
+ if (message.context) {
172
+ content = { ...content, contextInfo: { stanzaId: message.context.id } };
173
+ }
174
+ return content;
175
+ }
176
+
177
+ private messageAudioJson(received: any) {
178
+ const message = received.messages[0];
179
+ let content: any = {
180
+ audioMessage: {
181
+ ...message.audio,
182
+ ptt: message.audio.voice || false, // Define se é mensagem de voz
183
+ },
184
+ };
185
+ if (message.context) {
186
+ content = { ...content, contextInfo: { stanzaId: message.context.id } };
187
+ }
188
+ return content;
189
+ }
190
+
191
+ private messageInteractiveJson(received: any) {
192
+ const message = received.messages[0];
193
+ let content: any = { conversation: message.interactive[message.interactive.type].title };
194
+ message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
195
+ return content;
196
+ }
197
+
198
+ private messageButtonJson(received: any) {
199
+ const message = received.messages[0];
200
+ let content: any = { conversation: received.messages[0].button?.text };
201
+ message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
202
+ return content;
203
+ }
204
+
205
+ private messageReactionJson(received: any) {
206
+ const message = received.messages[0];
207
+ let content: any = {
208
+ reactionMessage: {
209
+ key: {
210
+ id: message.reaction.message_id,
211
+ },
212
+ text: message.reaction.emoji,
213
+ },
214
+ };
215
+ message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
216
+ return content;
217
+ }
218
+
219
+ private messageTextJson(received: any) {
220
+ // Verificar que received y received.messages existen
221
+ if (!received || !received.messages || received.messages.length === 0) {
222
+ this.logger.error('Error: received object or messages array is undefined or empty');
223
+ return null;
224
+ }
225
+
226
+ const message = received.messages[0];
227
+ let content: any;
228
+
229
+ // Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
230
+ if (!message.text) {
231
+ // Si no hay texto, manejamos diferente según el tipo de mensaje
232
+ if (message.type === 'sticker') {
233
+ content = { stickerMessage: {} };
234
+ } else if (message.type === 'location') {
235
+ content = {
236
+ locationMessage: {
237
+ degreesLatitude: message.location?.latitude,
238
+ degreesLongitude: message.location?.longitude,
239
+ name: message.location?.name,
240
+ address: message.location?.address,
241
+ },
242
+ };
243
+ } else {
244
+ // Para otros tipos de mensajes sin texto, creamos un contenido genérico
245
+ this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
246
+ content = { [message.type + 'Message']: message[message.type] || {} };
247
+ }
248
+
249
+ // Añadir contexto si existe
250
+ if (message.context) {
251
+ content = { ...content, contextInfo: { stanzaId: message.context.id } };
252
+ }
253
+
254
+ return content;
255
+ }
256
+
257
+ // Si el mensaje tiene texto, procesamos normalmente
258
+ if (!received.metadata || !received.metadata.phone_number_id) {
259
+ this.logger.error('Error: metadata or phone_number_id is undefined');
260
+ return null;
261
+ }
262
+
263
+ if (message.from === received.metadata.phone_number_id) {
264
+ content = {
265
+ extendedTextMessage: { text: message.text.body },
266
+ };
267
+ if (message.context) {
268
+ content = { ...content, contextInfo: { stanzaId: message.context.id } };
269
+ }
270
+ } else {
271
+ content = { conversation: message.text.body };
272
+ if (message.context) {
273
+ content = { ...content, contextInfo: { stanzaId: message.context.id } };
274
+ }
275
+ }
276
+
277
+ return content;
278
+ }
279
+
280
+ private messageLocationJson(received: any) {
281
+ const message = received.messages[0];
282
+ let content: any = {
283
+ locationMessage: {
284
+ degreesLatitude: message.location.latitude,
285
+ degreesLongitude: message.location.longitude,
286
+ name: message.location?.name,
287
+ address: message.location?.address,
288
+ },
289
+ };
290
+ message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
291
+ return content;
292
+ }
293
+
294
+ private messageContactsJson(received: any) {
295
+ const message = received.messages[0];
296
+ let content: any = {};
297
+
298
+ const vcard = (contact: any) => {
299
+ let result =
300
+ 'BEGIN:VCARD\n' +
301
+ 'VERSION:3.0\n' +
302
+ `N:${contact.name.formatted_name}\n` +
303
+ `FN:${contact.name.formatted_name}\n`;
304
+
305
+ if (contact.org) {
306
+ result += `ORG:${contact.org.company};\n`;
307
+ }
308
+
309
+ if (contact.emails) {
310
+ result += `EMAIL:${contact.emails[0].email}\n`;
311
+ }
312
+
313
+ if (contact.urls) {
314
+ result += `URL:${contact.urls[0].url}\n`;
315
+ }
316
+
317
+ if (!contact.phones[0]?.wa_id) {
318
+ contact.phones[0].wa_id = createJid(contact.phones[0].phone);
319
+ }
320
+
321
+ result +=
322
+ `item1.TEL;waid=${contact.phones[0]?.wa_id}:${contact.phones[0].phone}\n` +
323
+ 'item1.X-ABLabel:Celular\n' +
324
+ 'END:VCARD';
325
+
326
+ return result;
327
+ };
328
+
329
+ if (message.contacts.length === 1) {
330
+ content.contactMessage = {
331
+ displayName: message.contacts[0].name.formatted_name,
332
+ vcard: vcard(message.contacts[0]),
333
+ };
334
+ } else {
335
+ content.contactsArrayMessage = {
336
+ displayName: `${message.length} contacts`,
337
+ contacts: message.map((contact) => {
338
+ return {
339
+ displayName: contact.name.formatted_name,
340
+ vcard: vcard(contact),
341
+ };
342
+ }),
343
+ };
344
+ }
345
+ message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
346
+ return content;
347
+ }
348
+
349
+ private renderMessageType(type: string) {
350
+ let messageType: string;
351
+
352
+ switch (type) {
353
+ case 'text':
354
+ messageType = 'conversation';
355
+ break;
356
+ case 'image':
357
+ messageType = 'imageMessage';
358
+ break;
359
+ case 'video':
360
+ messageType = 'videoMessage';
361
+ break;
362
+ case 'audio':
363
+ messageType = 'audioMessage';
364
+ break;
365
+ case 'document':
366
+ messageType = 'documentMessage';
367
+ break;
368
+ case 'template':
369
+ messageType = 'conversation';
370
+ break;
371
+ case 'location':
372
+ messageType = 'locationMessage';
373
+ break;
374
+ case 'sticker':
375
+ messageType = 'stickerMessage';
376
+ break;
377
+ default:
378
+ messageType = 'conversation';
379
+ break;
380
+ }
381
+
382
+ return messageType;
383
+ }
384
+
385
+ protected async messageHandle(received: any, database: Database, settings: any) {
386
+ try {
387
+ let messageRaw: any;
388
+ let pushName: any;
389
+
390
+ if (received.contacts) pushName = received.contacts[0].profile.name;
391
+
392
+ if (received.messages) {
393
+ const message = received.messages[0]; // Añadir esta línea para definir message
394
+
395
+ const key = {
396
+ id: message.id,
397
+ remoteJid: this.phoneNumber,
398
+ fromMe: message.from === received.metadata.phone_number_id,
399
+ };
400
+
401
+ if (message.type === 'sticker') {
402
+ this.logger.log('Procesando mensaje de tipo sticker');
403
+ messageRaw = {
404
+ key,
405
+ pushName,
406
+ message: {
407
+ stickerMessage: message.sticker || {},
408
+ },
409
+ messageType: 'stickerMessage',
410
+ messageTimestamp: parseInt(message.timestamp) as number,
411
+ source: 'unknown',
412
+ instanceId: this.instanceId,
413
+ };
414
+ } else if (this.isMediaMessage(message)) {
415
+ const messageContent =
416
+ message.type === 'audio' ? this.messageAudioJson(received) : this.messageMediaJson(received);
417
+
418
+ messageRaw = {
419
+ key,
420
+ pushName,
421
+ message: messageContent,
422
+ contextInfo: messageContent?.contextInfo,
423
+ messageType: this.renderMessageType(received.messages[0].type),
424
+ messageTimestamp: parseInt(received.messages[0].timestamp) as number,
425
+ source: 'unknown',
426
+ instanceId: this.instanceId,
427
+ };
428
+
429
+ if (this.configService.get<S3>('S3').ENABLE) {
430
+ try {
431
+ const message: any = received;
432
+
433
+ // Verificação adicional para garantir que há conteúdo de mídia real
434
+ const hasRealMedia = this.hasValidMediaContent(messageRaw);
435
+
436
+ if (!hasRealMedia) {
437
+ this.logger.warn('Message detected as media but contains no valid media content');
438
+ } else {
439
+ const id = message.messages[0][message.messages[0].type].id;
440
+ let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
441
+ const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
442
+ urlServer = `${urlServer}/${version}/${id}`;
443
+ const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
444
+ const result = await axios.get(urlServer, { headers });
445
+
446
+ const buffer = await axios.get(result.data.url, {
447
+ headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
448
+ responseType: 'arraybuffer',
449
+ });
450
+
451
+ let mediaType;
452
+
453
+ if (message.messages[0].document) {
454
+ mediaType = 'document';
455
+ } else if (message.messages[0].image) {
456
+ mediaType = 'image';
457
+ } else if (message.messages[0].audio) {
458
+ mediaType = 'audio';
459
+ } else {
460
+ mediaType = 'video';
461
+ }
462
+
463
+ if (mediaType == 'video' && !this.configService.get<S3>('S3').SAVE_VIDEO) {
464
+ this.logger?.info?.('Video upload attempted but is disabled by configuration.');
465
+ return {
466
+ success: false,
467
+ message:
468
+ 'Video upload is currently disabled. Please contact support if you need this feature enabled.',
469
+ };
470
+ }
471
+
472
+ const mimetype = result.data?.mime_type || result.headers['content-type'];
473
+
474
+ const contentDisposition = result.headers['content-disposition'];
475
+ let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
476
+ if (contentDisposition) {
477
+ const match = contentDisposition.match(/filename="(.+?)"/);
478
+ if (match) {
479
+ fileName = match[1];
480
+ }
481
+ }
482
+
483
+ // Para áudio, garantir extensão correta baseada no mimetype
484
+ if (mediaType === 'audio') {
485
+ if (mimetype.includes('ogg')) {
486
+ fileName = `${message.messages[0].id}.ogg`;
487
+ } else if (mimetype.includes('mp3')) {
488
+ fileName = `${message.messages[0].id}.mp3`;
489
+ } else if (mimetype.includes('m4a')) {
490
+ fileName = `${message.messages[0].id}.m4a`;
491
+ }
492
+ }
493
+
494
+ const size = result.headers['content-length'] || buffer.data.byteLength;
495
+
496
+ const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
497
+
498
+ await s3Service.uploadFile(fullName, buffer.data, size, {
499
+ 'Content-Type': mimetype,
500
+ });
501
+
502
+ const createdMessage = await this.prismaRepository.message.create({
503
+ data: messageRaw,
504
+ });
505
+
506
+ await this.prismaRepository.media.create({
507
+ data: {
508
+ messageId: createdMessage.id,
509
+ instanceId: this.instanceId,
510
+ type: mediaType,
511
+ fileName: fullName,
512
+ mimetype,
513
+ },
514
+ });
515
+
516
+ const mediaUrl = await s3Service.getObjectUrl(fullName);
517
+
518
+ messageRaw.message.mediaUrl = mediaUrl;
519
+ messageRaw.message.base64 = buffer.data.toString('base64');
520
+
521
+ // Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
522
+ if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
523
+ const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
524
+ where: {
525
+ instanceId: this.instanceId,
526
+ },
527
+ include: {
528
+ OpenaiCreds: true,
529
+ },
530
+ });
531
+
532
+ if (
533
+ openAiDefaultSettings &&
534
+ openAiDefaultSettings.openaiCredsId &&
535
+ openAiDefaultSettings.speechToText
536
+ ) {
537
+ try {
538
+ messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
539
+ openAiDefaultSettings.OpenaiCreds,
540
+ {
541
+ message: {
542
+ mediaUrl: messageRaw.message.mediaUrl,
543
+ ...messageRaw,
544
+ },
545
+ },
546
+ )}`;
547
+ } catch (speechError) {
548
+ this.logger.error(`Error processing speech-to-text: ${speechError}`);
549
+ }
550
+ }
551
+ }
552
+ }
553
+ } catch (error) {
554
+ this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
555
+ }
556
+ } else {
557
+ const buffer = await this.downloadMediaMessage(received?.messages[0]);
558
+ messageRaw.message.base64 = buffer.toString('base64');
559
+
560
+ // Processar OpenAI speech-to-text para áudio mesmo sem S3
561
+ if (this.configService.get<Openai>('OPENAI').ENABLED && message.type === 'audio') {
562
+ const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
563
+ where: {
564
+ instanceId: this.instanceId,
565
+ },
566
+ include: {
567
+ OpenaiCreds: true,
568
+ },
569
+ });
570
+
571
+ if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
572
+ try {
573
+ messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
574
+ openAiDefaultSettings.OpenaiCreds,
575
+ {
576
+ message: {
577
+ base64: messageRaw.message.base64,
578
+ ...messageRaw,
579
+ },
580
+ },
581
+ )}`;
582
+ } catch (speechError) {
583
+ this.logger.error(`Error processing speech-to-text: ${speechError}`);
584
+ }
585
+ }
586
+ }
587
+ }
588
+ } else if (received?.messages[0].interactive) {
589
+ messageRaw = {
590
+ key,
591
+ pushName,
592
+ message: {
593
+ ...this.messageInteractiveJson(received),
594
+ },
595
+ contextInfo: this.messageInteractiveJson(received)?.contextInfo,
596
+ messageType: 'interactiveMessage',
597
+ messageTimestamp: parseInt(received.messages[0].timestamp) as number,
598
+ source: 'unknown',
599
+ instanceId: this.instanceId,
600
+ };
601
+ } else if (received?.messages[0].button) {
602
+ messageRaw = {
603
+ key,
604
+ pushName,
605
+ message: {
606
+ ...this.messageButtonJson(received),
607
+ },
608
+ contextInfo: this.messageButtonJson(received)?.contextInfo,
609
+ messageType: 'buttonMessage',
610
+ messageTimestamp: parseInt(received.messages[0].timestamp) as number,
611
+ source: 'unknown',
612
+ instanceId: this.instanceId,
613
+ };
614
+ } else if (received?.messages[0].reaction) {
615
+ messageRaw = {
616
+ key,
617
+ pushName,
618
+ message: {
619
+ ...this.messageReactionJson(received),
620
+ },
621
+ contextInfo: this.messageReactionJson(received)?.contextInfo,
622
+ messageType: 'reactionMessage',
623
+ messageTimestamp: parseInt(received.messages[0].timestamp) as number,
624
+ source: 'unknown',
625
+ instanceId: this.instanceId,
626
+ };
627
+ } else if (received?.messages[0].contacts) {
628
+ messageRaw = {
629
+ key,
630
+ pushName,
631
+ message: {
632
+ ...this.messageContactsJson(received),
633
+ },
634
+ contextInfo: this.messageContactsJson(received)?.contextInfo,
635
+ messageType: 'contactMessage',
636
+ messageTimestamp: parseInt(received.messages[0].timestamp) as number,
637
+ source: 'unknown',
638
+ instanceId: this.instanceId,
639
+ };
640
+ } else {
641
+ messageRaw = {
642
+ key,
643
+ pushName,
644
+ message: this.messageTextJson(received),
645
+ contextInfo: this.messageTextJson(received)?.contextInfo,
646
+ messageType: this.renderMessageType(received.messages[0].type),
647
+ messageTimestamp: parseInt(received.messages[0].timestamp) as number,
648
+ source: 'unknown',
649
+ instanceId: this.instanceId,
650
+ };
651
+ }
652
+
653
+ if (this.localSettings.readMessages) {
654
+ // await this.client.readMessages([received.key]);
655
+ }
656
+
657
+ this.logger.log(messageRaw);
658
+
659
+ sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
660
+
661
+ this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
662
+
663
+ await chatbotController.emit({
664
+ instance: { instanceName: this.instance.name, instanceId: this.instanceId },
665
+ remoteJid: messageRaw.key.remoteJid,
666
+ msg: messageRaw,
667
+ pushName: messageRaw.pushName,
668
+ });
669
+
670
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
671
+ const chatwootSentMessage = await this.chatwootService.eventWhatsapp(
672
+ Events.MESSAGES_UPSERT,
673
+ { instanceName: this.instance.name, instanceId: this.instanceId },
674
+ messageRaw,
675
+ );
676
+
677
+ if (chatwootSentMessage?.id) {
678
+ messageRaw.chatwootMessageId = chatwootSentMessage.id;
679
+ messageRaw.chatwootInboxId = chatwootSentMessage.id;
680
+ messageRaw.chatwootConversationId = chatwootSentMessage.id;
681
+ }
682
+ }
683
+
684
+ if (!this.isMediaMessage(message) && message.type !== 'sticker') {
685
+ await this.prismaRepository.message.create({
686
+ data: messageRaw,
687
+ });
688
+ }
689
+
690
+ const contact = await this.prismaRepository.contact.findFirst({
691
+ where: { instanceId: this.instanceId, remoteJid: key.remoteJid },
692
+ });
693
+
694
+ const contactRaw: any = {
695
+ remoteJid: received.contacts[0].profile.phone,
696
+ pushName,
697
+ // profilePicUrl: '',
698
+ instanceId: this.instanceId,
699
+ };
700
+
701
+ if (contactRaw.remoteJid === 'status@broadcast') {
702
+ return;
703
+ }
704
+
705
+ if (contact) {
706
+ const contactRaw: any = {
707
+ remoteJid: received.contacts[0].profile.phone,
708
+ pushName,
709
+ // profilePicUrl: '',
710
+ instanceId: this.instanceId,
711
+ };
712
+
713
+ this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
714
+
715
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
716
+ await this.chatwootService.eventWhatsapp(
717
+ Events.CONTACTS_UPDATE,
718
+ { instanceName: this.instance.name, instanceId: this.instanceId },
719
+ contactRaw,
720
+ );
721
+ }
722
+
723
+ await this.prismaRepository.contact.updateMany({
724
+ where: { remoteJid: contact.remoteJid },
725
+ data: contactRaw,
726
+ });
727
+ return;
728
+ }
729
+
730
+ this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
731
+
732
+ this.prismaRepository.contact.create({
733
+ data: contactRaw,
734
+ });
735
+ }
736
+ if (received.statuses) {
737
+ for await (const item of received.statuses) {
738
+ const key = {
739
+ id: item.id,
740
+ remoteJid: this.phoneNumber,
741
+ fromMe: this.phoneNumber === received.metadata.phone_number_id,
742
+ };
743
+ if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) {
744
+ return;
745
+ }
746
+ if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) {
747
+ const findMessage = await this.prismaRepository.message.findFirst({
748
+ where: {
749
+ instanceId: this.instanceId,
750
+ key: {
751
+ path: ['id'],
752
+ equals: key.id,
753
+ },
754
+ },
755
+ });
756
+
757
+ if (!findMessage) {
758
+ return;
759
+ }
760
+
761
+ if (item.message === null && item.status === undefined) {
762
+ this.sendDataWebhook(Events.MESSAGES_DELETE, key);
763
+
764
+ const message: any = {
765
+ messageId: findMessage.id,
766
+ keyId: key.id,
767
+ remoteJid: key.remoteJid,
768
+ fromMe: key.fromMe,
769
+ participant: key?.remoteJid,
770
+ status: 'DELETED',
771
+ instanceId: this.instanceId,
772
+ };
773
+
774
+ await this.prismaRepository.messageUpdate.create({
775
+ data: message,
776
+ });
777
+
778
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
779
+ this.chatwootService.eventWhatsapp(
780
+ Events.MESSAGES_DELETE,
781
+ { instanceName: this.instance.name, instanceId: this.instanceId },
782
+ { key: key },
783
+ );
784
+ }
785
+
786
+ return;
787
+ }
788
+
789
+ const message: any = {
790
+ messageId: findMessage.id,
791
+ keyId: key.id,
792
+ remoteJid: key.remoteJid,
793
+ fromMe: key.fromMe,
794
+ participant: key?.remoteJid,
795
+ status: item.status.toUpperCase(),
796
+ instanceId: this.instanceId,
797
+ };
798
+
799
+ this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
800
+
801
+ await this.prismaRepository.messageUpdate.create({
802
+ data: message,
803
+ });
804
+
805
+ if (findMessage.webhookUrl) {
806
+ await axios.post(findMessage.webhookUrl, message);
807
+ }
808
+ }
809
+ }
810
+ }
811
+ } catch (error) {
812
+ this.logger.error(error);
813
+ }
814
+ }
815
+
816
+ private convertMessageToRaw(message: any, content: any) {
817
+ let convertMessage: any;
818
+
819
+ if (message?.conversation) {
820
+ if (content?.context?.message_id) {
821
+ convertMessage = {
822
+ ...message,
823
+ contextInfo: { stanzaId: content.context.message_id },
824
+ };
825
+ return convertMessage;
826
+ }
827
+ convertMessage = message;
828
+ return convertMessage;
829
+ }
830
+
831
+ if (message?.mediaType === 'image') {
832
+ if (content?.context?.message_id) {
833
+ convertMessage = {
834
+ imageMessage: message,
835
+ contextInfo: { stanzaId: content.context.message_id },
836
+ };
837
+ return convertMessage;
838
+ }
839
+ return {
840
+ imageMessage: message,
841
+ };
842
+ }
843
+
844
+ if (message?.mediaType === 'video') {
845
+ if (content?.context?.message_id) {
846
+ convertMessage = {
847
+ videoMessage: message,
848
+ contextInfo: { stanzaId: content.context.message_id },
849
+ };
850
+ return convertMessage;
851
+ }
852
+ return {
853
+ videoMessage: message,
854
+ };
855
+ }
856
+
857
+ if (message?.mediaType === 'audio') {
858
+ if (content?.context?.message_id) {
859
+ convertMessage = {
860
+ audioMessage: message,
861
+ contextInfo: { stanzaId: content.context.message_id },
862
+ };
863
+ return convertMessage;
864
+ }
865
+ return {
866
+ audioMessage: message,
867
+ };
868
+ }
869
+
870
+ if (message?.mediaType === 'document') {
871
+ if (content?.context?.message_id) {
872
+ convertMessage = {
873
+ documentMessage: message,
874
+ contextInfo: { stanzaId: content.context.message_id },
875
+ };
876
+ return convertMessage;
877
+ }
878
+ return {
879
+ documentMessage: message,
880
+ };
881
+ }
882
+
883
+ return message;
884
+ }
885
+
886
+ protected async eventHandler(content: any) {
887
+ try {
888
+ // Registro para depuración
889
+ this.logger.log('Contenido recibido en eventHandler:');
890
+ this.logger.log(JSON.stringify(content, null, 2));
891
+
892
+ const database = this.configService.get<Database>('DATABASE');
893
+ const settings = await this.findSettings();
894
+
895
+ // Si hay mensajes, verificar primero el tipo
896
+ if (content.messages && content.messages.length > 0) {
897
+ const message = content.messages[0];
898
+ this.logger.log(`Tipo de mensaje recibido: ${message.type}`);
899
+
900
+ // Verificamos el tipo de mensaje antes de procesarlo
901
+ if (
902
+ message.type === 'text' ||
903
+ message.type === 'image' ||
904
+ message.type === 'video' ||
905
+ message.type === 'audio' ||
906
+ message.type === 'document' ||
907
+ message.type === 'sticker' ||
908
+ message.type === 'location' ||
909
+ message.type === 'contacts' ||
910
+ message.type === 'interactive' ||
911
+ message.type === 'button' ||
912
+ message.type === 'reaction'
913
+ ) {
914
+ // Procesar el mensaje normalmente
915
+ this.messageHandle(content, database, settings);
916
+ } else {
917
+ this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
918
+ }
919
+ } else if (content.statuses) {
920
+ // Procesar actualizaciones de estado
921
+ this.messageHandle(content, database, settings);
922
+ } else {
923
+ this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
924
+ }
925
+ } catch (error) {
926
+ this.logger.error('Error en eventHandler:');
927
+ this.logger.error(error);
928
+ }
929
+ }
930
+
931
+ protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
932
+ try {
933
+ let quoted: any;
934
+ let webhookUrl: any;
935
+ if (options?.quoted) {
936
+ const m = options?.quoted;
937
+
938
+ const msg = m?.key;
939
+
940
+ if (!msg) {
941
+ throw 'Message not found';
942
+ }
943
+
944
+ quoted = msg;
945
+ }
946
+ if (options?.webhookUrl) {
947
+ webhookUrl = options.webhookUrl;
948
+ }
949
+
950
+ let content: any;
951
+ const messageSent = await (async () => {
952
+ if (message['reactionMessage']) {
953
+ content = {
954
+ messaging_product: 'whatsapp',
955
+ recipient_type: 'individual',
956
+ type: 'reaction',
957
+ to: number.replace(/\D/g, ''),
958
+ reaction: {
959
+ message_id: message['reactionMessage']['key']['id'],
960
+ emoji: message['reactionMessage']['text'],
961
+ },
962
+ };
963
+ quoted ? (content.context = { message_id: quoted.id }) : content;
964
+ return await this.post(content, 'messages');
965
+ }
966
+ if (message['locationMessage']) {
967
+ content = {
968
+ messaging_product: 'whatsapp',
969
+ recipient_type: 'individual',
970
+ type: 'location',
971
+ to: number.replace(/\D/g, ''),
972
+ location: {
973
+ longitude: message['locationMessage']['degreesLongitude'],
974
+ latitude: message['locationMessage']['degreesLatitude'],
975
+ name: message['locationMessage']['name'],
976
+ address: message['locationMessage']['address'],
977
+ },
978
+ };
979
+ quoted ? (content.context = { message_id: quoted.id }) : content;
980
+ return await this.post(content, 'messages');
981
+ }
982
+ if (message['contacts']) {
983
+ content = {
984
+ messaging_product: 'whatsapp',
985
+ recipient_type: 'individual',
986
+ type: 'contacts',
987
+ to: number.replace(/\D/g, ''),
988
+ contacts: message['contacts'],
989
+ };
990
+ quoted ? (content.context = { message_id: quoted.id }) : content;
991
+ message = message['message'];
992
+ return await this.post(content, 'messages');
993
+ }
994
+ if (message['conversation']) {
995
+ content = {
996
+ messaging_product: 'whatsapp',
997
+ recipient_type: 'individual',
998
+ type: 'text',
999
+ to: number.replace(/\D/g, ''),
1000
+ text: {
1001
+ body: message['conversation'],
1002
+ preview_url: Boolean(options?.linkPreview),
1003
+ },
1004
+ };
1005
+ quoted ? (content.context = { message_id: quoted.id }) : content;
1006
+ return await this.post(content, 'messages');
1007
+ }
1008
+ if (message['media']) {
1009
+ const isImage = message['mimetype']?.startsWith('image/');
1010
+
1011
+ content = {
1012
+ messaging_product: 'whatsapp',
1013
+ recipient_type: 'individual',
1014
+ type: message['mediaType'],
1015
+ to: number.replace(/\D/g, ''),
1016
+ [message['mediaType']]: {
1017
+ [message['type']]: message['id'],
1018
+ ...(message['mediaType'] !== 'audio' &&
1019
+ message['fileName'] &&
1020
+ !isImage && { filename: message['fileName'] }),
1021
+ ...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
1022
+ },
1023
+ };
1024
+ quoted ? (content.context = { message_id: quoted.id }) : content;
1025
+ return await this.post(content, 'messages');
1026
+ }
1027
+ if (message['audio']) {
1028
+ content = {
1029
+ messaging_product: 'whatsapp',
1030
+ recipient_type: 'individual',
1031
+ type: 'audio',
1032
+ to: number.replace(/\D/g, ''),
1033
+ audio: {
1034
+ [message['type']]: message['id'],
1035
+ },
1036
+ };
1037
+ quoted ? (content.context = { message_id: quoted.id }) : content;
1038
+ return await this.post(content, 'messages');
1039
+ }
1040
+ if (message['buttons']) {
1041
+ content = {
1042
+ messaging_product: 'whatsapp',
1043
+ recipient_type: 'individual',
1044
+ to: number.replace(/\D/g, ''),
1045
+ type: 'interactive',
1046
+ interactive: {
1047
+ type: 'button',
1048
+ body: {
1049
+ text: message['text'] || 'Select',
1050
+ },
1051
+ action: {
1052
+ buttons: message['buttons'],
1053
+ },
1054
+ },
1055
+ };
1056
+ quoted ? (content.context = { message_id: quoted.id }) : content;
1057
+ let formattedText = '';
1058
+ for (const item of message['buttons']) {
1059
+ formattedText += `▶️ ${item.reply?.title}\n`;
1060
+ }
1061
+ message = { conversation: `${message['text'] || 'Select'}\n` + formattedText };
1062
+ return await this.post(content, 'messages');
1063
+ }
1064
+ if (message['listMessage']) {
1065
+ content = {
1066
+ messaging_product: 'whatsapp',
1067
+ recipient_type: 'individual',
1068
+ to: number.replace(/\D/g, ''),
1069
+ type: 'interactive',
1070
+ interactive: {
1071
+ type: 'list',
1072
+ header: {
1073
+ type: 'text',
1074
+ text: message['listMessage']['title'],
1075
+ },
1076
+ body: {
1077
+ text: message['listMessage']['description'],
1078
+ },
1079
+ footer: {
1080
+ text: message['listMessage']['footerText'],
1081
+ },
1082
+ action: {
1083
+ button: message['listMessage']['buttonText'],
1084
+ sections: message['listMessage']['sections'],
1085
+ },
1086
+ },
1087
+ };
1088
+ quoted ? (content.context = { message_id: quoted.id }) : content;
1089
+ let formattedText = '';
1090
+ for (const section of message['listMessage']['sections']) {
1091
+ formattedText += `${section?.title}\n`;
1092
+ for (const row of section.rows) {
1093
+ formattedText += `${row?.title}\n`;
1094
+ }
1095
+ }
1096
+ message = { conversation: `${message['listMessage']['title']}\n` + formattedText };
1097
+ return await this.post(content, 'messages');
1098
+ }
1099
+ if (message['template']) {
1100
+ content = {
1101
+ messaging_product: 'whatsapp',
1102
+ recipient_type: 'individual',
1103
+ to: number.replace(/\D/g, ''),
1104
+ type: 'template',
1105
+ template: {
1106
+ name: message['template']['name'],
1107
+ language: {
1108
+ code: message['template']['language'] || 'en_US',
1109
+ },
1110
+ components: message['template']['components'],
1111
+ },
1112
+ };
1113
+ quoted ? (content.context = { message_id: quoted.id }) : content;
1114
+ message = { conversation: `▶️${message['template']['name']}◀️` };
1115
+ return await this.post(content, 'messages');
1116
+ }
1117
+ })();
1118
+
1119
+ if (messageSent?.error_data || messageSent.message) {
1120
+ this.logger.error(messageSent);
1121
+ return messageSent;
1122
+ }
1123
+
1124
+ const messageRaw: any = {
1125
+ key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: createJid(number) },
1126
+ message: this.convertMessageToRaw(message, content),
1127
+ messageType: this.renderMessageType(content.type),
1128
+ messageTimestamp: (messageSent?.messages[0]?.timestamp as number) || Math.round(new Date().getTime() / 1000),
1129
+ instanceId: this.instanceId,
1130
+ webhookUrl,
1131
+ status: status[1],
1132
+ source: 'unknown',
1133
+ };
1134
+
1135
+ this.logger.log(messageRaw);
1136
+
1137
+ this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
1138
+
1139
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) {
1140
+ this.chatwootService.eventWhatsapp(
1141
+ Events.SEND_MESSAGE,
1142
+ { instanceName: this.instance.name, instanceId: this.instanceId },
1143
+ messageRaw,
1144
+ );
1145
+ }
1146
+
1147
+ if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && isIntegration)
1148
+ await chatbotController.emit({
1149
+ instance: { instanceName: this.instance.name, instanceId: this.instanceId },
1150
+ remoteJid: messageRaw.key.remoteJid,
1151
+ msg: messageRaw,
1152
+ pushName: messageRaw.pushName,
1153
+ });
1154
+
1155
+ await this.prismaRepository.message.create({
1156
+ data: messageRaw,
1157
+ });
1158
+
1159
+ return messageRaw;
1160
+ } catch (error) {
1161
+ this.logger.error(error);
1162
+ throw new BadRequestException(error.toString());
1163
+ }
1164
+ }
1165
+
1166
+ // Send Message Controller
1167
+ public async textMessage(data: SendTextDto, isIntegration = false) {
1168
+ const res = await this.sendMessageWithTyping(
1169
+ data.number,
1170
+ {
1171
+ conversation: data.text,
1172
+ },
1173
+ {
1174
+ delay: data?.delay,
1175
+ presence: 'composing',
1176
+ quoted: data?.quoted,
1177
+ linkPreview: data?.linkPreview,
1178
+ mentionsEveryOne: data?.mentionsEveryOne,
1179
+ mentioned: data?.mentioned,
1180
+ },
1181
+ isIntegration,
1182
+ );
1183
+ return res;
1184
+ }
1185
+
1186
+ private async getIdMedia(mediaMessage: any, isFile = false) {
1187
+ try {
1188
+ const formData = new FormData();
1189
+
1190
+ if (isFile === false) {
1191
+ if (isURL(mediaMessage.media)) {
1192
+ const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' });
1193
+ const buffer = Buffer.from(response.data, 'base64');
1194
+ formData.append('file', buffer, {
1195
+ filename: mediaMessage.fileName || 'media',
1196
+ contentType: mediaMessage.mimetype,
1197
+ });
1198
+ } else {
1199
+ const buffer = Buffer.from(mediaMessage.media, 'base64');
1200
+ formData.append('file', buffer, {
1201
+ filename: mediaMessage.fileName || 'media',
1202
+ contentType: mediaMessage.mimetype,
1203
+ });
1204
+ }
1205
+ } else {
1206
+ formData.append('file', mediaMessage.media.buffer, {
1207
+ filename: mediaMessage.media.originalname,
1208
+ contentType: mediaMessage.media.mimetype,
1209
+ });
1210
+ }
1211
+
1212
+ const mimetype = mediaMessage.mimetype || mediaMessage.media.mimetype;
1213
+
1214
+ formData.append('typeFile', mimetype);
1215
+ formData.append('messaging_product', 'whatsapp');
1216
+
1217
+ const token = this.token;
1218
+
1219
+ const headers = { Authorization: `Bearer ${token}` };
1220
+ const url = `${this.configService.get<WaBusiness>('WA_BUSINESS').URL}/${
1221
+ this.configService.get<WaBusiness>('WA_BUSINESS').VERSION
1222
+ }/${this.number}/media`;
1223
+
1224
+ const res = await axios.post(url, formData, { headers });
1225
+ return res.data.id;
1226
+ } catch (error) {
1227
+ this.logger.error(error.response.data);
1228
+ throw new InternalServerErrorException(error?.toString() || error);
1229
+ }
1230
+ }
1231
+
1232
+ protected async prepareMediaMessage(mediaMessage: MediaMessage) {
1233
+ try {
1234
+ if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) {
1235
+ const regex = new RegExp(/.*\/(.+?)\./);
1236
+ const arrayMatch = regex.exec(mediaMessage.media);
1237
+ mediaMessage.fileName = arrayMatch[1];
1238
+ }
1239
+
1240
+ if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) {
1241
+ mediaMessage.fileName = 'image.png';
1242
+ }
1243
+
1244
+ if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) {
1245
+ mediaMessage.fileName = 'video.mp4';
1246
+ }
1247
+
1248
+ let mimetype: string | false;
1249
+
1250
+ const prepareMedia: any = {
1251
+ caption: mediaMessage?.caption,
1252
+ fileName: mediaMessage.fileName,
1253
+ mediaType: mediaMessage.mediatype,
1254
+ media: mediaMessage.media,
1255
+ gifPlayback: false,
1256
+ };
1257
+
1258
+ if (isURL(mediaMessage.media)) {
1259
+ mimetype = mimeTypes.lookup(mediaMessage.media);
1260
+ prepareMedia.id = mediaMessage.media;
1261
+ prepareMedia.type = 'link';
1262
+ } else {
1263
+ mimetype = mimeTypes.lookup(mediaMessage.fileName);
1264
+ const id = await this.getIdMedia(prepareMedia);
1265
+ prepareMedia.id = id;
1266
+ prepareMedia.type = 'id';
1267
+ }
1268
+
1269
+ prepareMedia.mimetype = mimetype;
1270
+
1271
+ return prepareMedia;
1272
+ } catch (error) {
1273
+ this.logger.error(error);
1274
+ throw new InternalServerErrorException(error?.toString() || error);
1275
+ }
1276
+ }
1277
+
1278
+ public async mediaMessage(data: SendMediaDto, file?: any, isIntegration = false) {
1279
+ const mediaData: SendMediaDto = { ...data };
1280
+
1281
+ if (file) mediaData.media = file.buffer.toString('base64');
1282
+
1283
+ const message = await this.prepareMediaMessage(mediaData);
1284
+
1285
+ const mediaSent = await this.sendMessageWithTyping(
1286
+ data.number,
1287
+ { ...message },
1288
+ {
1289
+ delay: data?.delay,
1290
+ presence: 'composing',
1291
+ quoted: data?.quoted,
1292
+ linkPreview: data?.linkPreview,
1293
+ mentionsEveryOne: data?.mentionsEveryOne,
1294
+ mentioned: data?.mentioned,
1295
+ },
1296
+ isIntegration,
1297
+ );
1298
+
1299
+ return mediaSent;
1300
+ }
1301
+
1302
+ public async processAudio(audio: string, number: string, file: any) {
1303
+ number = number.replace(/\D/g, '');
1304
+ const hash = `${number}-${new Date().getTime()}`;
1305
+
1306
+ const audioConverterConfig = this.configService.get<AudioConverter>('AUDIO_CONVERTER');
1307
+ if (audioConverterConfig.API_URL) {
1308
+ this.logger.verbose('Using audio converter API');
1309
+ const formData = new FormData();
1310
+
1311
+ if (file) {
1312
+ formData.append('file', file.buffer, {
1313
+ filename: file.originalname,
1314
+ contentType: file.mimetype,
1315
+ });
1316
+ } else if (isURL(audio)) {
1317
+ formData.append('url', audio);
1318
+ } else {
1319
+ formData.append('base64', audio);
1320
+ }
1321
+
1322
+ formData.append('format', 'mp3');
1323
+
1324
+ const response = await axios.post(audioConverterConfig.API_URL, formData, {
1325
+ headers: {
1326
+ ...formData.getHeaders(),
1327
+ apikey: audioConverterConfig.API_KEY,
1328
+ },
1329
+ });
1330
+
1331
+ const audioConverter = response?.data?.audio || response?.data?.url;
1332
+
1333
+ if (!audioConverter) {
1334
+ throw new InternalServerErrorException('Failed to convert audio');
1335
+ }
1336
+
1337
+ const prepareMedia: any = {
1338
+ fileName: `${hash}.mp3`,
1339
+ mediaType: 'audio',
1340
+ media: audioConverter,
1341
+ mimetype: 'audio/mpeg',
1342
+ };
1343
+
1344
+ const id = await this.getIdMedia(prepareMedia);
1345
+ prepareMedia.id = id;
1346
+ prepareMedia.type = 'id';
1347
+
1348
+ this.logger.verbose('Audio converted');
1349
+ return prepareMedia;
1350
+ } else {
1351
+ let mimetype: string | false;
1352
+
1353
+ const prepareMedia: any = {
1354
+ fileName: `${hash}.mp3`,
1355
+ mediaType: 'audio',
1356
+ media: audio,
1357
+ };
1358
+
1359
+ if (isURL(audio)) {
1360
+ mimetype = mimeTypes.lookup(audio);
1361
+ prepareMedia.id = audio;
1362
+ prepareMedia.type = 'link';
1363
+ } else if (audio && !file) {
1364
+ mimetype = mimeTypes.lookup(prepareMedia.fileName);
1365
+ const id = await this.getIdMedia(prepareMedia);
1366
+ prepareMedia.id = id;
1367
+ prepareMedia.type = 'id';
1368
+ } else if (file) {
1369
+ prepareMedia.media = file;
1370
+ const id = await this.getIdMedia(prepareMedia, true);
1371
+ prepareMedia.id = id;
1372
+ prepareMedia.type = 'id';
1373
+ mimetype = file.mimetype;
1374
+ }
1375
+
1376
+ prepareMedia.mimetype = mimetype;
1377
+
1378
+ return prepareMedia;
1379
+ }
1380
+ }
1381
+
1382
+ public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
1383
+ const message = await this.processAudio(data.audio, data.number, file);
1384
+
1385
+ const audioSent = await this.sendMessageWithTyping(
1386
+ data.number,
1387
+ { ...message },
1388
+ {
1389
+ delay: data?.delay,
1390
+ presence: 'composing',
1391
+ quoted: data?.quoted,
1392
+ linkPreview: data?.linkPreview,
1393
+ mentionsEveryOne: data?.mentionsEveryOne,
1394
+ mentioned: data?.mentioned,
1395
+ },
1396
+ isIntegration,
1397
+ );
1398
+
1399
+ return audioSent;
1400
+ }
1401
+
1402
+ public async buttonMessage(data: SendButtonsDto) {
1403
+ const embeddedMedia: any = {};
1404
+
1405
+ const btnItems = {
1406
+ text: data.buttons.map((btn) => btn.displayText),
1407
+ ids: data.buttons.map((btn) => btn.id),
1408
+ };
1409
+
1410
+ if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) {
1411
+ throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.');
1412
+ }
1413
+
1414
+ return await this.sendMessageWithTyping(
1415
+ data.number,
1416
+ {
1417
+ text: !embeddedMedia?.mediaKey ? data.title : undefined,
1418
+ buttons: data.buttons.map((button) => {
1419
+ return {
1420
+ type: 'reply',
1421
+ reply: {
1422
+ title: button.displayText,
1423
+ id: button.id,
1424
+ },
1425
+ };
1426
+ }),
1427
+ [embeddedMedia?.mediaKey]: embeddedMedia?.message,
1428
+ },
1429
+ {
1430
+ delay: data?.delay,
1431
+ presence: 'composing',
1432
+ quoted: data?.quoted,
1433
+ linkPreview: data?.linkPreview,
1434
+ mentionsEveryOne: data?.mentionsEveryOne,
1435
+ mentioned: data?.mentioned,
1436
+ },
1437
+ );
1438
+ }
1439
+
1440
+ public async locationMessage(data: SendLocationDto) {
1441
+ return await this.sendMessageWithTyping(
1442
+ data.number,
1443
+ {
1444
+ locationMessage: {
1445
+ degreesLatitude: data.latitude,
1446
+ degreesLongitude: data.longitude,
1447
+ name: data?.name,
1448
+ address: data?.address,
1449
+ },
1450
+ },
1451
+ {
1452
+ delay: data?.delay,
1453
+ presence: 'composing',
1454
+ quoted: data?.quoted,
1455
+ linkPreview: data?.linkPreview,
1456
+ mentionsEveryOne: data?.mentionsEveryOne,
1457
+ mentioned: data?.mentioned,
1458
+ },
1459
+ );
1460
+ }
1461
+
1462
+ public async listMessage(data: SendListDto) {
1463
+ const sectionsItems = {
1464
+ title: data.sections.map((list) => list.title),
1465
+ };
1466
+
1467
+ if (!arrayUnique(sectionsItems.title)) {
1468
+ throw new BadRequestException('Section tiles cannot be repeated');
1469
+ }
1470
+
1471
+ const sendData: any = {
1472
+ listMessage: {
1473
+ title: data.title,
1474
+ description: data.description,
1475
+ footerText: data?.footerText,
1476
+ buttonText: data?.buttonText,
1477
+ sections: data.sections.map((section) => {
1478
+ return {
1479
+ title: section.title,
1480
+ rows: section.rows.map((row) => {
1481
+ return {
1482
+ title: row.title,
1483
+ description: row.description.substring(0, 72),
1484
+ id: row.rowId,
1485
+ };
1486
+ }),
1487
+ };
1488
+ }),
1489
+ },
1490
+ };
1491
+
1492
+ return await this.sendMessageWithTyping(data.number, sendData, {
1493
+ delay: data?.delay,
1494
+ presence: 'composing',
1495
+ quoted: data?.quoted,
1496
+ linkPreview: data?.linkPreview,
1497
+ mentionsEveryOne: data?.mentionsEveryOne,
1498
+ mentioned: data?.mentioned,
1499
+ });
1500
+ }
1501
+
1502
+ public async templateMessage(data: SendTemplateDto, isIntegration = false) {
1503
+ const res = await this.sendMessageWithTyping(
1504
+ data.number,
1505
+ {
1506
+ template: {
1507
+ name: data.name,
1508
+ language: data.language,
1509
+ components: data.components,
1510
+ },
1511
+ },
1512
+ {
1513
+ delay: data?.delay,
1514
+ presence: 'composing',
1515
+ quoted: data?.quoted,
1516
+ linkPreview: data?.linkPreview,
1517
+ mentionsEveryOne: data?.mentionsEveryOne,
1518
+ mentioned: data?.mentioned,
1519
+ webhookUrl: data?.webhookUrl,
1520
+ },
1521
+ isIntegration,
1522
+ );
1523
+ return res;
1524
+ }
1525
+
1526
+ public async contactMessage(data: SendContactDto) {
1527
+ const message: any = {};
1528
+
1529
+ const vcard = (contact: ContactMessage) => {
1530
+ let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`;
1531
+
1532
+ if (contact.organization) {
1533
+ result += `ORG:${contact.organization};\n`;
1534
+ }
1535
+
1536
+ if (contact.email) {
1537
+ result += `EMAIL:${contact.email}\n`;
1538
+ }
1539
+
1540
+ if (contact.url) {
1541
+ result += `URL:${contact.url}\n`;
1542
+ }
1543
+
1544
+ if (!contact.wuid) {
1545
+ contact.wuid = createJid(contact.phoneNumber);
1546
+ }
1547
+
1548
+ result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD';
1549
+
1550
+ return result;
1551
+ };
1552
+
1553
+ if (data.contact.length === 1) {
1554
+ message.contact = {
1555
+ displayName: data.contact[0].fullName,
1556
+ vcard: vcard(data.contact[0]),
1557
+ };
1558
+ } else {
1559
+ message.contactsArrayMessage = {
1560
+ displayName: `${data.contact.length} contacts`,
1561
+ contacts: data.contact.map((contact) => {
1562
+ return {
1563
+ displayName: contact.fullName,
1564
+ vcard: vcard(contact),
1565
+ };
1566
+ }),
1567
+ };
1568
+ }
1569
+ return await this.sendMessageWithTyping(
1570
+ data.number,
1571
+ {
1572
+ contacts: data.contact.map((contact) => {
1573
+ return {
1574
+ name: { formatted_name: contact.fullName, first_name: contact.fullName },
1575
+ phones: [{ phone: contact.phoneNumber }],
1576
+ urls: [{ url: contact.url }],
1577
+ emails: [{ email: contact.email }],
1578
+ org: { company: contact.organization },
1579
+ };
1580
+ }),
1581
+ message,
1582
+ },
1583
+ {
1584
+ delay: data?.delay,
1585
+ presence: 'composing',
1586
+ quoted: data?.quoted,
1587
+ linkPreview: data?.linkPreview,
1588
+ mentionsEveryOne: data?.mentionsEveryOne,
1589
+ mentioned: data?.mentioned,
1590
+ },
1591
+ );
1592
+ }
1593
+
1594
+ public async reactionMessage(data: SendReactionDto) {
1595
+ return await this.sendMessageWithTyping(data.key.remoteJid, {
1596
+ reactionMessage: {
1597
+ key: data.key,
1598
+ text: data.reaction,
1599
+ },
1600
+ });
1601
+ }
1602
+
1603
+ public async getBase64FromMediaMessage(data: any) {
1604
+ try {
1605
+ const msg = data.message;
1606
+ const messageType = msg.messageType.includes('Message') ? msg.messageType : msg.messageType + 'Message';
1607
+ const mediaMessage = msg.message[messageType];
1608
+
1609
+ return {
1610
+ mediaType: msg.messageType,
1611
+ fileName: mediaMessage?.fileName,
1612
+ caption: mediaMessage?.caption,
1613
+ size: {
1614
+ fileLength: mediaMessage?.fileLength,
1615
+ height: mediaMessage?.fileLength,
1616
+ width: mediaMessage?.width,
1617
+ },
1618
+ mimetype: mediaMessage?.mime_type,
1619
+ base64: msg.message.base64,
1620
+ };
1621
+ } catch (error) {
1622
+ this.logger.error(error);
1623
+ throw new BadRequestException(error.toString());
1624
+ }
1625
+ }
1626
+
1627
+ public async deleteMessage() {
1628
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1629
+ }
1630
+
1631
+ // methods not available on WhatsApp Business API
1632
+ public async mediaSticker() {
1633
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1634
+ }
1635
+ public async pollMessage() {
1636
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1637
+ }
1638
+ public async statusMessage() {
1639
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1640
+ }
1641
+ public async reloadConnection() {
1642
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1643
+ }
1644
+ public async whatsappNumber() {
1645
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1646
+ }
1647
+ public async markMessageAsRead() {
1648
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1649
+ }
1650
+ public async archiveChat() {
1651
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1652
+ }
1653
+ public async markChatUnread() {
1654
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1655
+ }
1656
+ public async fetchProfile() {
1657
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1658
+ }
1659
+ public async offerCall() {
1660
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1661
+ }
1662
+ public async sendPresence() {
1663
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1664
+ }
1665
+ public async setPresence() {
1666
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1667
+ }
1668
+ public async fetchPrivacySettings() {
1669
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1670
+ }
1671
+ public async updatePrivacySettings() {
1672
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1673
+ }
1674
+ public async fetchBusinessProfile() {
1675
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1676
+ }
1677
+ public async updateProfileName() {
1678
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1679
+ }
1680
+ public async updateProfileStatus() {
1681
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1682
+ }
1683
+ public async updateProfilePicture() {
1684
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1685
+ }
1686
+ public async removeProfilePicture() {
1687
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1688
+ }
1689
+ public async blockUser() {
1690
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1691
+ }
1692
+ public async updateMessage() {
1693
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1694
+ }
1695
+ public async createGroup() {
1696
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1697
+ }
1698
+ public async updateGroupPicture() {
1699
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1700
+ }
1701
+ public async updateGroupSubject() {
1702
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1703
+ }
1704
+ public async updateGroupDescription() {
1705
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1706
+ }
1707
+ public async findGroup() {
1708
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1709
+ }
1710
+ public async fetchAllGroups() {
1711
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1712
+ }
1713
+ public async inviteCode() {
1714
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1715
+ }
1716
+ public async inviteInfo() {
1717
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1718
+ }
1719
+ public async sendInvite() {
1720
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1721
+ }
1722
+ public async acceptInviteCode() {
1723
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1724
+ }
1725
+ public async revokeInviteCode() {
1726
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1727
+ }
1728
+ public async findParticipants() {
1729
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1730
+ }
1731
+ public async updateGParticipant() {
1732
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1733
+ }
1734
+ public async updateGSetting() {
1735
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1736
+ }
1737
+ public async toggleEphemeral() {
1738
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1739
+ }
1740
+ public async leaveGroup() {
1741
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1742
+ }
1743
+ public async fetchLabels() {
1744
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1745
+ }
1746
+ public async handleLabel() {
1747
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1748
+ }
1749
+ public async receiveMobileCode() {
1750
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1751
+ }
1752
+ public async fakeCall() {
1753
+ throw new BadRequestException('Method not available on WhatsApp Business API');
1754
+ }
1755
+ }
src/api/integrations/channel/whatsapp/baileys.controller.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { WAMonitoringService } from '@api/services/monitor.service';
3
+
4
+ export class BaileysController {
5
+ constructor(private readonly waMonitor: WAMonitoringService) {}
6
+
7
+ public async onWhatsapp({ instanceName }: InstanceDto, body: any) {
8
+ const instance = this.waMonitor.waInstances[instanceName];
9
+
10
+ return instance.baileysOnWhatsapp(body?.jid);
11
+ }
12
+
13
+ public async profilePictureUrl({ instanceName }: InstanceDto, body: any) {
14
+ const instance = this.waMonitor.waInstances[instanceName];
15
+
16
+ return instance.baileysProfilePictureUrl(body?.jid, body?.type, body?.timeoutMs);
17
+ }
18
+
19
+ public async assertSessions({ instanceName }: InstanceDto, body: any) {
20
+ const instance = this.waMonitor.waInstances[instanceName];
21
+
22
+ return instance.baileysAssertSessions(body?.jids, body?.force);
23
+ }
24
+
25
+ public async createParticipantNodes({ instanceName }: InstanceDto, body: any) {
26
+ const instance = this.waMonitor.waInstances[instanceName];
27
+
28
+ return instance.baileysCreateParticipantNodes(body?.jids, body?.message, body?.extraAttrs);
29
+ }
30
+
31
+ public async getUSyncDevices({ instanceName }: InstanceDto, body: any) {
32
+ const instance = this.waMonitor.waInstances[instanceName];
33
+
34
+ return instance.baileysGetUSyncDevices(body?.jids, body?.useCache, body?.ignoreZeroDevices);
35
+ }
36
+
37
+ public async generateMessageTag({ instanceName }: InstanceDto) {
38
+ const instance = this.waMonitor.waInstances[instanceName];
39
+
40
+ return instance.baileysGenerateMessageTag();
41
+ }
42
+
43
+ public async sendNode({ instanceName }: InstanceDto, body: any) {
44
+ const instance = this.waMonitor.waInstances[instanceName];
45
+
46
+ return instance.baileysSendNode(body?.stanza);
47
+ }
48
+
49
+ public async signalRepositoryDecryptMessage({ instanceName }: InstanceDto, body: any) {
50
+ const instance = this.waMonitor.waInstances[instanceName];
51
+
52
+ return instance.baileysSignalRepositoryDecryptMessage(body?.jid, body?.type, body?.ciphertext);
53
+ }
54
+
55
+ public async getAuthState({ instanceName }: InstanceDto) {
56
+ const instance = this.waMonitor.waInstances[instanceName];
57
+
58
+ return instance.baileysGetAuthState();
59
+ }
60
+ }
src/api/integrations/channel/whatsapp/baileys.router.ts ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RouterBroker } from '@api/abstract/abstract.router';
2
+ import { InstanceDto } from '@api/dto/instance.dto';
3
+ import { HttpStatus } from '@api/routes/index.router';
4
+ import { baileysController } from '@api/server.module';
5
+ import { instanceSchema } from '@validate/instance.schema';
6
+ import { RequestHandler, Router } from 'express';
7
+
8
+ export class BaileysRouter extends RouterBroker {
9
+ constructor(...guards: RequestHandler[]) {
10
+ super();
11
+ this.router
12
+ .post(this.routerPath('onWhatsapp'), ...guards, async (req, res) => {
13
+ const response = await this.dataValidate<InstanceDto>({
14
+ request: req,
15
+ schema: instanceSchema,
16
+ ClassRef: InstanceDto,
17
+ execute: (instance) => baileysController.onWhatsapp(instance, req.body),
18
+ });
19
+
20
+ res.status(HttpStatus.OK).json(response);
21
+ })
22
+ .post(this.routerPath('profilePictureUrl'), ...guards, async (req, res) => {
23
+ const response = await this.dataValidate<InstanceDto>({
24
+ request: req,
25
+ schema: instanceSchema,
26
+ ClassRef: InstanceDto,
27
+ execute: (instance) => baileysController.profilePictureUrl(instance, req.body),
28
+ });
29
+
30
+ res.status(HttpStatus.OK).json(response);
31
+ })
32
+ .post(this.routerPath('assertSessions'), ...guards, async (req, res) => {
33
+ const response = await this.dataValidate<InstanceDto>({
34
+ request: req,
35
+ schema: instanceSchema,
36
+ ClassRef: InstanceDto,
37
+ execute: (instance) => baileysController.assertSessions(instance, req.body),
38
+ });
39
+
40
+ res.status(HttpStatus.OK).json(response);
41
+ })
42
+ .post(this.routerPath('createParticipantNodes'), ...guards, async (req, res) => {
43
+ const response = await this.dataValidate<InstanceDto>({
44
+ request: req,
45
+ schema: instanceSchema,
46
+ ClassRef: InstanceDto,
47
+ execute: (instance) => baileysController.createParticipantNodes(instance, req.body),
48
+ });
49
+
50
+ res.status(HttpStatus.OK).json(response);
51
+ })
52
+ .post(this.routerPath('getUSyncDevices'), ...guards, async (req, res) => {
53
+ const response = await this.dataValidate<InstanceDto>({
54
+ request: req,
55
+ schema: instanceSchema,
56
+ ClassRef: InstanceDto,
57
+ execute: (instance) => baileysController.getUSyncDevices(instance, req.body),
58
+ });
59
+
60
+ res.status(HttpStatus.OK).json(response);
61
+ })
62
+ .post(this.routerPath('generateMessageTag'), ...guards, async (req, res) => {
63
+ const response = await this.dataValidate<InstanceDto>({
64
+ request: req,
65
+ schema: instanceSchema,
66
+ ClassRef: InstanceDto,
67
+ execute: (instance) => baileysController.generateMessageTag(instance),
68
+ });
69
+
70
+ res.status(HttpStatus.OK).json(response);
71
+ })
72
+ .post(this.routerPath('sendNode'), ...guards, async (req, res) => {
73
+ const response = await this.dataValidate<InstanceDto>({
74
+ request: req,
75
+ schema: instanceSchema,
76
+ ClassRef: InstanceDto,
77
+ execute: (instance) => baileysController.sendNode(instance, req.body),
78
+ });
79
+
80
+ res.status(HttpStatus.OK).json(response);
81
+ })
82
+ .post(this.routerPath('signalRepositoryDecryptMessage'), ...guards, async (req, res) => {
83
+ const response = await this.dataValidate<InstanceDto>({
84
+ request: req,
85
+ schema: instanceSchema,
86
+ ClassRef: InstanceDto,
87
+ execute: (instance) => baileysController.signalRepositoryDecryptMessage(instance, req.body),
88
+ });
89
+
90
+ res.status(HttpStatus.OK).json(response);
91
+ })
92
+ .post(this.routerPath('getAuthState'), ...guards, async (req, res) => {
93
+ const response = await this.dataValidate<InstanceDto>({
94
+ request: req,
95
+ schema: instanceSchema,
96
+ ClassRef: InstanceDto,
97
+ execute: (instance) => baileysController.getAuthState(instance),
98
+ });
99
+
100
+ res.status(HttpStatus.OK).json(response);
101
+ });
102
+ }
103
+
104
+ public readonly router: Router = Router();
105
+ }
src/api/integrations/channel/whatsapp/baileysMessage.processor.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Logger } from '@config/logger.config';
2
+ import { BaileysEventMap, MessageUpsertType, WAMessage } from 'baileys';
3
+ import { catchError, concatMap, delay, EMPTY, from, retryWhen, Subject, Subscription, take, tap } from 'rxjs';
4
+
5
+ type MessageUpsertPayload = BaileysEventMap['messages.upsert'];
6
+ type MountProps = {
7
+ onMessageReceive: (payload: MessageUpsertPayload, settings: any) => Promise<void>;
8
+ };
9
+
10
+ export class BaileysMessageProcessor {
11
+ private processorLogs = new Logger('BaileysMessageProcessor');
12
+ private subscription?: Subscription;
13
+
14
+ protected messageSubject = new Subject<{
15
+ messages: WAMessage[];
16
+ type: MessageUpsertType;
17
+ requestId?: string;
18
+ settings: any;
19
+ }>();
20
+
21
+ mount({ onMessageReceive }: MountProps) {
22
+ this.subscription = this.messageSubject
23
+ .pipe(
24
+ tap(({ messages }) => {
25
+ this.processorLogs.log(`Processing batch of ${messages.length} messages`);
26
+ }),
27
+ concatMap(({ messages, type, requestId, settings }) =>
28
+ from(onMessageReceive({ messages, type, requestId }, settings)).pipe(
29
+ retryWhen((errors) =>
30
+ errors.pipe(
31
+ tap((error) => this.processorLogs.warn(`Retrying message batch due to error: ${error.message}`)),
32
+ delay(1000), // 1 segundo de delay
33
+ take(3), // Máximo 3 tentativas
34
+ ),
35
+ ),
36
+ ),
37
+ ),
38
+ catchError((error) => {
39
+ this.processorLogs.error(`Error processing message batch: ${error}`);
40
+ return EMPTY;
41
+ }),
42
+ )
43
+ .subscribe({
44
+ error: (error) => {
45
+ this.processorLogs.error(`Message stream error: ${error}`);
46
+ },
47
+ });
48
+ }
49
+
50
+ processMessage(payload: MessageUpsertPayload, settings: any) {
51
+ const { messages, type, requestId } = payload;
52
+ this.messageSubject.next({ messages, type, requestId, settings });
53
+ }
54
+
55
+ onDestroy() {
56
+ this.subscription?.unsubscribe();
57
+ this.messageSubject.complete();
58
+ }
59
+ }
src/api/integrations/channel/whatsapp/voiceCalls/transport.type.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys';
2
+
3
+ export interface ServerToClientEvents {
4
+ withAck: (d: string, callback: (e: number) => void) => void;
5
+ onWhatsApp: onWhatsAppType;
6
+ profilePictureUrl: ProfilePictureUrlType;
7
+ assertSessions: AssertSessionsType;
8
+ createParticipantNodes: CreateParticipantNodesType;
9
+ getUSyncDevices: GetUSyncDevicesType;
10
+ generateMessageTag: GenerateMessageTagType;
11
+ sendNode: SendNodeType;
12
+ 'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType;
13
+ }
14
+
15
+ export interface ClientToServerEvents {
16
+ init: (
17
+ me: Contact | undefined,
18
+ account: proto.IADVSignedDeviceIdentity | undefined,
19
+ status: WAConnectionState,
20
+ ) => void;
21
+ 'CB:call': (packet: any) => void;
22
+ 'CB:ack,class:call': (packet: any) => void;
23
+ 'connection.update:status': (
24
+ me: Contact | undefined,
25
+ account: proto.IADVSignedDeviceIdentity | undefined,
26
+ status: WAConnectionState,
27
+ ) => void;
28
+ 'connection.update:qr': (qr: string) => void;
29
+ }
30
+
31
+ export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void;
32
+ export type onWhatsAppCallback = (
33
+ response: {
34
+ exists: boolean;
35
+ jid: string;
36
+ }[],
37
+ ) => void;
38
+
39
+ export type ProfilePictureUrlType = (
40
+ jid: string,
41
+ type: 'image' | 'preview',
42
+ timeoutMs: number | undefined,
43
+ callback: ProfilePictureUrlCallback,
44
+ ) => void;
45
+ export type ProfilePictureUrlCallback = (response: string | undefined) => void;
46
+
47
+ export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void;
48
+ export type AssertSessionsCallback = (response: boolean) => void;
49
+
50
+ export type CreateParticipantNodesType = (
51
+ jids: string[],
52
+ message: any,
53
+ extraAttrs: any,
54
+ callback: CreateParticipantNodesCallback,
55
+ ) => void;
56
+ export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void;
57
+
58
+ export type GetUSyncDevicesType = (
59
+ jids: string[],
60
+ useCache: boolean,
61
+ ignoreZeroDevices: boolean,
62
+ callback: GetUSyncDevicesTypeCallback,
63
+ ) => void;
64
+ export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void;
65
+
66
+ export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void;
67
+ export type GenerateMessageTagTypeCallback = (response: string) => void;
68
+
69
+ export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void;
70
+ export type SendNodeTypeCallback = (response: boolean) => void;
71
+
72
+ export type SignalRepositoryDecryptMessageType = (
73
+ jid: string,
74
+ type: 'pkmsg' | 'msg',
75
+ ciphertext: Buffer,
76
+ callback: SignalRepositoryDecryptMessageCallback,
77
+ ) => void;
78
+ export type SignalRepositoryDecryptMessageCallback = (response: any) => void;
src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ConnectionState, WAConnectionState, WASocket } from 'baileys';
2
+ import { io, Socket } from 'socket.io-client';
3
+
4
+ import { ClientToServerEvents, ServerToClientEvents } from './transport.type';
5
+
6
+ let baileys_connection_state: WAConnectionState = 'close';
7
+
8
+ export const useVoiceCallsBaileys = async (
9
+ wavoip_token: string,
10
+ baileys_sock: WASocket,
11
+ status?: WAConnectionState,
12
+ logger?: boolean,
13
+ ) => {
14
+ baileys_connection_state = status ?? 'close';
15
+
16
+ const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('https://devices.wavoip.com/baileys', {
17
+ transports: ['websocket'],
18
+ path: `/${wavoip_token}/websocket`,
19
+ });
20
+
21
+ socket.on('connect', () => {
22
+ if (logger) console.log('[*] - Wavoip connected', socket.id);
23
+
24
+ socket.emit(
25
+ 'init',
26
+ baileys_sock.authState.creds.me,
27
+ baileys_sock.authState.creds.account,
28
+ baileys_connection_state,
29
+ );
30
+ });
31
+
32
+ socket.on('disconnect', () => {
33
+ if (logger) console.log('[*] - Wavoip disconnect');
34
+ });
35
+
36
+ socket.on('connect_error', (error) => {
37
+ if (socket.active) {
38
+ if (logger)
39
+ console.log(
40
+ '[*] - Wavoip connection error temporary failure, the socket will automatically try to reconnect',
41
+ error,
42
+ );
43
+ } else {
44
+ if (logger) console.log('[*] - Wavoip connection error', error.message);
45
+ }
46
+ });
47
+
48
+ socket.on('onWhatsApp', async (jid, callback) => {
49
+ try {
50
+ const response: any = await baileys_sock.onWhatsApp(jid);
51
+
52
+ callback(response);
53
+
54
+ if (logger) console.log('[*] Success on call onWhatsApp function', response, jid);
55
+ } catch (error) {
56
+ if (logger) console.error('[*] Error on call onWhatsApp function', error);
57
+ }
58
+ });
59
+
60
+ socket.on('profilePictureUrl', async (jid, type, timeoutMs, callback) => {
61
+ try {
62
+ const response = await baileys_sock.profilePictureUrl(jid, type, timeoutMs);
63
+
64
+ callback(response);
65
+
66
+ if (logger) console.log('[*] Success on call profilePictureUrl function', response);
67
+ } catch (error) {
68
+ if (logger) console.error('[*] Error on call profilePictureUrl function', error);
69
+ }
70
+ });
71
+
72
+ socket.on('assertSessions', async (jids, force, callback) => {
73
+ try {
74
+ const response = await baileys_sock.assertSessions(jids);
75
+
76
+ callback(response);
77
+
78
+ if (logger) console.log('[*] Success on call assertSessions function', response);
79
+ } catch (error) {
80
+ if (logger) console.error('[*] Error on call assertSessions function', error);
81
+ }
82
+ });
83
+
84
+ socket.on('createParticipantNodes', async (jids, message, extraAttrs, callback) => {
85
+ try {
86
+ const response = await baileys_sock.createParticipantNodes(jids, message, extraAttrs);
87
+
88
+ callback(response, true);
89
+
90
+ if (logger) console.log('[*] Success on call createParticipantNodes function', response);
91
+ } catch (error) {
92
+ if (logger) console.error('[*] Error on call createParticipantNodes function', error);
93
+ }
94
+ });
95
+
96
+ socket.on('getUSyncDevices', async (jids, useCache, ignoreZeroDevices, callback) => {
97
+ try {
98
+ const response = await baileys_sock.getUSyncDevices(jids, useCache, ignoreZeroDevices);
99
+
100
+ callback(response);
101
+
102
+ if (logger) console.log('[*] Success on call getUSyncDevices function', response);
103
+ } catch (error) {
104
+ if (logger) console.error('[*] Error on call getUSyncDevices function', error);
105
+ }
106
+ });
107
+
108
+ socket.on('generateMessageTag', async (callback) => {
109
+ try {
110
+ const response = await baileys_sock.generateMessageTag();
111
+
112
+ callback(response);
113
+
114
+ if (logger) console.log('[*] Success on call generateMessageTag function', response);
115
+ } catch (error) {
116
+ if (logger) console.error('[*] Error on call generateMessageTag function', error);
117
+ }
118
+ });
119
+
120
+ socket.on('sendNode', async (stanza, callback) => {
121
+ try {
122
+ console.log('sendNode', JSON.stringify(stanza));
123
+ const response = await baileys_sock.sendNode(stanza);
124
+
125
+ callback(true);
126
+
127
+ if (logger) console.log('[*] Success on call sendNode function', response);
128
+ } catch (error) {
129
+ if (logger) console.error('[*] Error on call sendNode function', error);
130
+ }
131
+ });
132
+
133
+ socket.on('signalRepository:decryptMessage', async (jid, type, ciphertext, callback) => {
134
+ try {
135
+ const response = await baileys_sock.signalRepository.decryptMessage({
136
+ jid: jid,
137
+ type: type,
138
+ ciphertext: ciphertext,
139
+ });
140
+
141
+ callback(response);
142
+
143
+ if (logger) console.log('[*] Success on call signalRepository:decryptMessage function', response);
144
+ } catch (error) {
145
+ if (logger) console.error('[*] Error on call signalRepository:decryptMessage function', error);
146
+ }
147
+ });
148
+
149
+ // we only use this connection data to inform the webphone that the device is connected and creeds account to generate e2e whatsapp key for make call packets
150
+ baileys_sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
151
+ const { connection } = update;
152
+
153
+ if (connection) {
154
+ baileys_connection_state = connection;
155
+ socket
156
+ .timeout(1000)
157
+ .emit(
158
+ 'connection.update:status',
159
+ baileys_sock.authState.creds.me,
160
+ baileys_sock.authState.creds.account,
161
+ connection,
162
+ );
163
+ }
164
+
165
+ if (update.qr) {
166
+ socket.timeout(1000).emit('connection.update:qr', update.qr);
167
+ }
168
+ });
169
+
170
+ baileys_sock.ws.on('CB:call', (packet) => {
171
+ if (logger) console.log('[*] Signling received');
172
+ socket.volatile.timeout(1000).emit('CB:call', packet);
173
+ });
174
+
175
+ baileys_sock.ws.on('CB:ack,class:call', (packet) => {
176
+ if (logger) console.log('[*] Signling ack received');
177
+ socket.volatile.timeout(1000).emit('CB:ack,class:call', packet);
178
+ });
179
+
180
+ return socket;
181
+ };
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts ADDED
The diff for this file is too large to render. See raw diff
 
src/api/integrations/chatbot/base-chatbot.controller.ts ADDED
@@ -0,0 +1,950 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IgnoreJidDto } from '@api/dto/chatbot.dto';
2
+ import { InstanceDto } from '@api/dto/instance.dto';
3
+ import { PrismaRepository } from '@api/repository/repository.service';
4
+ import { WAMonitoringService } from '@api/services/monitor.service';
5
+ import { Events } from '@api/types/wa.types';
6
+ import { Logger } from '@config/logger.config';
7
+ import { BadRequestException } from '@exceptions';
8
+ import { TriggerOperator, TriggerType } from '@prisma/client';
9
+ import { getConversationMessage } from '@utils/getConversationMessage';
10
+
11
+ import { BaseChatbotDto } from './base-chatbot.dto';
12
+ import { ChatbotController, ChatbotControllerInterface, EmitData } from './chatbot.controller';
13
+
14
+ // Common settings interface for all chatbot integrations
15
+ export interface ChatbotSettings {
16
+ expire: number;
17
+ keywordFinish: string;
18
+ delayMessage: number;
19
+ unknownMessage: string;
20
+ listeningFromMe: boolean;
21
+ stopBotFromMe: boolean;
22
+ keepOpen: boolean;
23
+ debounceTime: number;
24
+ ignoreJids: string[];
25
+ splitMessages: boolean;
26
+ timePerChar: number;
27
+ [key: string]: any;
28
+ }
29
+
30
+ // Common bot properties for all chatbot integrations
31
+ export interface BaseBotData {
32
+ enabled?: boolean;
33
+ description: string;
34
+ expire?: number;
35
+ keywordFinish?: string;
36
+ delayMessage?: number;
37
+ unknownMessage?: string;
38
+ listeningFromMe?: boolean;
39
+ stopBotFromMe?: boolean;
40
+ keepOpen?: boolean;
41
+ debounceTime?: number;
42
+ triggerType: string | TriggerType;
43
+ triggerOperator?: string | TriggerOperator;
44
+ triggerValue?: string;
45
+ ignoreJids?: string[];
46
+ splitMessages?: boolean;
47
+ timePerChar?: number;
48
+ [key: string]: any;
49
+ }
50
+
51
+ export abstract class BaseChatbotController<BotType = any, BotData extends BaseChatbotDto = BaseChatbotDto>
52
+ extends ChatbotController
53
+ implements ChatbotControllerInterface
54
+ {
55
+ public readonly logger: Logger;
56
+
57
+ integrationEnabled: boolean;
58
+ botRepository: any;
59
+ settingsRepository: any;
60
+ sessionRepository: any;
61
+ userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
62
+
63
+ // Name of the integration, to be set by the derived class
64
+ protected abstract readonly integrationName: string;
65
+
66
+ // Method to process bot-specific logic
67
+ protected abstract processBot(
68
+ waInstance: any,
69
+ remoteJid: string,
70
+ bot: BotType,
71
+ session: any,
72
+ settings: ChatbotSettings,
73
+ content: string,
74
+ pushName?: string,
75
+ msg?: any,
76
+ ): Promise<void>;
77
+
78
+ // Method to get the fallback bot ID from settings
79
+ protected abstract getFallbackBotId(settings: any): string | undefined;
80
+
81
+ constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
82
+ super(prismaRepository, waMonitor);
83
+
84
+ this.sessionRepository = this.prismaRepository.integrationSession;
85
+ }
86
+
87
+ // Base create bot implementation
88
+ public async createBot(instance: InstanceDto, data: BotData) {
89
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
90
+
91
+ const instanceId = await this.prismaRepository.instance
92
+ .findFirst({
93
+ where: {
94
+ name: instance.instanceName,
95
+ },
96
+ })
97
+ .then((instance) => instance.id);
98
+
99
+ // Set default settings if not provided
100
+ if (
101
+ !data.expire ||
102
+ !data.keywordFinish ||
103
+ !data.delayMessage ||
104
+ !data.unknownMessage ||
105
+ !data.listeningFromMe ||
106
+ !data.stopBotFromMe ||
107
+ !data.keepOpen ||
108
+ !data.debounceTime ||
109
+ !data.ignoreJids ||
110
+ !data.splitMessages ||
111
+ !data.timePerChar
112
+ ) {
113
+ const defaultSettingCheck = await this.settingsRepository.findFirst({
114
+ where: {
115
+ instanceId: instanceId,
116
+ },
117
+ });
118
+
119
+ if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck?.expire;
120
+ if (data.keywordFinish === undefined || data.keywordFinish === null)
121
+ data.keywordFinish = defaultSettingCheck?.keywordFinish;
122
+ if (data.delayMessage === undefined || data.delayMessage === null)
123
+ data.delayMessage = defaultSettingCheck?.delayMessage;
124
+ if (data.unknownMessage === undefined || data.unknownMessage === null)
125
+ data.unknownMessage = defaultSettingCheck?.unknownMessage;
126
+ if (data.listeningFromMe === undefined || data.listeningFromMe === null)
127
+ data.listeningFromMe = defaultSettingCheck?.listeningFromMe;
128
+ if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
129
+ data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe;
130
+ if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck?.keepOpen;
131
+ if (data.debounceTime === undefined || data.debounceTime === null)
132
+ data.debounceTime = defaultSettingCheck?.debounceTime;
133
+ if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck?.ignoreJids;
134
+ if (data.splitMessages === undefined || data.splitMessages === null)
135
+ data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
136
+ if (data.timePerChar === undefined || data.timePerChar === null)
137
+ data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
138
+
139
+ if (!defaultSettingCheck) {
140
+ await this.settings(instance, {
141
+ expire: data.expire,
142
+ keywordFinish: data.keywordFinish,
143
+ delayMessage: data.delayMessage,
144
+ unknownMessage: data.unknownMessage,
145
+ listeningFromMe: data.listeningFromMe,
146
+ stopBotFromMe: data.stopBotFromMe,
147
+ keepOpen: data.keepOpen,
148
+ debounceTime: data.debounceTime,
149
+ ignoreJids: data.ignoreJids,
150
+ splitMessages: data.splitMessages,
151
+ timePerChar: data.timePerChar,
152
+ });
153
+ }
154
+ }
155
+
156
+ const checkTriggerAll = await this.botRepository.findFirst({
157
+ where: {
158
+ enabled: true,
159
+ triggerType: 'all',
160
+ instanceId: instanceId,
161
+ },
162
+ });
163
+
164
+ if (checkTriggerAll && data.triggerType === 'all') {
165
+ throw new Error(
166
+ `You already have a ${this.integrationName} with an "All" trigger, you cannot have more bots while it is active`,
167
+ );
168
+ }
169
+
170
+ // Check for trigger keyword duplicates
171
+ if (data.triggerType === 'keyword') {
172
+ if (!data.triggerOperator || !data.triggerValue) {
173
+ throw new Error('Trigger operator and value are required');
174
+ }
175
+
176
+ const checkDuplicate = await this.botRepository.findFirst({
177
+ where: {
178
+ triggerOperator: data.triggerOperator,
179
+ triggerValue: data.triggerValue,
180
+ instanceId: instanceId,
181
+ },
182
+ });
183
+
184
+ if (checkDuplicate) {
185
+ throw new Error('Trigger already exists');
186
+ }
187
+ }
188
+
189
+ // Check for trigger advanced duplicates
190
+ if (data.triggerType === 'advanced') {
191
+ if (!data.triggerValue) {
192
+ throw new Error('Trigger value is required');
193
+ }
194
+
195
+ const checkDuplicate = await this.botRepository.findFirst({
196
+ where: {
197
+ triggerValue: data.triggerValue,
198
+ instanceId: instanceId,
199
+ },
200
+ });
201
+
202
+ if (checkDuplicate) {
203
+ throw new Error('Trigger already exists');
204
+ }
205
+ }
206
+
207
+ // Derived classes should implement the specific duplicate checking before calling this method
208
+ // and add bot-specific fields to the data object
209
+
210
+ try {
211
+ const botData = {
212
+ enabled: data?.enabled,
213
+ description: data.description,
214
+ expire: data.expire,
215
+ keywordFinish: data.keywordFinish,
216
+ delayMessage: data.delayMessage,
217
+ unknownMessage: data.unknownMessage,
218
+ listeningFromMe: data.listeningFromMe,
219
+ stopBotFromMe: data.stopBotFromMe,
220
+ keepOpen: data.keepOpen,
221
+ debounceTime: data.debounceTime,
222
+ instanceId: instanceId,
223
+ triggerType: data.triggerType,
224
+ triggerOperator: data.triggerOperator,
225
+ triggerValue: data.triggerValue,
226
+ ignoreJids: data.ignoreJids,
227
+ splitMessages: data.splitMessages,
228
+ timePerChar: data.timePerChar,
229
+ ...this.getAdditionalBotData(data),
230
+ };
231
+
232
+ const bot = await this.botRepository.create({
233
+ data: botData,
234
+ });
235
+
236
+ return bot;
237
+ } catch (error) {
238
+ this.logger.error(error);
239
+ throw new Error(`Error creating ${this.integrationName}`);
240
+ }
241
+ }
242
+
243
+ // Additional fields needed for specific bot types
244
+ protected abstract getAdditionalBotData(data: BotData): Record<string, any>;
245
+
246
+ // Common implementation for findBot
247
+ public async findBot(instance: InstanceDto) {
248
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
249
+
250
+ const instanceId = await this.prismaRepository.instance
251
+ .findFirst({
252
+ where: {
253
+ name: instance.instanceName,
254
+ },
255
+ })
256
+ .then((instance) => instance.id);
257
+
258
+ try {
259
+ const bots = await this.botRepository.findMany({
260
+ where: {
261
+ instanceId: instanceId,
262
+ },
263
+ });
264
+
265
+ return bots;
266
+ } catch (error) {
267
+ this.logger.error(error);
268
+ throw new Error(`Error finding ${this.integrationName}`);
269
+ }
270
+ }
271
+
272
+ // Common implementation for fetchBot
273
+ public async fetchBot(instance: InstanceDto, botId: string) {
274
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
275
+
276
+ try {
277
+ const bot = await this.botRepository.findUnique({
278
+ where: {
279
+ id: botId,
280
+ },
281
+ });
282
+
283
+ if (!bot) {
284
+ return null;
285
+ }
286
+
287
+ return bot;
288
+ } catch (error) {
289
+ this.logger.error(error);
290
+ throw new Error(`Error fetching ${this.integrationName}`);
291
+ }
292
+ }
293
+
294
+ // Common implementation for settings
295
+ public async settings(instance: InstanceDto, data: any) {
296
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
297
+
298
+ try {
299
+ const instanceId = await this.prismaRepository.instance
300
+ .findFirst({
301
+ where: {
302
+ name: instance.instanceName,
303
+ },
304
+ })
305
+ .then((instance) => instance.id);
306
+
307
+ const existingSettings = await this.settingsRepository.findFirst({
308
+ where: {
309
+ instanceId: instanceId,
310
+ },
311
+ });
312
+
313
+ // Get the name of the fallback field for this integration type
314
+ const fallbackFieldName = this.getFallbackFieldName();
315
+
316
+ const settingsData = {
317
+ expire: data.expire,
318
+ keywordFinish: data.keywordFinish,
319
+ delayMessage: data.delayMessage,
320
+ unknownMessage: data.unknownMessage,
321
+ listeningFromMe: data.listeningFromMe,
322
+ stopBotFromMe: data.stopBotFromMe,
323
+ keepOpen: data.keepOpen,
324
+ debounceTime: data.debounceTime,
325
+ ignoreJids: data.ignoreJids,
326
+ splitMessages: data.splitMessages,
327
+ timePerChar: data.timePerChar,
328
+ [fallbackFieldName]: data.fallbackId, // Use the correct field name dynamically
329
+ };
330
+
331
+ if (existingSettings) {
332
+ const settings = await this.settingsRepository.update({
333
+ where: {
334
+ id: existingSettings.id,
335
+ },
336
+ data: settingsData,
337
+ });
338
+
339
+ // Map the specific fallback field to a generic 'fallbackId' in the response
340
+ return {
341
+ ...settings,
342
+ fallbackId: settings[fallbackFieldName],
343
+ };
344
+ } else {
345
+ const settings = await this.settingsRepository.create({
346
+ data: {
347
+ ...settingsData,
348
+ Instance: {
349
+ connect: {
350
+ id: instanceId,
351
+ },
352
+ },
353
+ },
354
+ });
355
+
356
+ // Map the specific fallback field to a generic 'fallbackId' in the response
357
+ return {
358
+ ...settings,
359
+ fallbackId: settings[fallbackFieldName],
360
+ };
361
+ }
362
+ } catch (error) {
363
+ this.logger.error(error);
364
+ throw new Error('Error setting default settings');
365
+ }
366
+ }
367
+
368
+ // Abstract method to get the field name for the fallback ID
369
+ protected abstract getFallbackFieldName(): string;
370
+
371
+ // Abstract method to get the integration type (dify, n8n, evoai, etc.)
372
+ protected abstract getIntegrationType(): string;
373
+
374
+ // Common implementation for fetchSettings
375
+ public async fetchSettings(instance: InstanceDto) {
376
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
377
+
378
+ try {
379
+ const instanceId = await this.prismaRepository.instance
380
+ .findFirst({
381
+ where: {
382
+ name: instance.instanceName,
383
+ },
384
+ })
385
+ .then((instance) => instance.id);
386
+
387
+ const settings = await this.settingsRepository.findFirst({
388
+ where: {
389
+ instanceId: instanceId,
390
+ },
391
+ include: {
392
+ Fallback: true,
393
+ },
394
+ });
395
+
396
+ // Get the name of the fallback field for this integration type
397
+ const fallbackFieldName = this.getFallbackFieldName();
398
+
399
+ if (!settings) {
400
+ return {
401
+ expire: 300,
402
+ keywordFinish: 'bye',
403
+ delayMessage: 1000,
404
+ unknownMessage: 'Sorry, I dont understand',
405
+ listeningFromMe: true,
406
+ stopBotFromMe: true,
407
+ keepOpen: false,
408
+ debounceTime: 1,
409
+ ignoreJids: [],
410
+ splitMessages: false,
411
+ timePerChar: 0,
412
+ fallbackId: '',
413
+ fallback: null,
414
+ };
415
+ }
416
+
417
+ // Return with standardized fallbackId field
418
+ return {
419
+ ...settings,
420
+ fallbackId: settings[fallbackFieldName],
421
+ fallback: settings.Fallback,
422
+ };
423
+ } catch (error) {
424
+ this.logger.error(error);
425
+ throw new Error('Error fetching settings');
426
+ }
427
+ }
428
+
429
+ // Common implementation for changeStatus
430
+ public async changeStatus(instance: InstanceDto, data: any) {
431
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
432
+
433
+ try {
434
+ const instanceId = await this.prismaRepository.instance
435
+ .findFirst({
436
+ where: {
437
+ name: instance.instanceName,
438
+ },
439
+ })
440
+ .then((instance) => instance.id);
441
+
442
+ const defaultSettingCheck = await this.settingsRepository.findFirst({
443
+ where: {
444
+ instanceId,
445
+ },
446
+ });
447
+
448
+ const remoteJid = data.remoteJid;
449
+ const status = data.status;
450
+ const session = await this.getSession(remoteJid, instance);
451
+
452
+ if (this.integrationName === 'Typebot') {
453
+ const typebotData = {
454
+ remoteJid: remoteJid,
455
+ status: status,
456
+ session,
457
+ };
458
+ this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData);
459
+ }
460
+
461
+ if (status === 'delete') {
462
+ await this.sessionRepository.deleteMany({
463
+ where: {
464
+ remoteJid: remoteJid,
465
+ botId: { not: null },
466
+ },
467
+ });
468
+
469
+ return { bot: { remoteJid: remoteJid, status: status } };
470
+ }
471
+
472
+ if (status === 'closed') {
473
+ if (defaultSettingCheck?.keepOpen) {
474
+ await this.sessionRepository.updateMany({
475
+ where: {
476
+ remoteJid: remoteJid,
477
+ botId: { not: null },
478
+ },
479
+ data: {
480
+ status: 'closed',
481
+ },
482
+ });
483
+ } else {
484
+ await this.sessionRepository.deleteMany({
485
+ where: {
486
+ remoteJid: remoteJid,
487
+ botId: { not: null },
488
+ },
489
+ });
490
+ }
491
+
492
+ return { bot: { ...instance, bot: { remoteJid: remoteJid, status: status } } };
493
+ } else {
494
+ const session = await this.sessionRepository.updateMany({
495
+ where: {
496
+ instanceId: instanceId,
497
+ remoteJid: remoteJid,
498
+ botId: { not: null },
499
+ },
500
+ data: {
501
+ status: status,
502
+ },
503
+ });
504
+
505
+ const botData = {
506
+ remoteJid: remoteJid,
507
+ status: status,
508
+ session,
509
+ };
510
+
511
+ return { bot: { ...instance, bot: botData } };
512
+ }
513
+ } catch (error) {
514
+ this.logger.error(error);
515
+ throw new Error(`Error changing ${this.integrationName} status`);
516
+ }
517
+ }
518
+
519
+ // Common implementation for fetchSessions
520
+ public async fetchSessions(instance: InstanceDto, botId: string, remoteJid?: string) {
521
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
522
+
523
+ try {
524
+ const instanceId = await this.prismaRepository.instance
525
+ .findFirst({
526
+ where: {
527
+ name: instance.instanceName,
528
+ },
529
+ })
530
+ .then((instance) => instance.id);
531
+
532
+ const bot = await this.botRepository.findFirst({
533
+ where: {
534
+ id: botId,
535
+ },
536
+ });
537
+
538
+ if (bot && bot.instanceId !== instanceId) {
539
+ throw new Error(`${this.integrationName} not found`);
540
+ }
541
+
542
+ // Get the integration type (dify, n8n, evoai, etc.)
543
+ const integrationType = this.getIntegrationType();
544
+
545
+ return await this.sessionRepository.findMany({
546
+ where: {
547
+ instanceId: instanceId,
548
+ remoteJid,
549
+ botId: bot ? botId : { not: null },
550
+ type: integrationType,
551
+ },
552
+ });
553
+ } catch (error) {
554
+ this.logger.error(error);
555
+ throw new Error('Error fetching sessions');
556
+ }
557
+ }
558
+
559
+ // Common implementation for ignoreJid
560
+ public async ignoreJid(instance: InstanceDto, data: IgnoreJidDto) {
561
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
562
+
563
+ try {
564
+ const instanceId = await this.prismaRepository.instance
565
+ .findFirst({
566
+ where: {
567
+ name: instance.instanceName,
568
+ },
569
+ })
570
+ .then((instance) => instance.id);
571
+
572
+ const settings = await this.settingsRepository.findFirst({
573
+ where: {
574
+ instanceId: instanceId,
575
+ },
576
+ });
577
+
578
+ if (!settings) {
579
+ throw new Error('Settings not found');
580
+ }
581
+
582
+ let ignoreJids: any = settings?.ignoreJids || [];
583
+
584
+ if (data.action === 'add') {
585
+ if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids };
586
+
587
+ ignoreJids.push(data.remoteJid);
588
+ } else {
589
+ ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid);
590
+ }
591
+
592
+ const updateSettings = await this.settingsRepository.update({
593
+ where: {
594
+ id: settings.id,
595
+ },
596
+ data: {
597
+ ignoreJids: ignoreJids,
598
+ },
599
+ });
600
+
601
+ return {
602
+ ignoreJids: updateSettings.ignoreJids,
603
+ };
604
+ } catch (error) {
605
+ this.logger.error(error);
606
+ throw new Error('Error setting default settings');
607
+ }
608
+ }
609
+
610
+ // Base implementation for updateBot
611
+ public async updateBot(instance: InstanceDto, botId: string, data: BotData) {
612
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
613
+
614
+ try {
615
+ const instanceId = await this.prismaRepository.instance
616
+ .findFirst({
617
+ where: {
618
+ name: instance.instanceName,
619
+ },
620
+ })
621
+ .then((instance) => instance.id);
622
+
623
+ const bot = await this.botRepository.findFirst({
624
+ where: {
625
+ id: botId,
626
+ },
627
+ });
628
+
629
+ if (!bot) {
630
+ throw new Error(`${this.integrationName} not found`);
631
+ }
632
+
633
+ if (bot.instanceId !== instanceId) {
634
+ throw new Error(`${this.integrationName} not found`);
635
+ }
636
+
637
+ // Check for "all" trigger type conflicts
638
+ if (data.triggerType === 'all') {
639
+ const checkTriggerAll = await this.botRepository.findFirst({
640
+ where: {
641
+ enabled: true,
642
+ triggerType: 'all',
643
+ id: {
644
+ not: botId,
645
+ },
646
+ instanceId: instanceId,
647
+ },
648
+ });
649
+
650
+ if (checkTriggerAll) {
651
+ throw new Error(
652
+ `You already have a ${this.integrationName} with an "All" trigger, you cannot have more bots while it is active`,
653
+ );
654
+ }
655
+ }
656
+
657
+ // Let subclasses check for integration-specific duplicates
658
+ await this.validateNoDuplicatesOnUpdate(botId, instanceId, data);
659
+
660
+ // Check for keyword trigger duplicates
661
+ if (data.triggerType === 'keyword') {
662
+ if (!data.triggerOperator || !data.triggerValue) {
663
+ throw new Error('Trigger operator and value are required');
664
+ }
665
+
666
+ const checkDuplicate = await this.botRepository.findFirst({
667
+ where: {
668
+ triggerOperator: data.triggerOperator,
669
+ triggerValue: data.triggerValue,
670
+ id: { not: botId },
671
+ instanceId: instanceId,
672
+ },
673
+ });
674
+
675
+ if (checkDuplicate) {
676
+ throw new Error('Trigger already exists');
677
+ }
678
+ }
679
+
680
+ // Check for advanced trigger duplicates
681
+ if (data.triggerType === 'advanced') {
682
+ if (!data.triggerValue) {
683
+ throw new Error('Trigger value is required');
684
+ }
685
+
686
+ const checkDuplicate = await this.botRepository.findFirst({
687
+ where: {
688
+ triggerValue: data.triggerValue,
689
+ id: { not: botId },
690
+ instanceId: instanceId,
691
+ },
692
+ });
693
+
694
+ if (checkDuplicate) {
695
+ throw new Error('Trigger already exists');
696
+ }
697
+ }
698
+
699
+ // Combine common fields with bot-specific fields
700
+ const updateData = {
701
+ enabled: data?.enabled,
702
+ description: data.description,
703
+ expire: data.expire,
704
+ keywordFinish: data.keywordFinish,
705
+ delayMessage: data.delayMessage,
706
+ unknownMessage: data.unknownMessage,
707
+ listeningFromMe: data.listeningFromMe,
708
+ stopBotFromMe: data.stopBotFromMe,
709
+ keepOpen: data.keepOpen,
710
+ debounceTime: data.debounceTime,
711
+ instanceId: instanceId,
712
+ triggerType: data.triggerType,
713
+ triggerOperator: data.triggerOperator,
714
+ triggerValue: data.triggerValue,
715
+ ignoreJids: data.ignoreJids,
716
+ splitMessages: data.splitMessages,
717
+ timePerChar: data.timePerChar,
718
+ ...this.getAdditionalUpdateFields(data),
719
+ };
720
+
721
+ const updatedBot = await this.botRepository.update({
722
+ where: {
723
+ id: botId,
724
+ },
725
+ data: updateData,
726
+ });
727
+
728
+ return updatedBot;
729
+ } catch (error) {
730
+ this.logger.error(error);
731
+ throw new Error(`Error updating ${this.integrationName}`);
732
+ }
733
+ }
734
+
735
+ // Abstract method for validating bot-specific duplicates on update
736
+ protected abstract validateNoDuplicatesOnUpdate(botId: string, instanceId: string, data: BotData): Promise<void>;
737
+
738
+ // Abstract method for getting additional fields for update
739
+ protected abstract getAdditionalUpdateFields(data: BotData): Record<string, any>;
740
+
741
+ // Base implementation for deleteBot
742
+ public async deleteBot(instance: InstanceDto, botId: string) {
743
+ if (!this.integrationEnabled) throw new BadRequestException(`${this.integrationName} is disabled`);
744
+
745
+ try {
746
+ const instanceId = await this.prismaRepository.instance
747
+ .findFirst({
748
+ where: {
749
+ name: instance.instanceName,
750
+ },
751
+ })
752
+ .then((instance) => instance.id);
753
+
754
+ const bot = await this.botRepository.findFirst({
755
+ where: {
756
+ id: botId,
757
+ },
758
+ });
759
+
760
+ if (!bot) {
761
+ throw new Error(`${this.integrationName} not found`);
762
+ }
763
+
764
+ if (bot.instanceId !== instanceId) {
765
+ throw new Error(`${this.integrationName} not found`);
766
+ }
767
+
768
+ await this.prismaRepository.integrationSession.deleteMany({
769
+ where: {
770
+ botId: botId,
771
+ },
772
+ });
773
+
774
+ await this.botRepository.delete({
775
+ where: {
776
+ id: botId,
777
+ },
778
+ });
779
+
780
+ return { bot: { id: botId } };
781
+ } catch (error) {
782
+ this.logger.error(error);
783
+ throw new Error(`Error deleting ${this.integrationName} bot`);
784
+ }
785
+ }
786
+
787
+ // Base implementation for emit
788
+ public async emit({ instance, remoteJid, msg }: EmitData) {
789
+ if (!this.integrationEnabled) return;
790
+
791
+ try {
792
+ const settings = await this.settingsRepository.findFirst({
793
+ where: {
794
+ instanceId: instance.instanceId,
795
+ },
796
+ });
797
+
798
+ if (this.checkIgnoreJids(settings?.ignoreJids, remoteJid)) return;
799
+
800
+ const session = await this.getSession(remoteJid, instance);
801
+
802
+ const content = getConversationMessage(msg);
803
+
804
+ // Get integration type
805
+ // const integrationType = this.getIntegrationType();
806
+
807
+ // Find a bot for this message
808
+ let findBot: any = await this.findBotTrigger(this.botRepository, content, instance, session);
809
+
810
+ // If no bot is found, try to use fallback
811
+ if (!findBot) {
812
+ const fallback = await this.settingsRepository.findFirst({
813
+ where: {
814
+ instanceId: instance.instanceId,
815
+ },
816
+ });
817
+
818
+ // Get the fallback ID for this integration type
819
+ const fallbackId = this.getFallbackBotId(fallback);
820
+
821
+ if (fallbackId) {
822
+ const findFallback = await this.botRepository.findFirst({
823
+ where: {
824
+ id: fallbackId,
825
+ },
826
+ });
827
+
828
+ findBot = findFallback;
829
+ } else {
830
+ return;
831
+ }
832
+ }
833
+
834
+ // If we still don't have a bot, return
835
+ if (!findBot) {
836
+ return;
837
+ }
838
+
839
+ // Collect settings with fallbacks to default settings
840
+ let expire = findBot.expire;
841
+ let keywordFinish = findBot.keywordFinish;
842
+ let delayMessage = findBot.delayMessage;
843
+ let unknownMessage = findBot.unknownMessage;
844
+ let listeningFromMe = findBot.listeningFromMe;
845
+ let stopBotFromMe = findBot.stopBotFromMe;
846
+ let keepOpen = findBot.keepOpen;
847
+ let debounceTime = findBot.debounceTime;
848
+ let ignoreJids = findBot.ignoreJids;
849
+ let splitMessages = findBot.splitMessages;
850
+ let timePerChar = findBot.timePerChar;
851
+
852
+ if (expire === undefined || expire === null) expire = settings.expire;
853
+ if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
854
+ if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
855
+ if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
856
+ if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
857
+ if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
858
+ if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
859
+ if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
860
+ if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
861
+ if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
862
+ if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;
863
+
864
+ const key = msg.key as {
865
+ id: string;
866
+ remoteJid: string;
867
+ fromMe: boolean;
868
+ participant: string;
869
+ };
870
+
871
+ // Handle stopping the bot if message is from me
872
+ if (stopBotFromMe && key.fromMe && session) {
873
+ await this.prismaRepository.integrationSession.update({
874
+ where: {
875
+ id: session.id,
876
+ },
877
+ data: {
878
+ status: 'paused',
879
+ },
880
+ });
881
+
882
+ if (this.integrationName === 'Typebot') {
883
+ const typebotData = {
884
+ remoteJid: remoteJid,
885
+ status: 'paused',
886
+ session,
887
+ };
888
+ this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_CHANGE_STATUS, typebotData);
889
+ }
890
+
891
+ return;
892
+ }
893
+
894
+ // Skip if not listening to messages from me
895
+ if (!listeningFromMe && key.fromMe) {
896
+ return;
897
+ }
898
+
899
+ // Skip if session exists but not awaiting user input
900
+ if (session && session.status === 'closed') {
901
+ return;
902
+ }
903
+
904
+ // Merged settings
905
+ const mergedSettings = {
906
+ ...settings,
907
+ expire,
908
+ keywordFinish,
909
+ delayMessage,
910
+ unknownMessage,
911
+ listeningFromMe,
912
+ stopBotFromMe,
913
+ keepOpen,
914
+ debounceTime,
915
+ ignoreJids,
916
+ splitMessages,
917
+ timePerChar,
918
+ };
919
+
920
+ // Process with debounce if needed
921
+ if (debounceTime && debounceTime > 0) {
922
+ this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
923
+ await this.processBot(
924
+ this.waMonitor.waInstances[instance.instanceName],
925
+ remoteJid,
926
+ findBot,
927
+ session,
928
+ mergedSettings,
929
+ debouncedContent,
930
+ msg?.pushName,
931
+ msg,
932
+ );
933
+ });
934
+ } else {
935
+ await this.processBot(
936
+ this.waMonitor.waInstances[instance.instanceName],
937
+ remoteJid,
938
+ findBot,
939
+ session,
940
+ mergedSettings,
941
+ content,
942
+ msg?.pushName,
943
+ msg,
944
+ );
945
+ }
946
+ } catch (error) {
947
+ this.logger.error(error);
948
+ }
949
+ }
950
+ }
src/api/integrations/chatbot/base-chatbot.dto.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TriggerOperator, TriggerType } from '@prisma/client';
2
+
3
+ /**
4
+ * Base DTO for all chatbot integrations
5
+ * Contains common properties shared by all chatbot types
6
+ */
7
+ export class BaseChatbotDto {
8
+ enabled?: boolean;
9
+ description: string;
10
+ expire?: number;
11
+ keywordFinish?: string;
12
+ delayMessage?: number;
13
+ unknownMessage?: string;
14
+ listeningFromMe?: boolean;
15
+ stopBotFromMe?: boolean;
16
+ keepOpen?: boolean;
17
+ debounceTime?: number;
18
+ triggerType: TriggerType;
19
+ triggerOperator?: TriggerOperator;
20
+ triggerValue?: string;
21
+ ignoreJids?: string[];
22
+ splitMessages?: boolean;
23
+ timePerChar?: number;
24
+ }
25
+
26
+ /**
27
+ * Base settings DTO for all chatbot integrations
28
+ */
29
+ export class BaseChatbotSettingDto {
30
+ expire?: number;
31
+ keywordFinish?: string;
32
+ delayMessage?: number;
33
+ unknownMessage?: string;
34
+ listeningFromMe?: boolean;
35
+ stopBotFromMe?: boolean;
36
+ keepOpen?: boolean;
37
+ debounceTime?: number;
38
+ ignoreJids?: any;
39
+ splitMessages?: boolean;
40
+ timePerChar?: number;
41
+ fallbackId?: string; // Unified fallback ID field for all integrations
42
+ }
src/api/integrations/chatbot/base-chatbot.service.ts ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InstanceDto } from '@api/dto/instance.dto';
2
+ import { PrismaRepository } from '@api/repository/repository.service';
3
+ import { WAMonitoringService } from '@api/services/monitor.service';
4
+ import { Integration } from '@api/types/wa.types';
5
+ import { ConfigService } from '@config/env.config';
6
+ import { Logger } from '@config/logger.config';
7
+ import { IntegrationSession } from '@prisma/client';
8
+
9
+ /**
10
+ * Base class for all chatbot service implementations
11
+ * Contains common methods shared across different chatbot integrations
12
+ */
13
+ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
14
+ protected readonly logger: Logger;
15
+ protected readonly waMonitor: WAMonitoringService;
16
+ protected readonly prismaRepository: PrismaRepository;
17
+ protected readonly configService?: ConfigService;
18
+
19
+ constructor(
20
+ waMonitor: WAMonitoringService,
21
+ prismaRepository: PrismaRepository,
22
+ loggerName: string,
23
+ configService?: ConfigService,
24
+ ) {
25
+ this.waMonitor = waMonitor;
26
+ this.prismaRepository = prismaRepository;
27
+ this.logger = new Logger(loggerName);
28
+ this.configService = configService;
29
+ }
30
+
31
+ /**
32
+ * Check if a message contains an image
33
+ */
34
+ protected isImageMessage(content: string): boolean {
35
+ return content.includes('imageMessage');
36
+ }
37
+
38
+ /**
39
+ * Check if a message contains audio
40
+ */
41
+ protected isAudioMessage(content: string): boolean {
42
+ return content.includes('audioMessage');
43
+ }
44
+
45
+ /**
46
+ * Check if a string is valid JSON
47
+ */
48
+ protected isJSON(str: string): boolean {
49
+ try {
50
+ JSON.parse(str);
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Determine the media type from a URL based on its extension
59
+ */
60
+ protected getMediaType(url: string): string | null {
61
+ const extension = url.split('.').pop()?.toLowerCase();
62
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
63
+ const audioExtensions = ['mp3', 'wav', 'aac', 'ogg'];
64
+ const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'];
65
+ const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
66
+
67
+ if (imageExtensions.includes(extension || '')) return 'image';
68
+ if (audioExtensions.includes(extension || '')) return 'audio';
69
+ if (videoExtensions.includes(extension || '')) return 'video';
70
+ if (documentExtensions.includes(extension || '')) return 'document';
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Create a new chatbot session
76
+ */
77
+ public async createNewSession(instance: InstanceDto | any, data: any, type: string) {
78
+ try {
79
+ // Extract pushName safely - if data.pushName is an object with a pushName property, use that
80
+ const pushNameValue =
81
+ typeof data.pushName === 'object' && data.pushName?.pushName
82
+ ? data.pushName.pushName
83
+ : typeof data.pushName === 'string'
84
+ ? data.pushName
85
+ : null;
86
+
87
+ // Extract remoteJid safely
88
+ const remoteJidValue =
89
+ typeof data.remoteJid === 'object' && data.remoteJid?.remoteJid ? data.remoteJid.remoteJid : data.remoteJid;
90
+
91
+ const session = await this.prismaRepository.integrationSession.create({
92
+ data: {
93
+ remoteJid: remoteJidValue,
94
+ pushName: pushNameValue,
95
+ sessionId: remoteJidValue,
96
+ status: 'opened',
97
+ awaitUser: false,
98
+ botId: data.botId,
99
+ instanceId: instance.instanceId,
100
+ type: type,
101
+ },
102
+ });
103
+
104
+ return { session };
105
+ } catch (error) {
106
+ this.logger.error(error);
107
+ return;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Standard implementation for processing incoming messages
113
+ * This handles the common workflow across all chatbot types:
114
+ * 1. Check for existing session or create new one
115
+ * 2. Handle message based on session state
116
+ */
117
+ public async process(
118
+ instance: any,
119
+ remoteJid: string,
120
+ bot: BotType,
121
+ session: IntegrationSession,
122
+ settings: SettingsType,
123
+ content: string,
124
+ pushName?: string,
125
+ msg?: any,
126
+ ): Promise<void> {
127
+ try {
128
+ // For new sessions or sessions awaiting initialization
129
+ if (!session) {
130
+ await this.initNewSession(instance, remoteJid, bot, settings, session, content, pushName, msg);
131
+ return;
132
+ }
133
+
134
+ // If session is paused, ignore the message
135
+ if (session.status === 'paused') {
136
+ return;
137
+ }
138
+
139
+ // For existing sessions, keywords might indicate the conversation should end
140
+ const keywordFinish = (settings as any)?.keywordFinish || '';
141
+ const normalizedContent = content.toLowerCase().trim();
142
+ if (keywordFinish.length > 0 && normalizedContent === keywordFinish.toLowerCase()) {
143
+ // Update session to closed and return
144
+ await this.prismaRepository.integrationSession.update({
145
+ where: {
146
+ id: session.id,
147
+ },
148
+ data: {
149
+ status: 'closed',
150
+ },
151
+ });
152
+ return;
153
+ }
154
+
155
+ // Forward the message to the chatbot API
156
+ await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
157
+
158
+ // Update session to indicate we're waiting for user response
159
+ await this.prismaRepository.integrationSession.update({
160
+ where: {
161
+ id: session.id,
162
+ },
163
+ data: {
164
+ status: 'opened',
165
+ awaitUser: true,
166
+ },
167
+ });
168
+ } catch (error) {
169
+ this.logger.error(`Error in process: ${error}`);
170
+ return;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Standard implementation for sending messages to WhatsApp
176
+ * This handles common patterns like markdown links and formatting
177
+ */
178
+ protected async sendMessageWhatsApp(
179
+ instance: any,
180
+ remoteJid: string,
181
+ message: string,
182
+ settings: SettingsType,
183
+ linkPreview: boolean = true,
184
+ ): Promise<void> {
185
+ if (!message) return;
186
+
187
+ const linkRegex = /!?\[(.*?)\]\((.*?)\)/g;
188
+ let textBuffer = '';
189
+ let lastIndex = 0;
190
+ let match: RegExpExecArray | null;
191
+
192
+ const splitMessages = (settings as any)?.splitMessages ?? false;
193
+
194
+ while ((match = linkRegex.exec(message)) !== null) {
195
+ const [fullMatch, altText, url] = match;
196
+ const mediaType = this.getMediaType(url);
197
+ const beforeText = message.slice(lastIndex, match.index);
198
+
199
+ if (beforeText) {
200
+ textBuffer += beforeText;
201
+ }
202
+
203
+ if (mediaType) {
204
+ // Send accumulated text before sending media
205
+ if (textBuffer.trim()) {
206
+ await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages, linkPreview);
207
+ textBuffer = '';
208
+ }
209
+
210
+ // Handle sending the media
211
+ try {
212
+ if (mediaType === 'audio') {
213
+ await instance.audioWhatsapp({
214
+ number: remoteJid.split('@')[0],
215
+ delay: (settings as any)?.delayMessage || 1000,
216
+ audio: url,
217
+ caption: altText,
218
+ });
219
+ } else {
220
+ await instance.mediaMessage(
221
+ {
222
+ number: remoteJid.split('@')[0],
223
+ delay: (settings as any)?.delayMessage || 1000,
224
+ mediatype: mediaType,
225
+ media: url,
226
+ caption: altText,
227
+ fileName: mediaType === 'document' ? altText || 'document' : undefined,
228
+ },
229
+ null,
230
+ false,
231
+ );
232
+ }
233
+ } catch (error) {
234
+ this.logger.error(`Error sending media: ${error}`);
235
+ // If media fails, at least send the alt text and URL
236
+ textBuffer += `${altText}: ${url}`;
237
+ }
238
+ } else {
239
+ // It's a regular link, keep it in the text
240
+ textBuffer += fullMatch;
241
+ }
242
+
243
+ lastIndex = linkRegex.lastIndex;
244
+ }
245
+
246
+ // Add any remaining text after the last match
247
+ if (lastIndex < message.length) {
248
+ const remainingText = message.slice(lastIndex);
249
+ if (remainingText.trim()) {
250
+ textBuffer += remainingText;
251
+ }
252
+ }
253
+
254
+ // Send any remaining text
255
+ if (textBuffer.trim()) {
256
+ await this.sendFormattedText(instance, remoteJid, textBuffer.trim(), settings, splitMessages, linkPreview);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Split message by double line breaks and return array of message parts
262
+ */
263
+ private splitMessageByDoubleLineBreaks(message: string): string[] {
264
+ return message.split('\n\n').filter((part) => part.trim().length > 0);
265
+ }
266
+
267
+ /**
268
+ * Send a single message with proper typing indicators and delays
269
+ */
270
+ private async sendSingleMessage(
271
+ instance: any,
272
+ remoteJid: string,
273
+ message: string,
274
+ settings: any,
275
+ linkPreview: boolean = true,
276
+ ): Promise<void> {
277
+ const timePerChar = settings?.timePerChar ?? 0;
278
+ const minDelay = 1000;
279
+ const maxDelay = 20000;
280
+ const delay = Math.min(Math.max(message.length * timePerChar, minDelay), maxDelay);
281
+
282
+ this.logger.debug(`[BaseChatbot] Sending single message with linkPreview: ${linkPreview}`);
283
+
284
+ if (instance.integration === Integration.WHATSAPP_BAILEYS) {
285
+ await instance.client.presenceSubscribe(remoteJid);
286
+ await instance.client.sendPresenceUpdate('composing', remoteJid);
287
+ }
288
+
289
+ await new Promise<void>((resolve) => {
290
+ setTimeout(async () => {
291
+ await instance.textMessage(
292
+ {
293
+ number: remoteJid.split('@')[0],
294
+ delay: settings?.delayMessage || 1000,
295
+ text: message,
296
+ linkPreview,
297
+ },
298
+ false,
299
+ );
300
+ resolve();
301
+ }, delay);
302
+ });
303
+
304
+ if (instance.integration === Integration.WHATSAPP_BAILEYS) {
305
+ await instance.client.sendPresenceUpdate('paused', remoteJid);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Helper method to send formatted text with proper typing indicators and delays
311
+ */
312
+ private async sendFormattedText(
313
+ instance: any,
314
+ remoteJid: string,
315
+ text: string,
316
+ settings: any,
317
+ splitMessages: boolean,
318
+ linkPreview: boolean = true,
319
+ ): Promise<void> {
320
+ if (splitMessages) {
321
+ const messageParts = this.splitMessageByDoubleLineBreaks(text);
322
+
323
+ this.logger.debug(`[BaseChatbot] Splitting message into ${messageParts.length} parts`);
324
+
325
+ for (let index = 0; index < messageParts.length; index++) {
326
+ const message = messageParts[index];
327
+
328
+ this.logger.debug(`[BaseChatbot] Sending message part ${index + 1}/${messageParts.length}`);
329
+ await this.sendSingleMessage(instance, remoteJid, message, settings, linkPreview);
330
+ }
331
+
332
+ this.logger.debug(`[BaseChatbot] All message parts sent successfully`);
333
+ } else {
334
+ this.logger.debug(`[BaseChatbot] Sending single message`);
335
+ await this.sendSingleMessage(instance, remoteJid, text, settings, linkPreview);
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Standard implementation for initializing a new session
341
+ * This method should be overridden if a subclass needs specific initialization
342
+ */
343
+ protected async initNewSession(
344
+ instance: any,
345
+ remoteJid: string,
346
+ bot: BotType,
347
+ settings: SettingsType,
348
+ session: IntegrationSession,
349
+ content: string,
350
+ pushName?: string | any,
351
+ msg?: any,
352
+ ): Promise<void> {
353
+ // Create a session if none exists
354
+ if (!session) {
355
+ // Extract pushName properly - if it's an object with pushName property, use that
356
+ const pushNameValue =
357
+ typeof pushName === 'object' && pushName?.pushName
358
+ ? pushName.pushName
359
+ : typeof pushName === 'string'
360
+ ? pushName
361
+ : null;
362
+
363
+ const sessionResult = await this.createNewSession(
364
+ {
365
+ instanceName: instance.instanceName,
366
+ instanceId: instance.instanceId,
367
+ },
368
+ {
369
+ remoteJid,
370
+ pushName: pushNameValue,
371
+ botId: (bot as any).id,
372
+ },
373
+ this.getBotType(),
374
+ );
375
+
376
+ if (!sessionResult || !sessionResult.session) {
377
+ this.logger.error('Failed to create new session');
378
+ return;
379
+ }
380
+
381
+ session = sessionResult.session;
382
+ }
383
+
384
+ // Update session status to opened
385
+ await this.prismaRepository.integrationSession.update({
386
+ where: {
387
+ id: session.id,
388
+ },
389
+ data: {
390
+ status: 'opened',
391
+ awaitUser: false,
392
+ },
393
+ });
394
+
395
+ // Forward the message to the chatbot
396
+ await this.sendMessageToBot(instance, session, settings, bot, remoteJid, pushName || '', content, msg);
397
+ }
398
+
399
+ /**
400
+ * Get the bot type identifier (e.g., 'dify', 'n8n', 'evoai')
401
+ * This should match the type field used in the IntegrationSession
402
+ */
403
+ protected abstract getBotType(): string;
404
+
405
+ /**
406
+ * Send a message to the chatbot API
407
+ * This is specific to each chatbot integration
408
+ */
409
+ protected abstract sendMessageToBot(
410
+ instance: any,
411
+ session: IntegrationSession,
412
+ settings: SettingsType,
413
+ bot: BotType,
414
+ remoteJid: string,
415
+ pushName: string,
416
+ content: string,
417
+ msg?: any,
418
+ ): Promise<void>;
419
+ }