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
8 changes: 8 additions & 0 deletions src/api/controllers/sendMessage.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SendLocationDto,
SendMediaDto,
SendPollDto,
SendProductDto,
SendPtvDto,
SendReactionDto,
SendStatusDto,
Expand Down Expand Up @@ -101,6 +102,13 @@ export class SendMessageController {
return await this.waMonitor.waInstances[instanceName].pollMessage(data);
}

public async sendProduct({ instanceName }: InstanceDto, data: SendProductDto) {
if (!isURL(data?.productImage) && !isBase64(data?.productImage)) {
throw new BadRequestException('productImage must be a URL or base64 string');
}
return await this.waMonitor.waInstances[instanceName].productMessage(data);
}

public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto, file?: any) {
return await this.waMonitor.waInstances[instanceName].statusMessage(data, file);
}
Expand Down
25 changes: 25 additions & 0 deletions src/api/dto/sendMessage.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,28 @@ export class SendReactionDto {
key: proto.IMessageKey;
reaction: string;
}

export class SendProductDto extends Metadata {
/** WhatsApp internal product id (from /business/getCatalog `id`) */
productId: string;
/** Business owner JID β€” `<phone>@s.whatsapp.net` of the catalog owner */
businessOwnerJid: string;
/** Product image β€” URL or base64 */
productImage: string;
/** Merchant-side retailer id (e.g. `BD3`). Optional. */
retailerId?: string;
/** Product title shown to recipients as a fallback. */
title?: string;
/** Product description shown as a fallback. */
description?: string;
/** ISO 4217 currency code (e.g. `ILS`, `USD`). Defaults to `USD`. */
currencyCode?: string;
/** Price Γ— 1000 (e.g. 5500 ILS β†’ `5500000`). */
priceAmount1000?: number;
/** Product landing URL. Optional. */
url?: string;
/** How many images the product has in the catalog. Defaults to 1. */
productImageCount?: number;
/** Optional caption sent alongside the product card. */
caption?: string;
}
48 changes: 48 additions & 0 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
SendLocationDto,
SendMediaDto,
SendPollDto,
SendProductDto,
SendPtvDto,
SendReactionDto,
SendStatusDto,
Expand Down Expand Up @@ -2207,6 +2208,13 @@ export class BaileysStartupService extends ChannelStartupService {
);
}

if (message['product']) {
return await this.client.sendMessage(
sender,
message as unknown as AnyMessageContent,
option as unknown as MiscMessageGenerationOptions,
);
}
if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') {
return await this.client.sendMessage(
sender,
Expand Down Expand Up @@ -2648,6 +2656,46 @@ export class BaileysStartupService extends ChannelStartupService {
);
}

public async productMessage(data: SendProductDto, isIntegration = false) {
if (!data.productId || data.productId.trim().length === 0) {
throw new BadRequestException('productId is required');
}
if (!data.businessOwnerJid || data.businessOwnerJid.trim().length === 0) {
throw new BadRequestException('businessOwnerJid is required');
}
if (!data.productImage || data.productImage.trim().length === 0) {
throw new BadRequestException('productImage is required');
}

const productImage = /^https?:\/\//i.test(data.productImage)
? { url: data.productImage }
: Buffer.from(data.productImage, 'base64');

return await this.sendMessageWithTyping(
data.number,
{
product: {
productImage,
productId: data.productId,
title: data.title ?? '',
description: data.description ?? '',
currencyCode: data.currencyCode ?? 'USD',
priceAmount1000: data.priceAmount1000 != null ? String(data.priceAmount1000) : undefined,
retailerId: data.retailerId ?? '',
url: data.url ?? '',
productImageCount: data.productImageCount ?? 1,
},
businessOwnerJid: data.businessOwnerJid,
caption: data.caption ?? '',
},
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
},
isIntegration,
);
}
public async pollMessage(data: SendPollDto) {
return await this.sendMessageWithTyping(
data.number,
Expand Down
12 changes: 12 additions & 0 deletions src/api/routes/sendMessage.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SendLocationDto,
SendMediaDto,
SendPollDto,
SendProductDto,
SendPtvDto,
SendReactionDto,
SendStatusDto,
Expand All @@ -23,6 +24,7 @@ import {
locationMessageSchema,
mediaMessageSchema,
pollMessageSchema,
productMessageSchema,
ptvMessageSchema,
reactionMessageSchema,
statusMessageSchema,
Expand Down Expand Up @@ -162,6 +164,16 @@ export class MessageRouter extends RouterBroker {

return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendProduct'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendProductDto>({
request: req,
schema: productMessageSchema,
ClassRef: SendProductDto,
execute: (instance, data) => sendMessageController.sendProduct(instance, data),
});

return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendList'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendListDto>({
request: req,
Expand Down
27 changes: 27 additions & 0 deletions src/validate/message.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,30 @@ export const buttonsMessageSchema: JSONSchema7 = {
},
required: ['number'],
};

export const productMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { ...numberDefinition },
productId: { type: 'string', minLength: 1 },
businessOwnerJid: {
type: 'string',
pattern: '^[0-9]+@s[.]whatsapp[.]net$',
description: '"businessOwnerJid" must look like "<phone>@s.whatsapp.net"',
},
productImage: { type: 'string', minLength: 1 },
retailerId: { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
currencyCode: { type: 'string', minLength: 3, maxLength: 3 },
priceAmount1000: { type: 'integer', minimum: 0 },
url: { type: 'string' },
productImageCount: { type: 'integer', minimum: 1 },
caption: { type: 'string' },
delay: { type: 'integer', description: 'Enter a value in milliseconds' },
quoted: { ...quotedOptionsSchema },
},
required: ['number', 'productId', 'businessOwnerJid', 'productImage'],
...isNotEmpty('number', 'productId', 'businessOwnerJid', 'productImage'),
};