From c4edcef5da575b675b04a7aed7826bd69528fca7 Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Sun, 6 Mar 2022 13:30:11 +0530 Subject: [PATCH] feat: implement fetching product catalog + order details on MD --- src/Socket/business.ts | 150 ++++++++++++++++++++++++++++++++++ src/Socket/index.ts | 2 +- src/Types/Product.ts | 13 +++ src/Utils/business.ts | 107 ++++++++++++++++++++++++ src/WABinary/generic-utils.ts | 9 ++ 5 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 src/Socket/business.ts create mode 100644 src/Utils/business.ts diff --git a/src/Socket/business.ts b/src/Socket/business.ts new file mode 100644 index 0000000..ab7fbe6 --- /dev/null +++ b/src/Socket/business.ts @@ -0,0 +1,150 @@ +import { SocketConfig } from '../Types' +import { parseCatalogNode, parseCollectionsNode, parseOrderDetailsNode } from '../Utils/business' +import { jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary' +import { makeMessagesRecvSocket } from './messages-recv' + +export const makeBusinessSocket = (config: SocketConfig) => { + const { logger } = config + const sock = makeMessagesRecvSocket(config) + const { + authState, + query + } = sock + + const getCatalog = async(jid?: string, limit = 10) => { + jid = jidNormalizedUser(jid || authState.creds.me?.id) + const result = await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'get', + xmlns: 'w:biz:catalog' + }, + content: [ + { + tag: 'product_catalog', + attrs: { + jid, + allow_shop_source: 'true' + }, + content: [ + { + tag: 'limit', + attrs: { }, + content: Buffer.from([ 49, 48 ]) + }, + { + tag: 'width', + attrs: { }, + content: Buffer.from([ 49, 48, 48 ]) + }, + { + tag: 'height', + attrs: { }, + content: Buffer.from([ 49, 48, 48 ]) + } + ] + } + ] + }) + return parseCatalogNode(result) + } + + const getCollections = async(jid?: string, limit = 51) => { + jid = jidNormalizedUser(jid || authState.creds.me?.id) + const result = await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'get', + xmlns: 'w:biz:catalog', + smax_id: '35' + }, + content: [ + { + tag: 'collections', + attrs: { + biz_jid: jid, + }, + content: [ + { + tag: 'collection_limit', + attrs: { }, + content: Buffer.from([ 49, 48 ]) + }, + { + tag: 'item_limit', + attrs: { }, + content: Buffer.from([ limit ]) + }, + { + tag: 'width', + attrs: { }, + content: Buffer.from([ 49, 48, 48 ]) + }, + { + tag: 'height', + attrs: { }, + content: Buffer.from([ 49, 48, 48 ]) + } + ] + } + ] + }) + + return parseCollectionsNode(result) + } + + const getOrderDetails = async(orderId: string, tokenBase64: string) => { + const result = await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'get', + xmlns: 'fb:thrift_iq', + smax_id: '5' + }, + content: [ + { + tag: 'order', + attrs: { + op: 'get', + id: orderId + }, + content: [ + { + tag: 'image_dimensions', + attrs: { }, + content: [ + { + tag: 'width', + attrs: { }, + content: Buffer.from([ 49, 48, 48 ]) + }, + { + tag: 'height', + attrs: { }, + content: Buffer.from([ 49, 48, 48 ]) + } + ] + }, + { + tag: 'token', + attrs: { }, + content: Buffer.from(tokenBase64, 'base64') + } + ] + } + ] + }) + + return parseOrderDetailsNode(result) + } + + return { + ...sock, + getCatalog, + getCollections, + getOrderDetails, + } +} \ No newline at end of file diff --git a/src/Socket/index.ts b/src/Socket/index.ts index b61c19e..455034f 100644 --- a/src/Socket/index.ts +++ b/src/Socket/index.ts @@ -1,6 +1,6 @@ import { DEFAULT_CONNECTION_CONFIG } from '../Defaults' import { SocketConfig } from '../Types' -import { makeMessagesRecvSocket as _makeSocket } from './messages-recv' +import { makeBusinessSocket as _makeSocket } from './business' // export the last socket layer const makeWASocket = (config: Partial) => ( diff --git a/src/Types/Product.ts b/src/Types/Product.ts index 653fe3c..8cce1b6 100644 --- a/src/Types/Product.ts +++ b/src/Types/Product.ts @@ -10,6 +10,19 @@ export type ProductCreateResult = { data: { product: any } } +export type CatalogStatus = { + status: string + canAppeal: boolean +} + +export type CatalogCollection = { + id: string + name: string + products: Product[] + + status: CatalogStatus +} + export type ProductAvailability = 'in stock' export type ProductBase = { diff --git a/src/Utils/business.ts b/src/Utils/business.ts new file mode 100644 index 0000000..b2725cb --- /dev/null +++ b/src/Utils/business.ts @@ -0,0 +1,107 @@ +import { CatalogCollection, CatalogStatus, OrderDetails, OrderProduct, Product } from '../Types' +import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString } from '../WABinary' + +export const parseCatalogNode = (node: BinaryNode) => { + const catalogNode = getBinaryNodeChild(node, 'product_catalog') + const products = getBinaryNodeChildren(catalogNode, 'product').map(parseProductNode) + return { products } +} + +export const parseCollectionsNode = (node: BinaryNode) => { + const collectionsNode = getBinaryNodeChild(node, 'collections') + const collections = getBinaryNodeChildren(collectionsNode, 'collection').map( + collectionNode => { + const id = parseCatalogId(collectionNode) + const name = getBinaryNodeChildString(collectionNode, 'name') + + const products = getBinaryNodeChildren(collectionNode, 'product').map(parseProductNode) + return { + id, + name, + products, + status: parseStatusInfo(collectionNode) + } + } + ) + + return { + collections + } +} + +export const parseOrderDetailsNode = (node: BinaryNode) => { + const orderNode = getBinaryNodeChild(node, 'order') + const products = getBinaryNodeChildren(orderNode, 'product').map( + productNode => { + const imageNode = getBinaryNodeChild(productNode, 'image') + return { + id: parseCatalogId(productNode), + name: getBinaryNodeChildString(productNode, 'name'), + imageUrl: getBinaryNodeChildString(imageNode, 'url'), + price: +getBinaryNodeChildString(productNode, 'price'), + currency: getBinaryNodeChildString(productNode, 'currency'), + quantity: +getBinaryNodeChildString(productNode, 'quantity') + } + } + ) + + const priceNode = getBinaryNodeChild(orderNode, 'price') + + const orderDetails: OrderDetails = { + price: { + total: +getBinaryNodeChildString(priceNode, 'total'), + currency: getBinaryNodeChildString(priceNode, 'currency'), + }, + products + } + + return orderDetails +} + +const parseProductNode = (productNode: BinaryNode) => { + const isHidden = productNode.attrs.is_hidden === 'true' + const id = parseCatalogId(productNode) + + const mediaNode = getBinaryNodeChild(productNode, 'media') + const statusInfoNode = getBinaryNodeChild(productNode, 'status_info') + + const product: Product = { + id, + imageUrls: parseImageUrls(mediaNode), + reviewStatus: { + whatsapp: getBinaryNodeChildString(statusInfoNode, 'status'), + }, + availability: 'in stock', + name: getBinaryNodeChildString(productNode, 'name'), + retailerId: getBinaryNodeChildString(productNode, 'retailer_id'), + url: getBinaryNodeChildString(productNode, 'url'), + description: getBinaryNodeChildString(productNode, 'description'), + price: +getBinaryNodeChildString(productNode, 'price'), + currency: getBinaryNodeChildString(productNode, 'currency'), + isHidden, + } + + return product +} + +const parseImageUrls = (mediaNode: BinaryNode) => { + const imgNode = getBinaryNodeChild(mediaNode, 'image') + return { + requested: getBinaryNodeChildString(imgNode, 'request_image_url'), + original: getBinaryNodeChildString(imgNode, 'original_image_url') + } +} + +const parseStatusInfo = (mediaNode: BinaryNode): CatalogStatus => { + const node = getBinaryNodeChild(mediaNode, 'status_info') + return { + status: getBinaryNodeChildString(node, 'status'), + canAppeal: getBinaryNodeChildString(node, 'can_appeal') === 'true', + } +} + +const parseCatalogId = (node: BinaryNode) => { + const idNode = getBinaryNodeChildBuffer(node, 'id') + const id = Buffer.from(idNode).readBigUInt64LE().toString() + return id +} \ No newline at end of file diff --git a/src/WABinary/generic-utils.ts b/src/WABinary/generic-utils.ts index 82d601c..ebe994a 100644 --- a/src/WABinary/generic-utils.ts +++ b/src/WABinary/generic-utils.ts @@ -33,6 +33,15 @@ export const getBinaryNodeChildBuffer = (node: BinaryNode, childTag: string) => } } +export const getBinaryNodeChildString = (node: BinaryNode, childTag: string) => { + const child = getBinaryNodeChild(node, childTag)?.content + if(Buffer.isBuffer(child) || child instanceof Uint8Array) { + return Buffer.from(child).toString('utf-8') + } else if(typeof child === 'string') { + return child + } +} + export const getBinaryNodeChildUInt = (node: BinaryNode, childTag: string, length: number) => { const buff = getBinaryNodeChildBuffer(node, childTag) if(buff) {