Add Evolution API files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Dockerfile +59 -0
- manager/dist/assets/index-CO3NSIFj.js +0 -0
- manager/dist/assets/index-DsIrum0U.css +1 -0
- manager/dist/index.html +14 -0
- package.json +156 -0
- src/@types/express.d.ts +9 -0
- src/api/abstract/abstract.cache.ts +19 -0
- src/api/abstract/abstract.repository.ts +66 -0
- src/api/abstract/abstract.router.ts +226 -0
- src/api/controllers/business.controller.ts +15 -0
- src/api/controllers/call.controller.ts +11 -0
- src/api/controllers/chat.controller.ts +116 -0
- src/api/controllers/group.controller.ts +84 -0
- src/api/controllers/instance.controller.ts +445 -0
- src/api/controllers/label.controller.ts +15 -0
- src/api/controllers/proxy.controller.ts +74 -0
- src/api/controllers/sendMessage.controller.ts +107 -0
- src/api/controllers/settings.controller.ts +16 -0
- src/api/controllers/template.controller.ts +15 -0
- src/api/dto/business.dto.ts +14 -0
- src/api/dto/call.dto.ts +8 -0
- src/api/dto/chat.dto.ts +129 -0
- src/api/dto/chatbot.dto.ts +12 -0
- src/api/dto/group.dto.ts +56 -0
- src/api/dto/instance.dto.ts +58 -0
- src/api/dto/label.dto.ts +12 -0
- src/api/dto/proxy.dto.ts +8 -0
- src/api/dto/sendMessage.dto.ts +169 -0
- src/api/dto/settings.dto.ts +10 -0
- src/api/dto/template.dto.ts +8 -0
- src/api/guards/auth.guard.ts +53 -0
- src/api/guards/instance.guard.ts +55 -0
- src/api/guards/telemetry.guard.ts +12 -0
- src/api/integrations/channel/channel.controller.ts +95 -0
- src/api/integrations/channel/channel.router.ts +17 -0
- src/api/integrations/channel/evolution/evolution.channel.service.ts +888 -0
- src/api/integrations/channel/evolution/evolution.controller.ts +39 -0
- src/api/integrations/channel/evolution/evolution.router.ts +18 -0
- src/api/integrations/channel/meta/meta.controller.ts +72 -0
- src/api/integrations/channel/meta/meta.router.ts +24 -0
- src/api/integrations/channel/meta/whatsapp.business.service.ts +1755 -0
- src/api/integrations/channel/whatsapp/baileys.controller.ts +60 -0
- src/api/integrations/channel/whatsapp/baileys.router.ts +105 -0
- src/api/integrations/channel/whatsapp/baileysMessage.processor.ts +59 -0
- src/api/integrations/channel/whatsapp/voiceCalls/transport.type.ts +78 -0
- src/api/integrations/channel/whatsapp/voiceCalls/useVoiceCallsBaileys.ts +181 -0
- src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +0 -0
- src/api/integrations/chatbot/base-chatbot.controller.ts +950 -0
- src/api/integrations/chatbot/base-chatbot.dto.ts +42 -0
- 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 |
+
}
|