diff --git a/src/LegacySocket/business.ts b/src/LegacySocket/business.ts index 3faba02..566eb24 100644 --- a/src/LegacySocket/business.ts +++ b/src/LegacySocket/business.ts @@ -1,5 +1,6 @@ import { LegacySocketConfig, OrderDetails } from '../Types' import { CatalogResult, Product, ProductCreate, ProductCreateResult, ProductUpdate } from '../Types' +import { uploadingNecessaryImages } from '../Utils/business' import makeGroupsSocket from './groups' const makeBusinessSocket = (config: LegacySocketConfig) => { @@ -7,6 +8,7 @@ const makeBusinessSocket = (config: LegacySocketConfig) => { const { query, generateMessageTag, + waUploadToServer, state } = sock @@ -46,7 +48,7 @@ const makeBusinessSocket = (config: LegacySocketConfig) => { json: [ 'action', 'addProduct_reh', - mapProductCreate(product) + await mapProductCreate(product) ] }) @@ -72,6 +74,10 @@ const makeBusinessSocket = (config: LegacySocketConfig) => { } const productUpdate = async(productId: string, update: ProductUpdate) => { + const productCreate = await mapProductCreate( + { ...update, originCountryCode: undefined }, + false + ) const result: ProductCreateResult = await query({ expect200: true, json: [ @@ -79,10 +85,7 @@ const makeBusinessSocket = (config: LegacySocketConfig) => { 'editProduct_reh', { product_id: productId, - ...mapProductCreate( - { ...update, originCountryCode: undefined }, - false - ) + ...productCreate } ] }) @@ -128,13 +131,17 @@ const makeBusinessSocket = (config: LegacySocketConfig) => { } // maps product create to send to WA - const mapProductCreate = (product: ProductCreate, mapCompliance = true) => { + const mapProductCreate = async(product: ProductCreate, mapCompliance = true) => { + const imgs = ( + await uploadingNecessaryImages(product.images, waUploadToServer) + ).map(img => img.url) + const result: any = { name: product.name, description: product.description, - image_url: product.imageUrls[0], + image_url: imgs[0], url: product.url || '', - additional_image_urls: product.imageUrls.slice(1), + additional_image_urls: imgs.slice(1), retailer_id: product.retailerId || '', width: '100', height: '100', diff --git a/src/LegacySocket/messages.ts b/src/LegacySocket/messages.ts index e07e5bf..b850cf1 100644 --- a/src/LegacySocket/messages.ts +++ b/src/LegacySocket/messages.ts @@ -407,6 +407,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => { return { ...sock, relayMessage, + waUploadToServer, generateUrlInfo, messageInfo: async(jid: string, messageID: string) => { const { content }: BinaryNode = await query({ diff --git a/src/Socket/business.ts b/src/Socket/business.ts index e3905c2..9c9fadb 100644 --- a/src/Socket/business.ts +++ b/src/Socket/business.ts @@ -1,15 +1,15 @@ import { ProductCreate, ProductUpdate, SocketConfig } from '../Types' -import { parseCatalogNode, parseCollectionsNode, parseOrderDetailsNode, parseProductNode, toProductNode } from '../Utils/business' +import { parseCatalogNode, parseCollectionsNode, parseOrderDetailsNode, parseProductNode, toProductNode, uploadingNecessaryImagesOfProduct } from '../Utils/business' import { jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary' import { getBinaryNodeChild } from '../WABinary/generic-utils' import { makeMessagesRecvSocket } from './messages-recv' export const makeBusinessSocket = (config: SocketConfig) => { - const { logger } = config const sock = makeMessagesRecvSocket(config) const { authState, - query + query, + waUploadToServer } = sock const getCatalog = async(jid?: string, limit = 10) => { @@ -143,6 +143,7 @@ export const makeBusinessSocket = (config: SocketConfig) => { } const productUpdate = async(productId: string, update: ProductUpdate) => { + update = await uploadingNecessaryImagesOfProduct(update, waUploadToServer) const editNode = toProductNode(productId, update) const result = await query({ @@ -168,6 +169,7 @@ export const makeBusinessSocket = (config: SocketConfig) => { } const productCreate = async(create: ProductCreate) => { + create = await uploadingNecessaryImagesOfProduct(create, waUploadToServer) const createNode = toProductNode(undefined, create) const result = await query({ diff --git a/src/Types/Product.ts b/src/Types/Product.ts index 8cce1b6..ca4d5d7 100644 --- a/src/Types/Product.ts +++ b/src/Types/Product.ts @@ -1,3 +1,4 @@ +import { WAMediaUpload } from './Message' export type CatalogResult = { data: { @@ -38,8 +39,8 @@ export type ProductBase = { export type ProductCreate = ProductBase & { /** ISO country code for product origin. Set to undefined for no country */ originCountryCode: string | undefined - - imageUrls: string[] + /** images of the product */ + images: WAMediaUpload[] } export type ProductUpdate = Omit diff --git a/src/Utils/business.ts b/src/Utils/business.ts index 37b935d..caca656 100644 --- a/src/Utils/business.ts +++ b/src/Utils/business.ts @@ -1,5 +1,8 @@ -import { CatalogCollection, CatalogStatus, OrderDetails, OrderProduct, Product, ProductCreate, ProductUpdate } from '../Types' +import { Boom } from '@hapi/boom' +import { createHash } from 'crypto' +import { CatalogCollection, CatalogStatus, OrderDetails, OrderProduct, Product, ProductCreate, ProductUpdate, WAMediaUpload, WAMediaUploadFunction } from '../Types' import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString } from '../WABinary' +import { getStream, toReadable } from './messages-media' export const parseCatalogNode = (node: BinaryNode) => { const catalogNode = getBinaryNodeChild(node, 'product_catalog') @@ -94,22 +97,28 @@ export const toProductNode = (productId: string | undefined, product: ProductCre }) } - if(product.imageUrls.length) { + if(product.images.length) { content.push({ tag: 'media', attrs: { }, - content: product.imageUrls.map( - url => ({ - tag: 'image', - attrs: { }, - content: [ - { - tag: 'url', - attrs: { }, - content: Buffer.from(url) - } - ] - }) + content: product.images.map( + img => { + if(!('url' in img)) { + throw new Boom('Expected img for product to already be uploaded', { statusCode: 400 }) + } + + return { + tag: 'image', + attrs: { }, + content: [ + { + tag: 'url', + attrs: { }, + content: Buffer.from(img.url.toString()) + } + ] + } + } ) }) } @@ -187,6 +196,53 @@ export const parseProductNode = (productNode: BinaryNode) => { return product } +/** + * Uploads images not already uploaded to WA's servers + */ +export async function uploadingNecessaryImagesOfProduct(product: T, waUploadToServer: WAMediaUploadFunction, timeoutMs = 30_000) { + product = { + ...product, + images: product.images ? await uploadingNecessaryImages(product.images, waUploadToServer, timeoutMs) : product.images + } + return product +} + +/** + * Uploads images not already uploaded to WA's servers + */ +export const uploadingNecessaryImages = async(images: WAMediaUpload[], waUploadToServer: WAMediaUploadFunction, timeoutMs = 30_000) => { + const results = await Promise.all( + images.map>( + async img => { + + if('url' in img) { + const url = img.url.toString() + if(url.includes('.whatsapp.net')) { + return { url } + } + } + + const { stream } = await getStream(img) + const hasher = createHash('sha256') + const contentBlocks: Buffer[] = [] + for await (const block of stream) { + hasher.update(block) + contentBlocks.push(block) + } + + const sha = hasher.digest('base64') + + const { mediaUrl } = await waUploadToServer( + toReadable(Buffer.concat(contentBlocks)), + { mediaType: 'image', fileEncSha256B64: sha, timeoutMs } + ) + return { url: mediaUrl } + } + ) + ) + return results +} + const parseImageUrls = (mediaNode: BinaryNode) => { const imgNode = getBinaryNodeChild(mediaNode, 'image') return {