mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-16 04:02:54 -06:00
Merge 37319966db
into 39606240da
This commit is contained in:
commit
713f6dde8c
@ -1,7 +1,6 @@
|
|||||||
.git
|
.git
|
||||||
*Dockerfile*
|
*Dockerfile*
|
||||||
*docker-compose*
|
*docker-compose*
|
||||||
package-lock.json
|
|
||||||
.env
|
.env
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
@ -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
|
||||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -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
|
||||||
|
@ -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
|
||||||
|
12
Dockerfile
12
Dockerfile
@ -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
|
||||||
|
|
||||||
@ -55,4 +57,4 @@ ENV DOCKER_ENV=true
|
|||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ]
|
ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ]
|
||||||
|
3635
package-lock.json
generated
3635
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||||
|
@ -455,39 +455,46 @@ 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 {
|
||||||
const fileBuffer = audioFile?.buffer || file?.buffer;
|
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||||
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
|
const hasRealMedia = this.hasValidMediaContent(messageRaw);
|
||||||
|
|
||||||
let mediaType: string;
|
if (!hasRealMedia) {
|
||||||
let mimetype = audioFile?.mimetype || file.mimetype;
|
this.logger.warn('Message detected as media but contains no valid media content');
|
||||||
|
} else {
|
||||||
|
const fileBuffer = audioFile?.buffer || file?.buffer;
|
||||||
|
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
|
||||||
|
|
||||||
if (messageRaw.messageType === 'documentMessage') {
|
let mediaType: string;
|
||||||
mediaType = 'document';
|
let mimetype = audioFile?.mimetype || file.mimetype;
|
||||||
mimetype = !mimetype ? 'application/pdf' : mimetype;
|
|
||||||
} else if (messageRaw.messageType === 'imageMessage') {
|
if (messageRaw.messageType === 'documentMessage') {
|
||||||
mediaType = 'image';
|
mediaType = 'document';
|
||||||
mimetype = !mimetype ? 'image/png' : mimetype;
|
mimetype = !mimetype ? 'application/pdf' : mimetype;
|
||||||
} else if (messageRaw.messageType === 'audioMessage') {
|
} else if (messageRaw.messageType === 'imageMessage') {
|
||||||
mediaType = 'audio';
|
mediaType = 'image';
|
||||||
mimetype = !mimetype ? 'audio/mp4' : mimetype;
|
mimetype = !mimetype ? 'image/png' : mimetype;
|
||||||
} else if (messageRaw.messageType === 'videoMessage') {
|
} else if (messageRaw.messageType === 'audioMessage') {
|
||||||
mediaType = 'video';
|
mediaType = 'audio';
|
||||||
mimetype = !mimetype ? 'video/mp4' : mimetype;
|
mimetype = !mimetype ? 'audio/mp4' : mimetype;
|
||||||
|
} else if (messageRaw.messageType === 'videoMessage') {
|
||||||
|
mediaType = 'video';
|
||||||
|
mimetype = !mimetype ? 'video/mp4' : mimetype;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
|
||||||
|
|
||||||
|
const size = buffer.byteLength;
|
||||||
|
|
||||||
|
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
|
||||||
|
|
||||||
|
await s3Service.uploadFile(fullName, buffer, size, {
|
||||||
|
'Content-Type': mimetype,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||||
|
|
||||||
|
messageRaw.message.mediaUrl = mediaUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
|
|
||||||
|
|
||||||
const size = buffer.byteLength;
|
|
||||||
|
|
||||||
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
|
|
||||||
|
|
||||||
await s3Service.uploadFile(fullName, buffer, size, {
|
|
||||||
'Content-Type': mimetype,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
|
||||||
|
|
||||||
messageRaw.message.mediaUrl = mediaUrl;
|
|
||||||
} catch (error) {
|
} 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]);
|
||||||
}
|
}
|
||||||
|
@ -429,107 +429,114 @@ export class BusinessStartupService extends ChannelStartupService {
|
|||||||
try {
|
try {
|
||||||
const message: any = received;
|
const message: any = received;
|
||||||
|
|
||||||
const id = message.messages[0][message.messages[0].type].id;
|
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||||
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
const hasRealMedia = this.hasValidMediaContent(messageRaw);
|
||||||
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
|
||||||
urlServer = `${urlServer}/${version}/${id}`;
|
|
||||||
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
|
||||||
const result = await axios.get(urlServer, { headers });
|
|
||||||
|
|
||||||
const buffer = await axios.get(result.data.url, {
|
if (!hasRealMedia) {
|
||||||
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
this.logger.warn('Message detected as media but contains no valid media content');
|
||||||
responseType: 'arraybuffer',
|
|
||||||
});
|
|
||||||
|
|
||||||
let mediaType;
|
|
||||||
|
|
||||||
if (message.messages[0].document) {
|
|
||||||
mediaType = 'document';
|
|
||||||
} else if (message.messages[0].image) {
|
|
||||||
mediaType = 'image';
|
|
||||||
} else if (message.messages[0].audio) {
|
|
||||||
mediaType = 'audio';
|
|
||||||
} else {
|
} else {
|
||||||
mediaType = 'video';
|
const id = message.messages[0][message.messages[0].type].id;
|
||||||
}
|
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
|
||||||
|
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
|
||||||
|
urlServer = `${urlServer}/${version}/${id}`;
|
||||||
|
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
|
||||||
|
const result = await axios.get(urlServer, { headers });
|
||||||
|
|
||||||
const mimetype = result.data?.mime_type || result.headers['content-type'];
|
const buffer = await axios.get(result.data.url, {
|
||||||
|
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
});
|
||||||
|
|
||||||
const contentDisposition = result.headers['content-disposition'];
|
let mediaType;
|
||||||
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
|
|
||||||
if (contentDisposition) {
|
if (message.messages[0].document) {
|
||||||
const match = contentDisposition.match(/filename="(.+?)"/);
|
mediaType = 'document';
|
||||||
if (match) {
|
} else if (message.messages[0].image) {
|
||||||
fileName = match[1];
|
mediaType = 'image';
|
||||||
|
} else if (message.messages[0].audio) {
|
||||||
|
mediaType = 'audio';
|
||||||
|
} else {
|
||||||
|
mediaType = 'video';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Para áudio, garantir extensão correta baseada no mimetype
|
const mimetype = result.data?.mime_type || result.headers['content-type'];
|
||||||
if (mediaType === 'audio') {
|
|
||||||
if (mimetype.includes('ogg')) {
|
const contentDisposition = result.headers['content-disposition'];
|
||||||
fileName = `${message.messages[0].id}.ogg`;
|
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
|
||||||
} else if (mimetype.includes('mp3')) {
|
if (contentDisposition) {
|
||||||
fileName = `${message.messages[0].id}.mp3`;
|
const match = contentDisposition.match(/filename="(.+?)"/);
|
||||||
} else if (mimetype.includes('m4a')) {
|
if (match) {
|
||||||
fileName = `${message.messages[0].id}.m4a`;
|
fileName = match[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const size = result.headers['content-length'] || buffer.data.byteLength;
|
// Para áudio, garantir extensão correta baseada no mimetype
|
||||||
|
if (mediaType === 'audio') {
|
||||||
|
if (mimetype.includes('ogg')) {
|
||||||
|
fileName = `${message.messages[0].id}.ogg`;
|
||||||
|
} else if (mimetype.includes('mp3')) {
|
||||||
|
fileName = `${message.messages[0].id}.mp3`;
|
||||||
|
} else if (mimetype.includes('m4a')) {
|
||||||
|
fileName = `${message.messages[0].id}.m4a`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
|
const size = result.headers['content-length'] || buffer.data.byteLength;
|
||||||
|
|
||||||
await s3Service.uploadFile(fullName, buffer.data, size, {
|
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
|
||||||
'Content-Type': mimetype,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdMessage = await this.prismaRepository.message.create({
|
await s3Service.uploadFile(fullName, buffer.data, size, {
|
||||||
data: messageRaw,
|
'Content-Type': mimetype,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prismaRepository.media.create({
|
const createdMessage = await this.prismaRepository.message.create({
|
||||||
data: {
|
data: messageRaw,
|
||||||
messageId: createdMessage.id,
|
});
|
||||||
instanceId: this.instanceId,
|
|
||||||
type: mediaType,
|
|
||||||
fileName: fullName,
|
|
||||||
mimetype,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
await this.prismaRepository.media.create({
|
||||||
|
data: {
|
||||||
messageRaw.message.mediaUrl = mediaUrl;
|
messageId: createdMessage.id,
|
||||||
messageRaw.message.base64 = buffer.data.toString('base64');
|
|
||||||
|
|
||||||
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
|
|
||||||
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
|
|
||||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
|
||||||
where: {
|
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
},
|
type: mediaType,
|
||||||
include: {
|
fileName: fullName,
|
||||||
OpenaiCreds: true,
|
mimetype,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||||
openAiDefaultSettings &&
|
|
||||||
openAiDefaultSettings.openaiCredsId &&
|
messageRaw.message.mediaUrl = mediaUrl;
|
||||||
openAiDefaultSettings.speechToText
|
messageRaw.message.base64 = buffer.data.toString('base64');
|
||||||
) {
|
|
||||||
try {
|
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
|
||||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
|
||||||
openAiDefaultSettings.OpenaiCreds,
|
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||||
{
|
where: {
|
||||||
message: {
|
instanceId: this.instanceId,
|
||||||
mediaUrl: messageRaw.message.mediaUrl,
|
},
|
||||||
...messageRaw,
|
include: {
|
||||||
|
OpenaiCreds: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
openAiDefaultSettings &&
|
||||||
|
openAiDefaultSettings.openaiCredsId &&
|
||||||
|
openAiDefaultSettings.speechToText
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
|
||||||
|
openAiDefaultSettings.OpenaiCreds,
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
mediaUrl: messageRaw.message.mediaUrl,
|
||||||
|
...messageRaw,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
)}`;
|
||||||
)}`;
|
} catch (speechError) {
|
||||||
} catch (speechError) {
|
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
||||||
this.logger.error(`Error processing speech-to-text: ${speechError}`);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,33 +1249,41 @@ 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;
|
||||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
|
||||||
|
|
||||||
const { buffer, mediaType, fileName, size } = media;
|
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
const hasRealMedia = this.hasValidMediaContent(message);
|
||||||
const fullName = join(
|
|
||||||
`${this.instance.id}`,
|
|
||||||
received.key.remoteJid,
|
|
||||||
mediaType,
|
|
||||||
`${Date.now()}_${fileName}`,
|
|
||||||
);
|
|
||||||
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
|
|
||||||
|
|
||||||
await this.prismaRepository.media.create({
|
if (!hasRealMedia) {
|
||||||
data: {
|
this.logger.warn('Message detected as media but contains no valid media content');
|
||||||
messageId: msg.id,
|
} else {
|
||||||
instanceId: this.instanceId,
|
const media = await this.getBase64FromMediaMessage({ message }, true);
|
||||||
type: mediaType,
|
|
||||||
fileName: fullName,
|
|
||||||
mimetype,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
const { buffer, mediaType, fileName, size } = media;
|
||||||
|
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||||
|
const fullName = join(
|
||||||
|
`${this.instance.id}`,
|
||||||
|
received.key.remoteJid,
|
||||||
|
mediaType,
|
||||||
|
`${Date.now()}_${fileName}`,
|
||||||
|
);
|
||||||
|
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
|
||||||
|
|
||||||
messageRaw.message.mediaUrl = mediaUrl;
|
await this.prismaRepository.media.create({
|
||||||
|
data: {
|
||||||
|
messageId: msg.id,
|
||||||
|
instanceId: this.instanceId,
|
||||||
|
type: mediaType,
|
||||||
|
fileName: fullName,
|
||||||
|
mimetype,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||||
|
|
||||||
|
messageRaw.message.mediaUrl = mediaUrl;
|
||||||
|
|
||||||
|
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,31 +2156,39 @@ 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;
|
||||||
const media = await this.getBase64FromMediaMessage({ message }, true);
|
|
||||||
|
|
||||||
const { buffer, mediaType, fileName, size } = media;
|
// Verificação adicional para garantir que há conteúdo de mídia real
|
||||||
|
const hasRealMedia = this.hasValidMediaContent(message);
|
||||||
|
|
||||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
if (!hasRealMedia) {
|
||||||
|
this.logger.warn('Message detected as media but contains no valid media content');
|
||||||
|
} else {
|
||||||
|
const media = await this.getBase64FromMediaMessage({ message }, true);
|
||||||
|
|
||||||
const fullName = join(
|
const { buffer, mediaType, fileName, size } = media;
|
||||||
`${this.instance.id}`,
|
|
||||||
messageRaw.key.remoteJid,
|
|
||||||
`${messageRaw.key.id}`,
|
|
||||||
mediaType,
|
|
||||||
fileName,
|
|
||||||
);
|
|
||||||
|
|
||||||
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
|
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||||
|
|
||||||
await this.prismaRepository.media.create({
|
const fullName = join(
|
||||||
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype },
|
`${this.instance.id}`,
|
||||||
});
|
messageRaw.key.remoteJid,
|
||||||
|
`${messageRaw.key.id}`,
|
||||||
|
mediaType,
|
||||||
|
fileName,
|
||||||
|
);
|
||||||
|
|
||||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
|
||||||
|
|
||||||
messageRaw.message.mediaUrl = mediaUrl;
|
await this.prismaRepository.media.create({
|
||||||
|
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype },
|
||||||
|
});
|
||||||
|
|
||||||
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||||
|
|
||||||
|
messageRaw.message.mediaUrl = mediaUrl;
|
||||||
|
|
||||||
|
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;
|
||||||
{ key: msg?.key, message: msg?.message },
|
|
||||||
'buffer',
|
try {
|
||||||
{},
|
buffer = await downloadMediaMessage(
|
||||||
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
|
{ key: msg?.key, message: msg?.message },
|
||||||
);
|
'buffer',
|
||||||
|
{},
|
||||||
|
{ 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']);
|
||||||
|
@ -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
|
||||||
|
@ -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 = [
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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
19
src/railway.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user