Compare commits

...

49 Commits

Author SHA1 Message Date
Davidson Gomes
d458c978f3 Merge branch 'develop' into develop 2025-07-14 14:42:18 -03:00
Willian Coqueiro
37319966db Remove redudantent code 2025-07-09 18:42:35 +00:00
Willian Coqueiro
630f5c5624 fix:
- [Baileys] Trocar @lids em remoteJid por senderPn em todos os serviços;
 - [Baileys] Adicionar valor @lid recebido em remoteJid para previousRemoteJid (Posteriormente utilizasse em ChatwootService);
 - Minors fixes;
2025-07-09 18:35:57 +00:00
Davidson Gomes
e6ec706a38 Merge pull request #1665 from pauloboc/fix-prisma-type-mysql
Fix prisma type mysql
2025-07-02 11:22:48 -03:00
Davidson Gomes
53101d4571 Merge pull request #1664 from pauloboc/fix-setup-mysql
(mysql): remove out-of-order wavoipToken migration
2025-07-01 08:15:26 -03:00
Davidson Gomes
c7b5abce6e Merge pull request #1670 from Santosl2/fix/typebot-variables
fix: correçao do typebot não conseguir ouvir mensagens de input
2025-07-01 08:14:28 -03:00
Santosl2
5b1b5ff9d2 fix: bind applyFormatting method in processMessages to maintain context 2025-06-29 20:23:31 -03:00
Paulo Ferreira
3efe69ada3 fix(prisma) Mysql: update data types for N8n, N8nSetting, Evoai, and EvoaiSetting models 2025-06-28 09:34:52 -03:00
Paulo Ferreira
287c679ce4 (mysql): remove out-of-order wavoipToken migration
Delete prisma/mysql-migrations/1707735894523_add_wavoip_token_to_settings_table, which executes before the initial Setting table is created and breaks fresh MySQL installs.

The later migration 20250214181954_add_wavoip_token_column, line 145, already adds the column correctly, so keeping only that directory guarantees a clean deploy.
2025-06-28 09:27:13 -03:00
Davidson Gomes
918697866f fix(package-lock): update baileys dependency to latest commit hash 2025-06-27 09:59:40 -03:00
Davidson Gomes
3c917af602 Merge pull request #1660 from KokeroO/develop
fix(whatsapp-baileys): Verifica eventos com falhas e fallback para erro ao baixar mídias
2025-06-27 09:55:22 -03:00
KokeroO
52798fd761 fix(whatsapp-baileys): adjust indentation and access modifier for getBase64FromMediaMessage method 2025-06-27 09:29:12 -03:00
KokeroO
34446d188e fix(whatsapp-baileys):
- Implement broken event checking before duplicate message checking. (Do not process failed events).
- Implement error handling when downloading media with a fallback mechanism.
2025-06-27 09:03:32 -03:00
Davidson Gomes
ef31b6de1f Merge pull request #1655 from KokeroO/develop
fix/references-instanceName-typebot
2025-06-26 17:10:58 -03:00
KokeroO
d7afe5d7ab fix(typebot): update instance query to use instance name instead of instance ID 2025-06-26 14:53:54 -03:00
Davidson Gomes
72fb2f408b feat(rabbitmq): implement robust connection handling with reconnection logic and error logging 2025-06-26 14:25:37 -03:00
KokeroO
e4f7856ca9 fix(typebot): update instance query to use instanceName instead of instanceId 2025-06-26 14:05:28 -03:00
Davidson Gomes
e86b6463fd Merge pull request #1652 from KokeroO/patch-1
Corrige ref. instance typebot.controller.ts
2025-06-26 11:49:41 -03:00
Willian Coqueiro
0302944654 Corrige ref. instance typebot.controller.ts 2025-06-26 11:42:27 -03:00
Davidson Gomes
1b0d81b022 Merge pull request #1641 from caduzin02/patch-1
Create railway.json
2025-06-25 16:32:30 -03:00
Davidson Gomes
854d357518 chore(package-lock): update Baileys dependency to version 6.7.18 2025-06-25 16:25:16 -03:00
caduzin02
6e8da4a8dc Create railway.json 2025-06-24 11:02:00 -03:00
Davidson Gomes
e92e98dd22 chore: optimize Dockerfile by using npm ci for dependency installation and streamlining file copy operations 2025-06-23 16:55:04 -03:00
Davidson Gomes
cdd2e59755 chore: update CHANGELOG for version 2.3.1 to include fix for S3 media upload 2025-06-23 16:51:54 -03:00
Davidson Gomes
1a1d9fc957 refactor: add validation for media content in Evolution and Business services to enhance error handling 2025-06-23 16:42:29 -03:00
Davidson Gomes
8ea4d65bc2 refactor: enhance media handling in Baileys service with validation for valid media content 2025-06-23 16:42:24 -03:00
Davidson Gomes
af713dee55 chore: update Dockerfile to use npm run build instead of npm run build:docker 2025-06-23 16:00:01 -03:00
Davidson Gomes
ee9ccb55ca chore: update version to 2.3.1 in Dockerfile, package.json, and package-lock.json 2025-06-23 15:41:49 -03:00
Davidson Gomes
977f686233 chore: update CHANGELOG for version 2.3.1 and fix package.json formatting 2025-06-23 15:38:54 -03:00
Davidson Gomes
a38caeaf5f chore(package-lock): update Baileys dependency to version 6.7.18 2025-06-23 15:36:54 -03:00
Davidson Gomes
873fdcb22a Merge pull request #1633 from VCalazans/FIX/ISSUE-28
🐛 fix: Phone number as message ID for Evo AI #ISSUE 28
2025-06-23 15:35:56 -03:00
Davidson Gomes
2e077a77ef Merge pull request #1626 from fernandeshenrique15/main
add unreadMessages in the response
2025-06-23 15:35:44 -03:00
Davidson Gomes
4be818a436 Merge pull request #1605 from ToniShelby/patch-2
Update Dockerfile
2025-06-23 15:35:33 -03:00
Davidson Gomes
a7badb9af5 Merge pull request #1624 from matheusfterra/main
Correção do envio de variáveis pelo typeboy
2025-06-23 15:35:20 -03:00
Davidson Gomes
ba5fb567eb Merge branch 'develop' into patch-2 2025-06-23 15:34:54 -03:00
Davidson Gomes
1070caf131 Merge pull request #1609 from KokeroO/feat/merge-contacts-lid-in-message-upsert
feat: Adiciona mesclagem de contatos @lid no Chatwoot
2025-06-23 15:32:46 -03:00
Davidson Gomes
9766e10ce9 Merge pull request #1623 from skarious/main
Update Dockerhub Repository and Delete Config Session Variable
2025-06-23 15:32:00 -03:00
Davidson Gomes
cc5612822f Merge pull request #1635 from autonomaia/patch-8
fix: corrige versão inválida do swagger-ui-express
2025-06-23 15:30:58 -03:00
autonomaia
83c1040114 fix: corrige versão inválida do swagger-ui-express 2025-06-22 22:10:51 -03:00
Victor Calazans
b4d1148d6a 🐛 fix: Phone number as message ID for Evo AI 2025-06-22 09:40:14 -03:00
Henrique Fernandes Neto
bd94423e5b add unreadMessages in the response 2025-06-19 16:30:57 -03:00
Matheus Terra
94670cbca3 Merge pull request #1 from matheusfterra/codex/corrigir-erro-ao-processar-mensagens-com-variáveis
Fix Typebot formatting function
2025-06-19 12:45:33 -03:00
Matheus Terra
bda3f5f146 Fix applyFormatting context in Typebot service 2025-06-19 12:45:09 -03:00
Ron Schneider
7724fe3a4f Merge branch 'main' into main 2025-06-19 02:11:50 -03:00
Ron Schneider
8b5f49592a Update evolution_api_v2.yaml
Update EvolutionApi DockerHub Repository and Delete Config Session Variable
2025-06-19 02:02:59 -03:00
Willian Coqueiro
6c0082cd7a feat: adicionar funcionalidade de mesclagem de contatos no Chatwoot 2025-06-17 22:33:51 +00:00
ToniShelby
3391b2ce4b Update Dockerfile 2025-06-17 16:33:50 +01:00
Ron Schneider
9db56560e4 Merge pull request #1 from skarious/skarious-patch-1
CONFIG_SESSION_PHONE_VERSION Updated
2025-05-29 14:06:12 -03:00
Ron Schneider
786e57603f CONFIG_SESSION_PHONE_VERSION Updated
CONFIG_SESSION_PHONE_VERSION updated
2025-05-29 14:05:42 -03:00
19 changed files with 2408 additions and 2058 deletions

View File

@@ -1,7 +1,6 @@
.git .git
*Dockerfile* *Dockerfile*
*docker-compose* *docker-compose*
package-lock.json
.env .env
node_modules node_modules
dist dist

View File

@@ -196,7 +196,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
# Whatsapp Web version for baileys channel # Whatsapp Web version for baileys channel
# https://web.whatsapp.com/check-update?version=0&platform=web # https://web.whatsapp.com/check-update?version=0&platform=web
# CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
# Set qrcode display limit # Set qrcode display limit
QRCODE_LIMIT=30 QRCODE_LIMIT=30

View File

@@ -1,3 +1,14 @@
# 2.3.1 (develop)
### Fixed
* Update Baileys Version
* Update Dockerhub Repository and Delete Config Session Variable
* Fixed sending variables in typebot
* Add unreadMessages in the response
* Phone number as message ID for Evo AI
* Fix upload to s3 when media message
# 2.3.0 (2025-06-17 09:19) # 2.3.0 (2025-06-17 09:19)
### Feature ### Feature

View File

@@ -2,7 +2,7 @@ version: "3.7"
services: services:
evolution_v2: evolution_v2:
image: atendai/evolution-api:v2.2.3 image: evoapicloud/evolution-api:v2.3.1
volumes: volumes:
- evolution_instances:/evolution/instances - evolution_instances:/evolution/instances
networks: networks:
@@ -94,7 +94,7 @@ services:
- WEBHOOK_EVENTS_ERRORS_WEBHOOK= - WEBHOOK_EVENTS_ERRORS_WEBHOOK=
- CONFIG_SESSION_PHONE_CLIENT=Evolution API V2 - CONFIG_SESSION_PHONE_CLIENT=Evolution API V2
- CONFIG_SESSION_PHONE_NAME=Chrome - CONFIG_SESSION_PHONE_NAME=Chrome
- CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200 #- CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
- QRCODE_LIMIT=30 - QRCODE_LIMIT=30
- OPENAI_ENABLED=true - OPENAI_ENABLED=true
- DIFY_ENABLED=true - DIFY_ENABLED=true

View File

@@ -3,15 +3,17 @@ FROM node:20-alpine AS builder
RUN apk update && \ RUN apk update && \
apk add --no-cache git ffmpeg wget curl bash openssl apk add --no-cache git ffmpeg wget curl bash openssl
LABEL version="2.3.0" description="Api to control whatsapp features through http requests." LABEL version="2.3.1" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@evolution-api.com" LABEL contact="contato@evolution-api.com"
WORKDIR /evolution WORKDIR /evolution
COPY ./package.json ./tsconfig.json ./ COPY ./package*.json ./
COPY ./tsconfig.json ./
COPY ./tsup.config.ts ./
RUN npm install RUN npm ci --silent
COPY ./src ./src COPY ./src ./src
COPY ./public ./public COPY ./public ./public
@@ -19,7 +21,6 @@ COPY ./prisma ./prisma
COPY ./manager ./manager COPY ./manager ./manager
COPY ./.env.example ./.env COPY ./.env.example ./.env
COPY ./runWithProvider.js ./ COPY ./runWithProvider.js ./
COPY ./tsup.config.ts ./
COPY ./Docker ./Docker COPY ./Docker ./Docker
@@ -35,6 +36,7 @@ RUN apk update && \
apk add tzdata ffmpeg bash openssl apk add tzdata ffmpeg bash openssl
ENV TZ=America/Sao_Paulo ENV TZ=America/Sao_Paulo
ENV DOCKER_ENV=true
WORKDIR /evolution WORKDIR /evolution

3625
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "2.3.0", "version": "2.3.1",
"description": "Rest api for communication with WhatsApp", "description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js", "main": "./dist/main.js",
"type": "commonjs", "type": "commonjs",
@@ -73,7 +73,7 @@
"form-data": "^4.0.1", "form-data": "^4.0.1",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"i18next": "^23.7.19", "i18next": "^23.7.19",
"jimp": "^0.16.13", "jimp": "^1.6.0",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@@ -95,9 +95,10 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"redis": "^4.7.0", "redis": "^4.7.0",
"sharp": "^0.32.6", "sharp": "^0.34.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"swagger-ui-express": "^5.0.1",
"tsup": "^8.3.5" "tsup": "^8.3.5"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,9 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE `Setting`
ADD COLUMN IF NOT EXISTS `wavoipToken` VARCHAR(100);

View File

@@ -652,16 +652,16 @@ model N8n {
webhookUrl String? @db.VarChar(255) webhookUrl String? @db.VarChar(255)
basicAuthUser String? @db.VarChar(255) basicAuthUser String? @db.VarChar(255)
basicAuthPass String? @db.VarChar(255) basicAuthPass String? @db.VarChar(255)
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false)
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Integer timePerChar Int? @default(50) @db.Integer
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
@@ -675,16 +675,16 @@ model N8n {
model N8nSetting { model N8nSetting {
id String @id @default(cuid()) id String @id @default(cuid())
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false)
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Integer timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp
@@ -700,16 +700,16 @@ model Evoai {
description String? @db.VarChar(255) description String? @db.VarChar(255)
agentUrl String? @db.VarChar(255) agentUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255) apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false)
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Integer timePerChar Int? @default(50) @db.Integer
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
@@ -723,16 +723,16 @@ model Evoai {
model EvoaiSetting { model EvoaiSetting {
id String @id @default(cuid()) id String @id @default(cuid())
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false)
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Integer timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp

View File

@@ -455,6 +455,12 @@ export class EvolutionStartupService extends ChannelStartupService {
if (base64 || file || audioFile) { if (base64 || file || audioFile) {
if (this.configService.get<S3>('S3').ENABLE) { if (this.configService.get<S3>('S3').ENABLE) {
try { try {
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(messageRaw);
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const fileBuffer = audioFile?.buffer || file?.buffer; const fileBuffer = audioFile?.buffer || file?.buffer;
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer; const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
@@ -488,6 +494,7 @@ export class EvolutionStartupService extends ChannelStartupService {
const mediaUrl = await s3Service.getObjectUrl(fullName); const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl; messageRaw.message.mediaUrl = mediaUrl;
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }

View File

@@ -429,6 +429,12 @@ export class BusinessStartupService extends ChannelStartupService {
try { try {
const message: any = received; const message: any = received;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(messageRaw);
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const id = message.messages[0][message.messages[0].type].id; const id = message.messages[0][message.messages[0].type].id;
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL; let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION; const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
@@ -533,6 +539,7 @@ export class BusinessStartupService extends ChannelStartupService {
} }
} }
} }
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }

View File

@@ -99,6 +99,7 @@ import makeWASocket, {
Contact, Contact,
delay, delay,
DisconnectReason, DisconnectReason,
downloadContentFromMessage,
downloadMediaMessage, downloadMediaMessage,
generateWAMessageFromContent, generateWAMessageFromContent,
getAggregateVotesInPollMessage, getAggregateVotesInPollMessage,
@@ -122,7 +123,7 @@ import makeWASocket, {
WABrowserDescription, WABrowserDescription,
WAMediaUpload, WAMediaUpload,
WAMessage, WAMessage,
WAMessageUpdate, WAMessageKey,
WAPresence, WAPresence,
WASocket, WASocket,
} from 'baileys'; } from 'baileys';
@@ -887,7 +888,7 @@ export class BaileysStartupService extends ChannelStartupService {
}: { }: {
chats: Chat[]; chats: Chat[];
contacts: Contact[]; contacts: Contact[];
messages: proto.IWebMessageInfo[]; messages: WAMessage[];
isLatest?: boolean; isLatest?: boolean;
progress?: number; progress?: number;
syncType?: proto.HistorySync.HistorySyncType; syncType?: proto.HistorySync.HistorySyncType;
@@ -973,6 +974,10 @@ export class BaileysStartupService extends ChannelStartupService {
continue; continue;
} }
if (m.key.remoteJid?.includes('@lid') && m.key.senderPn) {
m.key.remoteJid = m.key.senderPn;
}
if (Long.isLong(m?.messageTimestamp)) { if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber(); m.messageTimestamp = m.messageTimestamp?.toNumber();
} }
@@ -1030,11 +1035,29 @@ export class BaileysStartupService extends ChannelStartupService {
}, },
'messages.upsert': async ( 'messages.upsert': async (
{ messages, type, requestId }: { messages: proto.IWebMessageInfo[]; type: MessageUpsertType; requestId?: string }, { messages, type, requestId }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string },
settings: any, settings: any,
) => { ) => {
try { try {
for (const received of messages) { for (const received of messages) {
if (received.key.remoteJid?.includes('@lid') && received.key.senderPn) {
(received.key as { previousRemoteJid?: string | null }).previousRemoteJid = received.key.remoteJid;
received.key.remoteJid = received.key.senderPn;
}
if (
received?.messageStubParameters?.some?.((param) =>
[
'No matching sessions found for message',
'Bad MAC',
'failed to decrypt message',
'SessionError',
'Invalid PreKey ID',
].some((err) => param?.includes?.(err)),
)
) {
this.logger.warn(`Message ignored with messageStubParameters: ${JSON.stringify(received, null, 2)}`);
continue;
}
if (received.message?.conversation || received.message?.extendedTextMessage?.text) { if (received.message?.conversation || received.message?.extendedTextMessage?.text) {
const text = received.message?.conversation || received.message?.extendedTextMessage?.text; const text = received.message?.conversation || received.message?.extendedTextMessage?.text;
@@ -1226,6 +1249,13 @@ export class BaileysStartupService extends ChannelStartupService {
if (this.configService.get<S3>('S3').ENABLE) { if (this.configService.get<S3>('S3').ENABLE) {
try { try {
const message: any = received; const message: any = received;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(message);
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const media = await this.getBase64FromMediaMessage({ message }, true); const media = await this.getBase64FromMediaMessage({ message }, true);
const { buffer, mediaType, fileName, size } = media; const { buffer, mediaType, fileName, size } = media;
@@ -1253,6 +1283,7 @@ export class BaileysStartupService extends ChannelStartupService {
messageRaw.message.mediaUrl = mediaUrl; messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }
@@ -1356,7 +1387,7 @@ export class BaileysStartupService extends ChannelStartupService {
} }
}, },
'messages.update': async (args: WAMessageUpdate[], settings: any) => { 'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`); this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`);
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true} const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
@@ -1366,6 +1397,10 @@ export class BaileysStartupService extends ChannelStartupService {
continue; continue;
} }
if (key.remoteJid?.includes('@lid') && key.senderPn) {
key.remoteJid = key.senderPn;
}
const updateKey = `${this.instance.id}_${key.id}_${update.status}`; const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
const cached = await this.baileysCache.get(updateKey); const cached = await this.baileysCache.get(updateKey);
@@ -2121,6 +2156,13 @@ export class BaileysStartupService extends ChannelStartupService {
if (isMedia && this.configService.get<S3>('S3').ENABLE) { if (isMedia && this.configService.get<S3>('S3').ENABLE) {
try { try {
const message: any = messageRaw; const message: any = messageRaw;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(message);
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const media = await this.getBase64FromMediaMessage({ message }, true); const media = await this.getBase64FromMediaMessage({ message }, true);
const { buffer, mediaType, fileName, size } = media; const { buffer, mediaType, fileName, size } = media;
@@ -2146,6 +2188,7 @@ export class BaileysStartupService extends ChannelStartupService {
messageRaw.message.mediaUrl = mediaUrl; messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }
@@ -3413,6 +3456,18 @@ export class BaileysStartupService extends ChannelStartupService {
} }
} }
public async mapMediaType(mediaType) {
const map = {
imageMessage: 'image',
videoMessage: 'video',
documentMessage: 'document',
stickerMessage: 'sticker',
audioMessage: 'audio',
ptvMessage: 'video',
};
return map[mediaType] || null;
}
public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) { public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) {
try { try {
const m = data?.message; const m = data?.message;
@@ -3453,12 +3508,39 @@ export class BaileysStartupService extends ChannelStartupService {
msg.message = JSON.parse(JSON.stringify(msg.message)); msg.message = JSON.parse(JSON.stringify(msg.message));
} }
const buffer = await downloadMediaMessage( let buffer: Buffer;
try {
buffer = await downloadMediaMessage(
{ key: msg?.key, message: msg?.message }, { key: msg?.key, message: msg?.message },
'buffer', 'buffer',
{}, {},
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
); );
} catch (err) {
this.logger.error('Download Media failed, trying to retry in 5 seconds...');
await new Promise((resolve) => setTimeout(resolve, 5000));
const mediaType = Object.keys(msg.message).find((key) => key.endsWith('Message'));
try {
const media = await downloadContentFromMessage(
{
mediaKey: msg.message?.[mediaType]?.mediaKey,
directPath: msg.message?.[mediaType]?.directPath,
url: `https://mmg.whatsapp.net${msg?.message?.[mediaType]?.directPath}`,
},
await this.mapMediaType(mediaType),
{},
);
const chunks = [];
for await (const chunk of media) {
chunks.push(chunk);
}
buffer = Buffer.concat(chunks);
this.logger.info('Download Media with downloadContentFromMessage was successful!');
} catch (fallbackErr) {
this.logger.error('Download Media with downloadContentFromMessage also failed!');
}
}
const typeMessage = getContentType(msg.message); const typeMessage = getContentType(msg.message);
const ext = mimeTypes.extension(mediaMessage?.['mimetype']); const ext = mimeTypes.extension(mediaMessage?.['mimetype']);

View File

@@ -26,7 +26,7 @@ import axios from 'axios';
import { proto } from 'baileys'; import { proto } from 'baileys';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import FormData from 'form-data'; import FormData from 'form-data';
import Jimp from 'jimp'; import { Jimp, JimpMime } from 'jimp';
import Long from 'long'; import Long from 'long';
import mimeTypes from 'mime-types'; import mimeTypes from 'mime-types';
import path from 'path'; import path from 'path';
@@ -457,6 +457,24 @@ export class ChatwootService {
} }
} }
private async mergeContacts(baseId: number, mergeId: number) {
try {
const contact = await chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
body: {
base_contact_id: baseId,
mergee_contact_id: mergeId,
},
});
return contact;
} catch {
this.logger.error('Error merging contacts');
return null;
}
}
private async mergeBrazilianContacts(contacts: any[]) { private async mergeBrazilianContacts(contacts: any[]) {
try { try {
const contact = await chatwootRequest(this.getClientCwConfig(), { const contact = await chatwootRequest(this.getClientCwConfig(), {
@@ -549,24 +567,37 @@ export class ChatwootService {
} }
public async createConversation(instance: InstanceDto, body: any) { public async createConversation(instance: InstanceDto, body: any) {
const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn; const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid; const remoteJid = body.key.remoteJid;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`; const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`; const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
const maxWaitTime = 5000; // 5 secounds const maxWaitTime = 5000; // 5 secounds
try { try {
// Processa atualização de contatos já criados @lid // Processa atualização de contatos já criados @lid
if (body.key.remoteJid.includes('@lid') && body.key.senderPn && body.key.senderPn !== body.key.remoteJid) { if (
isLid &&
body.key.senderPn !== body.key.previousRemoteJid
) {
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]); const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
if (contact && contact.identifier !== body.key.senderPn) { if (contact && contact.identifier !== body.key.senderPn) {
this.logger.verbose( this.logger.verbose(
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn})`, `Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn}`,
); );
await this.updateContact(instance, contact.id, { const updateContact = await this.updateContact(instance, contact.id, {
identifier: body.key.senderPn, identifier: body.key.senderPn,
phone_number: `+${body.key.senderPn.split('@')[0]}`, phone_number: `+${body.key.senderPn.split('@')[0]}`,
}); });
if (updateContact === null) {
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
if (baseContact) {
await this.mergeContacts(baseContact.id, contact.id);
this.logger.verbose(
`Merge contacts: (${baseContact.id}) ${baseContact.phone_number} and (${contact.id}) ${contact.phone_number}`,
);
}
}
} }
} }
this.logger.verbose(`--- Start createConversation ---`); this.logger.verbose(`--- Start createConversation ---`);
@@ -685,7 +716,6 @@ export class ChatwootService {
} }
} }
} else { } else {
const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
contact = await this.createContact( contact = await this.createContact(
instance, instance,
chatId, chatId,
@@ -693,7 +723,7 @@ export class ChatwootService {
isGroup, isGroup,
nameContact, nameContact,
picture_url.profilePictureUrl || null, picture_url.profilePictureUrl || null,
jid, remoteJid,
); );
} }
@@ -2101,9 +2131,11 @@ export class ChatwootService {
const fileData = Buffer.from(imgBuffer.data, 'binary'); const fileData = Buffer.from(imgBuffer.data, 'binary');
const img = await Jimp.read(fileData); const img = await Jimp.read(fileData);
await img.cover(320, 180); await img.cover({
w: 320,
const processedBuffer = await img.getBufferAsync(Jimp.MIME_PNG); h: 180,
});
const processedBuffer = await img.getBuffer(JimpMime.png);
const fileStream = new Readable(); const fileStream = new Readable();
fileStream._read = () => {}; // _read is required but you can noop it fileStream._read = () => {}; // _read is required but you can noop it

View File

@@ -70,7 +70,7 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
} }
const callId = `req-${uuidv4().substring(0, 8)}`; const callId = `req-${uuidv4().substring(0, 8)}`;
const messageId = msg?.key?.id || uuidv4(); const messageId = remoteJid.split('@')[0] || uuidv4(); // Use phone number as messageId
// Prepare message parts // Prepare message parts
const parts = [ const parts = [

View File

@@ -119,7 +119,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
const instanceData = await this.prismaRepository.instance.findFirst({ const instanceData = await this.prismaRepository.instance.findFirst({
where: { where: {
id: instance.instanceId, name: instance.instanceName,
}, },
}); });
@@ -290,7 +290,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
request.data.clientSideActions, request.data.clientSideActions,
); );
this.waMonitor.waInstances[instance.instanceId].sendDataWebhook(Events.TYPEBOT_START, { this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
remoteJid: remoteJid, remoteJid: remoteJid,
url: url, url: url,
typebot: typebot, typebot: typebot,

View File

@@ -186,7 +186,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
messages, messages,
input, input,
clientSideActions, clientSideActions,
this.applyFormatting, this.applyFormatting.bind(this),
this.prismaRepository, this.prismaRepository,
).catch((err) => { ).catch((err) => {
console.error('Erro ao processar mensagens:', err); console.error('Erro ao processar mensagens:', err);

View File

@@ -8,7 +8,12 @@ import { EmitData, EventController, EventControllerInterface } from '../event.co
export class RabbitmqController extends EventController implements EventControllerInterface { export class RabbitmqController extends EventController implements EventControllerInterface {
public amqpChannel: amqp.Channel | null = null; public amqpChannel: amqp.Channel | null = null;
private amqpConnection: amqp.Connection | null = null;
private readonly logger = new Logger('RabbitmqController'); private readonly logger = new Logger('RabbitmqController');
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectDelay = 5000; // 5 seconds
private isReconnecting = false;
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor, configService.get<Rabbitmq>('RABBITMQ')?.ENABLED, 'rabbitmq'); super(prismaRepository, waMonitor, configService.get<Rabbitmq>('RABBITMQ')?.ENABLED, 'rabbitmq');
@@ -19,7 +24,11 @@ export class RabbitmqController extends EventController implements EventControll
return; return;
} }
await new Promise<void>((resolve, reject) => { await this.connect();
}
private async connect(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI; const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX; const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX;
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME; const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
@@ -33,22 +42,61 @@ export class RabbitmqController extends EventController implements EventControll
password: url.password || 'guest', password: url.password || 'guest',
vhost: url.pathname.slice(1) || '/', vhost: url.pathname.slice(1) || '/',
frameMax: frameMax, frameMax: frameMax,
heartbeat: 30, // Add heartbeat of 30 seconds
}; };
amqp.connect(connectionOptions, (error, connection) => { amqp.connect(connectionOptions, (error, connection) => {
if (error) { if (error) {
this.logger.error({
local: 'RabbitmqController.connect',
message: 'Failed to connect to RabbitMQ',
error: error.message || error,
});
reject(error); reject(error);
return; return;
} }
// Connection event handlers
connection.on('error', (err) => {
this.logger.error({
local: 'RabbitmqController.connectionError',
message: 'RabbitMQ connection error',
error: err.message || err,
});
this.handleConnectionLoss();
});
connection.on('close', () => {
this.logger.warn('RabbitMQ connection closed');
this.handleConnectionLoss();
});
connection.createChannel((channelError, channel) => { connection.createChannel((channelError, channel) => {
if (channelError) { if (channelError) {
this.logger.error({
local: 'RabbitmqController.createChannel',
message: 'Failed to create RabbitMQ channel',
error: channelError.message || channelError,
});
reject(channelError); reject(channelError);
return; return;
} }
// Channel event handlers
channel.on('error', (err) => {
this.logger.error({
local: 'RabbitmqController.channelError',
message: 'RabbitMQ channel error',
error: err.message || err,
});
this.handleConnectionLoss();
});
channel.on('close', () => {
this.logger.warn('RabbitMQ channel closed');
this.handleConnectionLoss();
});
const exchangeName = rabbitmqExchangeName; const exchangeName = rabbitmqExchangeName;
channel.assertExchange(exchangeName, 'topic', { channel.assertExchange(exchangeName, 'topic', {
@@ -56,16 +104,81 @@ export class RabbitmqController extends EventController implements EventControll
autoDelete: false, autoDelete: false,
}); });
this.amqpConnection = connection;
this.amqpChannel = channel; this.amqpChannel = channel;
this.reconnectAttempts = 0; // Reset reconnect attempts on successful connection
this.isReconnecting = false;
this.logger.info('AMQP initialized'); this.logger.info('AMQP initialized successfully');
resolve(); resolve();
}); });
}); });
}).then(() => { })
if (configService.get<Rabbitmq>('RABBITMQ')?.GLOBAL_ENABLED) this.initGlobalQueues(); .then(() => {
if (configService.get<Rabbitmq>('RABBITMQ')?.GLOBAL_ENABLED) {
this.initGlobalQueues();
}
})
.catch((error) => {
this.logger.error({
local: 'RabbitmqController.init',
message: 'Failed to initialize AMQP',
error: error.message || error,
}); });
this.scheduleReconnect();
throw error;
});
}
private handleConnectionLoss(): void {
if (this.isReconnecting) {
return; // Already attempting to reconnect
}
this.amqpChannel = null;
this.amqpConnection = null;
this.scheduleReconnect();
}
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.logger.error(
`Maximum reconnect attempts (${this.maxReconnectAttempts}) reached. Stopping reconnection attempts.`,
);
return;
}
if (this.isReconnecting) {
return; // Already scheduled
}
this.isReconnecting = true;
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, Math.min(this.reconnectAttempts - 1, 5)); // Exponential backoff with max delay
this.logger.info(
`Scheduling RabbitMQ reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`,
);
setTimeout(async () => {
try {
this.logger.info(
`Attempting to reconnect to RabbitMQ (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
);
await this.connect();
this.logger.info('Successfully reconnected to RabbitMQ');
} catch (error) {
this.logger.error({
local: 'RabbitmqController.scheduleReconnect',
message: `Reconnection attempt ${this.reconnectAttempts} failed`,
error: error.message || error,
});
this.isReconnecting = false;
this.scheduleReconnect();
}
}, delay);
} }
private set channel(channel: amqp.Channel) { private set channel(channel: amqp.Channel) {
@@ -76,6 +189,17 @@ export class RabbitmqController extends EventController implements EventControll
return this.amqpChannel; return this.amqpChannel;
} }
private async ensureConnection(): Promise<boolean> {
if (!this.amqpChannel) {
this.logger.warn('AMQP channel is not available, attempting to reconnect...');
if (!this.isReconnecting) {
this.scheduleReconnect();
}
return false;
}
return true;
}
public async emit({ public async emit({
instanceName, instanceName,
origin, origin,
@@ -95,6 +219,11 @@ export class RabbitmqController extends EventController implements EventControll
return; return;
} }
if (!(await this.ensureConnection())) {
this.logger.warn(`Failed to emit event ${event} for instance ${instanceName}: No AMQP connection`);
return;
}
const instanceRabbitmq = await this.get(instanceName); const instanceRabbitmq = await this.get(instanceName);
const rabbitmqLocal = instanceRabbitmq?.events; const rabbitmqLocal = instanceRabbitmq?.events;
const rabbitmqGlobal = configService.get<Rabbitmq>('RABBITMQ').GLOBAL_ENABLED; const rabbitmqGlobal = configService.get<Rabbitmq>('RABBITMQ').GLOBAL_ENABLED;
@@ -154,7 +283,15 @@ export class RabbitmqController extends EventController implements EventControll
break; break;
} catch (error) { } catch (error) {
this.logger.error({
local: 'RabbitmqController.emit',
message: `Error publishing local RabbitMQ message (attempt ${retry + 1}/3)`,
error: error.message || error,
});
retry++; retry++;
if (retry >= 3) {
this.handleConnectionLoss();
}
} }
} }
} }
@@ -199,7 +336,15 @@ export class RabbitmqController extends EventController implements EventControll
break; break;
} catch (error) { } catch (error) {
this.logger.error({
local: 'RabbitmqController.emit',
message: `Error publishing global RabbitMQ message (attempt ${retry + 1}/3)`,
error: error.message || error,
});
retry++; retry++;
if (retry >= 3) {
this.handleConnectionLoss();
}
} }
} }
} }
@@ -208,33 +353,38 @@ export class RabbitmqController extends EventController implements EventControll
private async initGlobalQueues(): Promise<void> { private async initGlobalQueues(): Promise<void> {
this.logger.info('Initializing global queues'); this.logger.info('Initializing global queues');
if (!(await this.ensureConnection())) {
this.logger.error('Cannot initialize global queues: No AMQP connection');
return;
}
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME; const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS; const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
const prefixKey = configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY; const prefixKey = configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY;
if (!events) { if (!events) {
this.logger.warn('No events to initialize on AMQP'); this.logger.warn('No events to initialize on AMQP');
return; return;
} }
const eventKeys = Object.keys(events); const eventKeys = Object.keys(events);
eventKeys.forEach((event) => { for (const event of eventKeys) {
if (events[event] === false) return; if (events[event] === false) continue;
try {
const queueName = const queueName =
prefixKey !== '' prefixKey !== ''
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}` ? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
: `${event.replace(/_/g, '.').toLowerCase()}`; : `${event.replace(/_/g, '.').toLowerCase()}`;
const exchangeName = rabbitmqExchangeName; const exchangeName = rabbitmqExchangeName;
this.amqpChannel.assertExchange(exchangeName, 'topic', { await this.amqpChannel.assertExchange(exchangeName, 'topic', {
durable: true, durable: true,
autoDelete: false, autoDelete: false,
}); });
this.amqpChannel.assertQueue(queueName, { await this.amqpChannel.assertQueue(queueName, {
durable: true, durable: true,
autoDelete: false, autoDelete: false,
arguments: { arguments: {
@@ -242,7 +392,18 @@ export class RabbitmqController extends EventController implements EventControll
}, },
}); });
this.amqpChannel.bindQueue(queueName, exchangeName, event); await this.amqpChannel.bindQueue(queueName, exchangeName, event);
this.logger.info(`Global queue initialized: ${queueName}`);
} catch (error) {
this.logger.error({
local: 'RabbitmqController.initGlobalQueues',
message: `Failed to initialize global queue for event ${event}`,
error: error.message || error,
}); });
this.handleConnectionLoss();
break;
}
}
} }
} }

View File

@@ -738,6 +738,7 @@ export class ChannelStartupService {
"Chat"."name" as "pushName", "Chat"."name" as "pushName",
"Chat"."createdAt" as "windowStart", "Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires", "Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
"Chat"."unreadMessages" as "unreadMessages",
CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive", CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive",
"Message"."id" AS lastMessageId, "Message"."id" AS lastMessageId,
"Message"."key" AS lastMessage_key, "Message"."key" AS lastMessage_key,
@@ -797,7 +798,7 @@ export class ChannelStartupService {
windowExpires: contact.windowexpires, windowExpires: contact.windowexpires,
windowActive: contact.windowactive, windowActive: contact.windowactive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined, lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
unreadCount: 0, unreadCount: contact.unreadMessages,
isSaved: !!contact.contactid, isSaved: !!contact.contactid,
}; };
}); });
@@ -813,4 +814,28 @@ export class ChannelStartupService {
return []; return [];
} }
public hasValidMediaContent(message: any): boolean {
if (!message?.message) return false;
const msg = message.message;
// Se só tem messageContextInfo, não é mídia válida
if (Object.keys(msg).length === 1 && 'messageContextInfo' in msg) {
return false;
}
// Verifica se tem pelo menos um tipo de mídia válido
const mediaTypes = [
'imageMessage',
'videoMessage',
'stickerMessage',
'documentMessage',
'documentWithCaptionMessage',
'ptvMessage',
'audioMessage',
];
return mediaTypes.some((type) => msg[type] && Object.keys(msg[type]).length > 0);
}
} }

19
src/railway.json Normal file
View File

@@ -0,0 +1,19 @@
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"runtime": "V2",
"numReplicas": 1,
"sleepApplication": false,
"multiRegionConfig": {
"us-east4-eqdc4a": {
"numReplicas": 1
}
},
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}