From 50fc83b0086f2f1592135dab670fc546c4731ec9 Mon Sep 17 00:00:00 2001 From: vini Date: Sun, 7 Sep 2025 08:08:26 -0300 Subject: [PATCH] feat(business): change profile/cover photo and manage quick replies (#1724) --- src/Defaults/index.ts | 6 +- src/Socket/business.ts | 143 +++++++++++++++++++++++++++++++++++- src/Socket/chats.ts | 29 +++++++- src/Types/Bussines.ts | 20 +++++ src/Types/Chat.ts | 2 + src/Types/Message.ts | 2 +- src/Utils/chat-utils.ts | 16 ++++ src/Utils/messages-media.ts | 7 +- 8 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 src/Types/Bussines.ts diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index 118f567f..4f7885aa 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -79,7 +79,8 @@ export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = { 'thumbnail-link': '/mms/image', 'product-catalog-image': '/product/image', 'md-app-state': '', - 'md-msg-hist': '/mms/md-app-state' + 'md-msg-hist': '/mms/md-app-state', + 'biz-cover-photo': '/pps/biz-cover-photo' } export const MEDIA_HKDF_KEY_MAPPING = { @@ -100,7 +101,8 @@ export const MEDIA_HKDF_KEY_MAPPING = { 'md-app-state': 'App State', 'product-catalog-image': '', 'payment-bg-image': 'Payment Background', - ptv: 'Video' + ptv: 'Video', + 'biz-cover-photo': 'Image' } export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[] diff --git a/src/Socket/business.ts b/src/Socket/business.ts index e35ed558..cd00b7bd 100644 --- a/src/Socket/business.ts +++ b/src/Socket/business.ts @@ -1,4 +1,6 @@ -import type { GetCatalogOptions, ProductCreate, ProductUpdate, SocketConfig } from '../Types' +import type { GetCatalogOptions, ProductCreate, ProductUpdate, SocketConfig, WAMediaUpload } from '../Types' +import type { UpdateBussinesProfileProps } from '../Types/Bussines' +import { getRawMediaUploadData } from '../Utils' import { parseCatalogNode, parseCollectionsNode, @@ -15,6 +17,140 @@ export const makeBusinessSocket = (config: SocketConfig) => { const sock = makeMessagesRecvSocket(config) const { authState, query, waUploadToServer } = sock + const updateBussinesProfile = async (args: UpdateBussinesProfileProps) => { + const node: BinaryNode[] = [] + const simpleFields: (keyof UpdateBussinesProfileProps)[] = ['address', 'email', 'description'] + + node.push( + ...simpleFields + .filter(key => args[key]) + .map(key => ({ + tag: key, + attrs: {}, + content: args[key] as string + })) + ) + + if (args.websites) { + node.push( + ...args.websites.map(website => ({ + tag: 'website', + attrs: {}, + content: website + })) + ) + } + + if (args.hours) { + node.push({ + tag: 'business_hours', + attrs: { timezone: args.hours.timezone }, + content: args.hours.days.map(config => { + const base = { + tag: 'business_hours_config', + attrs: { day_of_week: config.day, mode: config.mode } + } + + if (config.mode === 'specific_hours') { + return { + ...base, + attrs: { + ...base.attrs, + open_time: config.openTimeInMinutes, + close_time: config.closeTimeInMinutes + } + } + } + + return base + }) + }) + } + + const result = await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'set', + xmlns: 'w:biz' + }, + content: [ + { + tag: 'business_profile', + attrs: { + v: '3', + mutation_type: 'delta' + }, + content: node + } + ] + }) + + return result + } + + const updateCoverPhoto = async (photo: WAMediaUpload) => { + const { fileSha256, filePath } = await getRawMediaUploadData(photo, 'biz-cover-photo') + const fileSha256B64 = fileSha256.toString('base64') + + const { meta_hmac, fbid, ts } = await waUploadToServer(filePath, { + fileEncSha256B64: fileSha256B64, + mediaType: 'biz-cover-photo' + }) + + await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'set', + xmlns: 'w:biz' + }, + content: [ + { + tag: 'business_profile', + attrs: { + v: '3', + mutation_type: 'delta' + }, + content: [ + { + tag: 'cover_photo', + attrs: { id: String(fbid), op: 'update', token: meta_hmac!, ts: String(ts) } + } + ] + } + ] + }) + + return fbid! + } + + const removeCoverPhoto = async (id: string) => { + return await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + type: 'set', + xmlns: 'w:biz' + }, + content: [ + { + tag: 'business_profile', + attrs: { + v: '3', + mutation_type: 'delta' + }, + content: [ + { + tag: 'cover_photo', + attrs: { op: 'delete', id } + } + ] + } + ] + }) + } + const getCatalog = async ({ jid, limit, cursor }: GetCatalogOptions) => { jid = jid || authState.creds.me?.id jid = jidNormalizedUser(jid) @@ -277,6 +413,9 @@ export const makeBusinessSocket = (config: SocketConfig) => { getCollections, productCreate, productDelete, - productUpdate + productUpdate, + updateBussinesProfile, + updateCoverPhoto, + removeCoverPhoto } } diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index 3c29c399..939bb20e 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -26,6 +26,7 @@ import type { WAReadReceiptsValue } from '../Types' import { ALL_WA_PATCH_NAMES } from '../Types' +import type { QuickReplyAction } from '../Types/Bussines.js' import type { LabelActionBody } from '../Types/Label' import { SyncState } from '../Types/State' import { @@ -997,6 +998,30 @@ export const makeChatsSocket = (config: SocketConfig) => { ) } + /** + * Add or Edit Quick Reply + */ + const addOrEditQuickReply = (quickReply: QuickReplyAction) => { + return chatModify( + { + quickReply + }, + '' + ) + } + + /** + * Remove Quick Reply + */ + const removeQuickReply = (timestamp: string) => { + return chatModify( + { + quickReply: { timestamp, deleted: true } + }, + '' + ) + } + /** * queries need to be fired on connection open * help ensure parity with WA Web @@ -1200,6 +1225,8 @@ export const makeChatsSocket = (config: SocketConfig) => { removeChatLabel, addMessageLabel, removeMessageLabel, - star + star, + addOrEditQuickReply, + removeQuickReply } } diff --git a/src/Types/Bussines.ts b/src/Types/Bussines.ts new file mode 100644 index 00000000..60f01beb --- /dev/null +++ b/src/Types/Bussines.ts @@ -0,0 +1,20 @@ +import type { proto } from '../../WAProto' + +export type DayOfWeekBussines = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' + +export type HoursDay = + | { day: DayOfWeekBussines; mode: 'specific_hours'; openTimeInMinutes: string; closeTimeInMinutes: string } + | { day: DayOfWeekBussines; mode: 'open_24h' | 'appointment_only' } + +export type UpdateBussinesProfileProps = { + address?: string + websites?: string[] + email?: string + description?: string + hours?: { + timezone: string + days: HoursDay[] + } +} + +export type QuickReplyAction = proto.SyncActionValue.IQuickReplyAction & { timestamp?: string } diff --git a/src/Types/Chat.ts b/src/Types/Chat.ts index 9b47fa2b..e951c22c 100644 --- a/src/Types/Chat.ts +++ b/src/Types/Chat.ts @@ -1,5 +1,6 @@ import type { proto } from '../../WAProto/index.js' import type { AccountSettings } from './Auth' +import type { QuickReplyAction } from './Bussines.js' import type { BufferedEventData } from './Events' import type { LabelActionBody } from './Label' import type { ChatLabelAssociationActionBody } from './LabelAssociation' @@ -119,6 +120,7 @@ export type ChatModification = | { removeChatLabel: ChatLabelAssociationActionBody } | { addMessageLabel: MessageLabelAssociationActionBody } | { removeMessageLabel: MessageLabelAssociationActionBody } + | { quickReply: QuickReplyAction } export type InitialReceivedChatsState = { [jid: string]: { diff --git a/src/Types/Message.ts b/src/Types/Message.ts index d6ce0944..581857a3 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -321,7 +321,7 @@ export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions & export type WAMediaUploadFunction = ( encFilePath: string, opts: { fileEncSha256B64: string; mediaType: MediaType; timeoutMs?: number } -) => Promise<{ mediaUrl: string; directPath: string }> +) => Promise<{ mediaUrl: string; directPath: string; meta_hmac?: string; ts?: number; fbid?: number }> export type MediaGenerationOptions = { logger?: ILogger diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index a6bf85c9..73772df7 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -666,6 +666,22 @@ export const chatModificationToAppPatch = (mod: ChatModification, jid: string) = apiVersion: 1, operation: OP.SET } + } else if ('quickReply' in mod) { + patch = { + syncAction: { + quickReplyAction: { + count: 0, + deleted: mod.quickReply.deleted || false, + keywords: [], + message: mod.quickReply.message || '', + shortcut: mod.quickReply.shortcut || '' + } + }, + index: ['quick_reply', mod.quickReply.timestamp || String(Math.floor(Date.now() / 1000))], + type: 'regular', + apiVersion: 2, + operation: OP.SET + } } else if ('addLabel' in mod) { patch = { syncAction: { diff --git a/src/Utils/messages-media.ts b/src/Utils/messages-media.ts index e928a37d..6020f61b 100644 --- a/src/Utils/messages-media.ts +++ b/src/Utils/messages-media.ts @@ -632,7 +632,7 @@ export const getWAUploadToServer = ( // send a query JSON to obtain the url & auth token to upload our media let uploadInfo = await refreshMediaConn(false) - let urls: { mediaUrl: string; directPath: string } | undefined + let urls: { mediaUrl: string; directPath: string; meta_hmac?: string; ts?: number; fbid?: number } | undefined const hosts = [...customUploadHosts, ...uploadInfo.hosts] fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64) @@ -664,7 +664,10 @@ export const getWAUploadToServer = ( if (result?.url || result?.directPath) { urls = { mediaUrl: result.url, - directPath: result.direct_path + directPath: result.direct_path, + meta_hmac: result.meta_hmac, + fbid: result.fbid, + ts: result.ts } break } else {