Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion prisma/mysql-schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ model Instance {
integration String? @db.VarChar(100)
number String? @db.VarChar(100)
businessId String? @db.VarChar(100)
token String? @db.VarChar(255)
token String? @db.Text
clientName String? @db.VarChar(100)
disconnectionReasonCode Int? @db.Int
disconnectionObject Json? @db.Json
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Instance" ALTER COLUMN "token" SET DATA TYPE TEXT;
2 changes: 1 addition & 1 deletion prisma/postgresql-migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
provider = "postgresql"
2 changes: 1 addition & 1 deletion prisma/postgresql-schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ model Instance {
integration String? @db.VarChar(100)
number String? @db.VarChar(100)
businessId String? @db.VarChar(100)
token String? @db.VarChar(255)
token String? @db.Text
clientName String? @db.VarChar(100)
disconnectionReasonCode Int? @db.Integer
disconnectionObject Json? @db.JsonB
Expand Down
132 changes: 98 additions & 34 deletions src/api/integrations/channel/meta/meta.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,54 +15,118 @@ export class MetaController extends ChannelController implements ChannelControll
integrationEnabled: boolean;

public async receiveWebhook(data: any) {
if (data.object === 'whatsapp_business_account') {
if (data.entry[0]?.changes[0]?.field === 'message_template_status_update') {
const template = await this.prismaRepository.template.findFirst({
where: { templateId: `${data.entry[0].changes[0].value.message_template_id}` },
});
if (data.object !== 'whatsapp_business_account') {
return {
status: 'success',
};
}

if (!template) {
console.log('template not found');
return;
}
const entries = data.entry ?? [];

const { webhookUrl } = template;
for (const entry of entries) {
const changes = entry.changes ?? [];

await axios.post(webhookUrl, data.entry[0].changes[0].value, {
headers: {
'Content-Type': 'application/json',
},
});
return;
}
for (const change of changes) {
if (change?.field === 'message_template_status_update') {
const templateId = change?.value?.message_template_id;

if (!templateId) {
this.logger.error('WebhookService -> receiveWebhookMeta -> templateId not found');
continue;
}

const template = await this.prismaRepository.template.findFirst({
where: {
templateId: String(templateId),
},
});

if (!template) {
this.logger.error(`WebhookService -> receiveWebhookMeta -> template not found: ${templateId}`);
continue;
}

if (!template.webhookUrl) {
this.logger.error(`WebhookService -> receiveWebhookMeta -> template webhookUrl not found: ${templateId}`);
continue;
}

try {
await axios.post(template.webhookUrl, change.value, {
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
this.logger.error(
`WebhookService -> receiveWebhookMeta -> error sending template webhook: ${
error instanceof Error ? error.message : String(error)
}`,
);
}

continue;
}

data.entry?.forEach(async (entry: any) => {
const numberId = entry.changes[0].value.metadata.phone_number_id;
const numberId = change?.value?.metadata?.phone_number_id;

if (!numberId) {
this.logger.error('WebhookService -> receiveWebhookMeta -> numberId not found');
return {
status: 'success',
};
continue;
}

const instance = await this.prismaRepository.instance.findFirst({
where: { number: numberId },
const instances = await this.prismaRepository.instance.findMany({
where: {
number: String(numberId),
},
});

if (!instance) {
this.logger.error('WebhookService -> receiveWebhookMeta -> instance not found');
return {
status: 'success',
};
if (!instances.length) {
this.logger.error(`WebhookService -> receiveWebhookMeta -> instances not found for numberId: ${numberId}`);
continue;
}

await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data);

return {
status: 'success',
const webhookData = {
...data,
entry: [
{
...entry,
changes: [change],
},
],
};
});

const results = await Promise.allSettled(
instances.map(async (instance) => {
const waInstance = this.waMonitor.waInstances[instance.name];

if (!waInstance) {
throw new Error(`Instance not loaded: ${instance.name}`);
}

await waInstance.connectToWhatsapp(webhookData);

return instance.name;
}),
);

results.forEach((result, index) => {
const instanceName = instances[index].name;

if (result.status === 'rejected') {
this.logger.error(
`WebhookService -> receiveWebhookMeta -> error processing webhook for instance ${instanceName}: ${
result.reason instanceof Error ? result.reason.message : String(result.reason)
}`,
);
return;
}

this.logger.log(
`WebhookService -> receiveWebhookMeta -> webhook processed successfully for instance ${instanceName}`,
);
});
}
}

return {
Expand Down
89 changes: 77 additions & 12 deletions src/api/integrations/channel/meta/whatsapp.business.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,38 @@ export class BusinessStartupService extends ChannelStartupService {
public async connectToWhatsapp(data?: any): Promise<any> {
if (!data) return;

const content = data.entry[0].changes[0].value;
const content = data?.entry?.[0]?.changes?.[0]?.value;

if (!content) {
this.logger.error('ChannelStartupService -> connectToWhatsapp -> webhook content not found');
return;
}

try {
this.loadChatwoot();
const message = Array.isArray(content.messages) ? content.messages[0] : undefined;

const status = Array.isArray(content.statuses) ? content.statuses[0] : undefined;

const messageEcho = Array.isArray(content.message_echoes) ? content.message_echoes[0] : undefined;

const phoneNumber = message?.from ?? status?.recipient_id ?? messageEcho?.to;

if (!phoneNumber) {
this.logger.error(
'ChannelStartupService -> connectToWhatsapp -> phone number not found in messages, statuses or message_echoes',
);
return;
}

this.phoneNumber = createJid(phoneNumber);

this.eventHandler(content);
this.loadChatwoot();

this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id);
await this.eventHandler(content);
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(error?.toString());

throw new InternalServerErrorException(error instanceof Error ? error.message : String(error));
}
}

Expand Down Expand Up @@ -387,7 +408,10 @@ export class BusinessStartupService extends ChannelStartupService {
let messageRaw: any;
let pushName: any;

if (received.contacts) pushName = received.contacts[0].profile.name;
const receivedContact = received.contacts[0];
const remoteJid = receivedContact.profile?.phone || receivedContact.wa_id;

if (received.contacts) pushName = receivedContact.profile?.name || '';
Comment on lines +411 to +414

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against missing or empty received.contacts before indexing into it.

receivedContact = received.contacts[0] runs before the if (received.contacts) guard, so an undefined or empty contacts array will cause a runtime error. Consider guarding first, e.g.:

const receivedContact = Array.isArray(received.contacts) ? received.contacts[0] : undefined;
if (receivedContact) {
  const remoteJid = receivedContact.profile?.phone || receivedContact.wa_id;
  pushName = receivedContact.profile?.name || '';
}

This prevents crashes when contacts is missing or empty and keeps remoteJid aligned with the same presence checks as pushName.


if (received.messages) {
const message = received.messages[0]; // AΓ±adir esta lΓ­nea para definir message
Expand Down Expand Up @@ -702,7 +726,7 @@ export class BusinessStartupService extends ChannelStartupService {
});

const contactRaw: any = {
remoteJid: received.contacts[0].profile.phone,
remoteJid,
pushName,
// profilePicUrl: '',
instanceId: this.instanceId,
Expand All @@ -714,7 +738,7 @@ export class BusinessStartupService extends ChannelStartupService {

if (contact) {
const contactRaw: any = {
remoteJid: received.contacts[0].profile.phone,
remoteJid,
pushName,
// profilePicUrl: '',
instanceId: this.instanceId,
Expand Down Expand Up @@ -1016,7 +1040,8 @@ export class BusinessStartupService extends ChannelStartupService {
return await this.post(content, 'messages');
}
if (message['media']) {
const isImage = message['mimetype']?.startsWith('image/');
const mimeType = this.normalizeMimeType(message['mimetype']);
const isImage = mimeType.startsWith('image/');

content = {
messaging_product: 'whatsapp',
Expand All @@ -1025,14 +1050,25 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''),
[message['mediaType']]: {
[message['type']]: message['id'],

...(message['mediaType'] !== 'audio' &&
message['mediaType'] !== 'video' &&
message['fileName'] &&
!isImage && { filename: message['fileName'] }),
...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }),
!isImage && {
filename: message['fileName'],
}),

...(message['mediaType'] !== 'audio' &&
message['caption'] && {
caption: message['caption'],
}),
},
};
quoted ? (content.context = { message_id: quoted.id }) : content;

if (quoted) {
content.context = { message_id: quoted.id };
}

return await this.post(content, 'messages');
}
if (message['audio']) {
Expand Down Expand Up @@ -1174,6 +1210,35 @@ export class BusinessStartupService extends ChannelStartupService {
}
}

private normalizeMimeType(value: unknown): string {
if (typeof value === 'string') {
return value;
}

if (Array.isArray(value)) {
const mimeType = value.find((item) => typeof item === 'string');
return typeof mimeType === 'string' ? mimeType : '';
}

if (value && typeof value === 'object') {
const mimeTypeObject = value as Record<string, unknown>;

const candidates = [
mimeTypeObject.mimetype,
mimeTypeObject.mimeType,
mimeTypeObject.contentType,
mimeTypeObject.content_type,
mimeTypeObject.type,
];

const mimeType = candidates.find((item) => typeof item === 'string');

return typeof mimeType === 'string' ? mimeType : '';
}

return '';
}

// Send Message Controller
public async textMessage(data: SendTextDto, isIntegration = false) {
const res = await this.sendMessageWithTyping(
Expand Down