'use strict';
exports.LoadUtils = () => {
window.WWebJS = {};
/**
* Helper function that compares between two WWeb versions. Its purpose is to help the developer to choose the correct code implementation depending on the comparison value and the WWeb version.
* @param {string} lOperand The left operand for the WWeb version string to compare with
* @param {string} operator The comparison operator
* @param {string} rOperand The right operand for the WWeb version string to compare with
* @returns {boolean} Boolean value that indicates the result of the comparison
*/
window.WWebJS.compareWwebVersions = (lOperand, operator, rOperand) => {
if (!['>', '>=', '<', '<=', '='].includes(operator)) {
throw new (class _ extends Error {
constructor(m) {
super(m);
this.name = 'CompareWwebVersionsError';
}
})('Invalid comparison operator is provided');
}
if (typeof lOperand !== 'string' || typeof rOperand !== 'string') {
throw new (class _ extends Error {
constructor(m) {
super(m);
this.name = 'CompareWwebVersionsError';
}
})('A non-string WWeb version type is provided');
}
lOperand = lOperand.replace(/-beta$/, '');
rOperand = rOperand.replace(/-beta$/, '');
while (lOperand.length !== rOperand.length) {
lOperand.length > rOperand.length
? (rOperand = rOperand.concat('0'))
: (lOperand = lOperand.concat('0'));
}
lOperand = Number(lOperand.replace(/\./g, ''));
rOperand = Number(rOperand.replace(/\./g, ''));
return operator === '>'
? lOperand > rOperand
: operator === '>='
? lOperand >= rOperand
: operator === '<'
? lOperand < rOperand
: operator === '<='
? lOperand <= rOperand
: operator === '='
? lOperand === rOperand
: false;
};
// Customer notes are enabled only for eligible WhatsApp Business accounts.
// The gating module is not shipped to every WhatsApp Web session, so a
// missing module must behave as an unavailable feature rather than an
// exception from the public notes API.
window.WWebJS.isCustomerNotesEnabled = () => {
try {
return Boolean(
window.require('WAWebBizGatingUtils')?.smbNotesV1Enabled?.(),
);
} catch {
return false;
}
};
/**
* Target options object description
* @typedef {Object} TargetOptions
* @property {string|number} module The target module
* @property {string} function The function name to get from a module
*/
/**
* Function to modify functions
* @param {TargetOptions} target Options specifying the target function to search for modifying
* @param {Function} callback Modified function
*/
window.WWebJS.injectToFunction = (target, callback) => {
try {
let module = window.require(target.module);
if (!module) return;
const path = target.function.split('.');
const funcName = path.pop();
for (const key of path) {
if (!module[key]) return;
module = module[key];
}
const originalFunction = module[funcName];
if (typeof originalFunction !== 'function') return;
module[funcName] = ((...args) => {
try {
return callback(module, originalFunction, ...args);
} catch {
return originalFunction.apply(module, args);
}
}).bind(module);
} catch {
return;
}
};
window.WWebJS.injectToFunction(
{ module: 'WAWebBackendJobsCommon', function: 'mediaTypeFromProtobuf' },
(module, func, ...args) => {
const [proto] = args;
return proto.locationMessage ? null : func(...args);
},
);
window.WWebJS.injectToFunction(
{ module: 'WAWebE2EProtoUtils', function: 'typeAttributeFromProtobuf' },
(module, func, ...args) => {
const [proto] = args;
return proto.locationMessage || proto.groupInviteMessage
? 'text'
: func(...args);
},
);
// WhatsApp Web can generate the native-flow protobuf while still emitting
// an incomplete <biz> companion for messages created outside its own UI.
// The server rejects that wire shape with SMAX_INVALID. Keep this adapter
// pinned to builds that have been inspected and canaried, and scope every
// mutation to a message id reserved by the typed InteractiveMessage path.
const INTERACTIVE_FANOUT_ADAPTER_ABI = 'interactive-biz-v2';
const INTERACTIVE_FANOUT_SUPPORTED_WEB_VERSIONS = new Set([
// Inspected against the live WAWeb fanout/WAP modules on 2026-09-02.
// Keep exact builds here: the structural checks below are necessary,
// but a new private-runtime build still requires an explicit canary.
'2.3000.1046623424',
'2.3000.1046532346',
'2.3000.1046527095',
]);
const INTERACTIVE_FANOUT_PENDING_MAX = 100;
const INTERACTIVE_FANOUT_RESERVATION_TTL_MS = 10 * 60 * 1000;
const INTERACTIVE_ACK_CONFIRMATION_TIMEOUT_MS = 10000;
const INTERACTIVE_FANOUT_CONTROLLER = '__wwebjsInteractiveFanoutController';
const INTERACTIVE_FANOUT_KINDS = new Set(['native_flow', 'legacy_list']);
let interactiveFanoutController;
const interactiveNodeTag = (node) =>
typeof node?.tag === 'string' ? node.tag : String(node?.tag || '');
const normalizeInteractiveFanoutTarget = (value) =>
String(value || '').replace(/@c\.us$/u, '@s.whatsapp.net');
const visitInteractiveNode = (root, callback) => {
const seen = new WeakSet();
const visit = (value) => {
if (!value || typeof value !== 'object' || seen.has(value)) return;
seen.add(value);
callback(value);
if (Array.isArray(value.content)) {
for (const child of value.content) visit(child);
}
// The current ABI returns the stanza directly. These explicit
// wrapper keys allow the adapter to fail closed if a compatible
// runtime wraps it without making the traversal unbounded.
for (const key of ['node', 'stanza', 'messageNode']) {
visit(value[key]);
}
};
visit(root);
};
const findInteractiveMessageNode = (root) => {
const matches = [];
visitInteractiveNode(root, (node) => {
if (interactiveNodeTag(node) === 'message') matches.push(node);
});
return matches.length === 1 ? matches[0] : undefined;
};
const requireInteractiveWap = () => {
const Wap = window.require('WAWap');
if (typeof Wap?.wap !== 'function') {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The runtime WAP builder is unavailable.',
);
}
return Wap;
};
const createNativeFlowBizContent = () => {
const Wap = requireInteractiveWap();
return Wap.wap('interactive', { type: 'native_flow', v: '1' }, [
Wap.wap('native_flow', { name: 'mixed', v: '9' }),
]);
};
const createLegacyListBizContent = () => {
const Wap = requireInteractiveWap();
return Wap.wap('list', { type: 'product_list', v: '2' });
};
const applyInteractiveFanoutAdapter = (result, reservation) => {
const messageNode = findInteractiveMessageNode(result);
if (
!messageNode?.attrs ||
!Array.isArray(messageNode.content) ||
String(messageNode.attrs.id || '') !== reservation.id
) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The fanout stanza ABI did not match the pinned adapter.',
);
}
const bizIndexes = [];
messageNode.content.forEach((node, index) => {
if (interactiveNodeTag(node) === 'biz') bizIndexes.push(index);
});
if (
(reservation.kind === 'native_flow' && bizIndexes.length !== 1) ||
(reservation.kind === 'legacy_list' && bizIndexes.length > 1)
) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The fanout business companion ABI did not match the pinned adapter.',
);
}
messageNode.attrs.type = 'text';
visitInteractiveNode(messageNode, (node) => {
if (
interactiveNodeTag(node) === 'enc' &&
node.attrs?.mediatype !== undefined
) {
delete node.attrs.mediatype;
}
});
let bizNode = messageNode.content[bizIndexes[0]];
if (!bizNode && reservation.kind === 'legacy_list') {
const Wap = requireInteractiveWap();
bizNode = Wap.wap('biz', {}, []);
messageNode.content.push(bizNode);
}
if (!bizNode.attrs || typeof bizNode.attrs !== 'object') {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The business companion attributes are unavailable.',
);
}
if (reservation.kind === 'native_flow') {
delete bizNode.attrs.native_flow_name;
const preservedBizChildren = Array.isArray(bizNode.content)
? bizNode.content.filter(
(node) => interactiveNodeTag(node) !== 'interactive',
)
: [];
const canonicalInteractiveNode = createNativeFlowBizContent();
bizNode.content = [
...preservedBizChildren,
canonicalInteractiveNode,
];
const interactiveNodes = bizNode.content.filter(
(node) => interactiveNodeTag(node) === 'interactive',
);
const interactiveNode = interactiveNodes[0];
const nativeFlowNodes = Array.isArray(interactiveNode?.content)
? interactiveNode.content.filter(
(node) => interactiveNodeTag(node) === 'native_flow',
)
: [];
const nativeFlowNode = nativeFlowNodes[0];
if (
interactiveNodeTag(bizNode) !== 'biz' ||
interactiveNodes.length !== 1 ||
interactiveNode?.attrs?.type !== 'native_flow' ||
String(interactiveNode?.attrs?.v) !== '1' ||
nativeFlowNodes.length !== 1 ||
nativeFlowNode?.attrs?.name !== 'mixed' ||
String(nativeFlowNode?.attrs?.v) !== '9'
) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The canonical business companion could not be verified.',
);
}
} else if (reservation.kind === 'legacy_list') {
const preservedBizChildren = Array.isArray(bizNode.content)
? bizNode.content.filter(
(node) => interactiveNodeTag(node) !== 'list',
)
: [];
bizNode.content = [
...preservedBizChildren,
createLegacyListBizContent(),
];
const listNodes = bizNode.content.filter(
(node) => interactiveNodeTag(node) === 'list',
);
const listNode = listNodes[0];
if (
interactiveNodeTag(bizNode) !== 'biz' ||
listNodes.length !== 1 ||
listNode?.attrs?.type !== 'product_list' ||
String(listNode?.attrs?.v) !== '2'
) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The canonical list business companion could not be verified.',
);
}
} else {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The fanout reservation kind is unsupported.',
);
}
reservation.applied = true;
reservation.appliedCount += 1;
};
const pruneInteractiveFanoutReservations = (
controller,
now = Date.now(),
) => {
for (const [id, reservation] of controller.pending) {
if (reservation.expiresAt <= now) controller.pending.delete(id);
}
while (controller.pending.size > INTERACTIVE_FANOUT_PENDING_MAX) {
const oldest = controller.pending.keys().next().value;
if (oldest === undefined) break;
controller.pending.delete(oldest);
}
};
const installInteractiveFanoutAdapter = () => {
const webVersion = window.Debug?.VERSION || null;
if (!INTERACTIVE_FANOUT_SUPPORTED_WEB_VERSIONS.has(webVersion)) {
return {
installed: false,
abi: INTERACTIVE_FANOUT_ADAPTER_ABI,
webVersion,
reason: 'unsupported_web_version',
};
}
let fanoutModule;
let Wap;
try {
fanoutModule = window.require('WAWebSendMsgCreateFanoutStanza');
Wap = window.require('WAWap');
} catch {
return {
installed: false,
abi: INTERACTIVE_FANOUT_ADAPTER_ABI,
webVersion,
reason: 'required_module_unavailable',
};
}
const current = fanoutModule?.createFanoutMsgStanza;
const existing = current?.[INTERACTIVE_FANOUT_CONTROLLER];
if (
existing?.abi === INTERACTIVE_FANOUT_ADAPTER_ABI &&
existing?.webVersion === webVersion &&
existing.pending instanceof Map
) {
interactiveFanoutController = existing;
return existing.status;
}
const descriptor = Object.getOwnPropertyDescriptor(
fanoutModule || {},
'createFanoutMsgStanza',
);
if (
typeof current !== 'function' ||
current.length !== 1 ||
descriptor?.writable !== true ||
typeof Wap?.wap !== 'function'
) {
return {
installed: false,
abi: INTERACTIVE_FANOUT_ADAPTER_ABI,
webVersion,
reason: 'fanout_abi_mismatch',
};
}
const controller = {
abi: INTERACTIVE_FANOUT_ADAPTER_ABI,
webVersion,
pending: new Map(),
original: current,
status: Object.freeze({
installed: true,
abi: INTERACTIVE_FANOUT_ADAPTER_ABI,
webVersion,
reason: null,
}),
};
const wrapped = async function (...args) {
const result = await controller.original.apply(this, args);
pruneInteractiveFanoutReservations(controller);
const input = args[0];
const msgData = input?.msgRecord?.data;
const messageId = String(msgData?.id?.id || '');
const reservation = controller.pending.get(messageId);
if (!reservation) return result;
const recordTarget = normalizeInteractiveFanoutTarget(msgData?.to);
const chatTarget = normalizeInteractiveFanoutTarget(input?.chatId);
let matchesReservation =
recordTarget === reservation.target &&
chatTarget === reservation.target;
if (matchesReservation && reservation.kind === 'native_flow') {
const nativeFlowMessage =
input?.msgProtobuf?.interactiveMessage?.nativeFlowMessage;
const nativeFlowButtons = nativeFlowMessage?.buttons;
const allowedButtonNames = new Set([
'quick_reply',
'cta_url',
'cta_call',
'cta_copy',
]);
matchesReservation = Boolean(
msgData?.type === 'interactive' &&
msgData?.nativeFlowInteractiveMsg === true &&
nativeFlowMessage?.messageVersion === 1 &&
Array.isArray(nativeFlowButtons) &&
nativeFlowButtons.length >= 1 &&
nativeFlowButtons.length <= 3 &&
nativeFlowButtons.every((button) =>
allowedButtonNames.has(button?.name),
),
);
} else if (
matchesReservation &&
reservation.kind === 'legacy_list'
) {
const listMessage = input?.msgProtobuf?.listMessage;
const sections = listMessage?.sections;
const rows = Array.isArray(sections)
? sections.flatMap((section) =>
Array.isArray(section?.rows) ? section.rows : [],
)
: [];
const rowIds = rows.map((row) => row?.rowId);
matchesReservation = Boolean(
msgData?.type === 'list' &&
msgData?.nativeFlowInteractiveMsg !== true &&
Number(listMessage?.listType) === 1 &&
Array.isArray(sections) &&
sections.length >= 1 &&
sections.length <= 10 &&
sections.every(
(section) =>
Array.isArray(section?.rows) &&
section.rows.length >= 1,
) &&
rows.length >= 1 &&
rows.length <= 10 &&
rows.every(
(row) =>
typeof row?.rowId === 'string' &&
row.rowId.length > 0 &&
typeof row?.title === 'string' &&
row.title.length > 0,
) &&
new Set(rowIds).size === rowIds.length &&
listMessage?.productListInfo === undefined,
);
} else {
matchesReservation = false;
}
if (!matchesReservation) {
const error = new Error(
'[INTERACTIVE_UNAVAILABLE] The reserved fanout record did not match the guarded message.',
);
reservation.error = error;
throw error;
}
try {
applyInteractiveFanoutAdapter(result, reservation);
return result;
} catch (error) {
reservation.error = error;
throw error;
}
};
Object.defineProperty(wrapped, INTERACTIVE_FANOUT_CONTROLLER, {
configurable: false,
enumerable: false,
writable: false,
value: controller,
});
fanoutModule.createFanoutMsgStanza = wrapped;
if (fanoutModule.createFanoutMsgStanza !== wrapped) {
return {
installed: false,
abi: INTERACTIVE_FANOUT_ADAPTER_ABI,
webVersion,
reason: 'fanout_adapter_install_failed',
};
}
interactiveFanoutController = controller;
return controller.status;
};
const reserveInteractiveFanout = (
messageId,
targetId,
kind = 'native_flow',
) => {
const status = installInteractiveFanoutAdapter();
if (!status.installed || !interactiveFanoutController) {
throw new Error(
`[INTERACTIVE_UNAVAILABLE] The pinned fanout adapter is unavailable (${status.reason || 'unknown'}).`,
);
}
pruneInteractiveFanoutReservations(interactiveFanoutController);
if (
interactiveFanoutController.pending.size >=
INTERACTIVE_FANOUT_PENDING_MAX
) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The interactive fanout reservation limit was reached.',
);
}
const id = String(messageId || '');
const target = normalizeInteractiveFanoutTarget(targetId);
if (
!id ||
!INTERACTIVE_FANOUT_KINDS.has(kind) ||
interactiveFanoutController.pending.has(id)
) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The interactive fanout message id is invalid or duplicated.',
);
}
if (!/@(?:c\.us|s\.whatsapp\.net|lid)$/u.test(target)) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The pinned fanout adapter supports direct user chats only.',
);
}
const reservation = {
id,
target,
kind,
applied: false,
appliedCount: 0,
error: null,
expiresAt: Date.now() + INTERACTIVE_FANOUT_RESERVATION_TTL_MS,
};
interactiveFanoutController.pending.set(id, reservation);
return {
reservation,
release() {
interactiveFanoutController?.pending.delete(id);
},
};
};
window.WWebJS.getInteractiveFanoutAdapterStatus = () =>
installInteractiveFanoutAdapter();
window.WWebJS.forwardMessage = async (chatId, msgId) => {
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (!msg) {
const error = new Error('Forward source message was not found');
error.code = 'FORWARD_SOURCE_NOT_FOUND';
throw error;
}
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
if (!chat) {
const error = new Error('Forward target chat was not found');
error.code = 'FORWARD_TARGET_NOT_FOUND';
throw error;
}
const failures = await window
.require('WAWebChatForwardMessage')
.forwardMessages({
chat,
msgs: [msg],
multicast: true,
includeCaption: true,
appendedText: undefined,
});
if (Array.isArray(failures) && failures.length > 0) {
const error = new Error(
`WhatsApp rejected forwarding to ${failures.length} chat(s)`,
);
error.code = 'FORWARD_MESSAGE_FAILED';
error.failureCount = failures.length;
throw error;
}
return true;
};
window.WWebJS.sendSeen = async (chatId) => {
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
if (chat) {
window.require('WAWebStreamModel').Stream.markAvailable();
await window.require('WAWebUpdateUnreadChatAction').sendSeen({
chat: chat,
threadId: undefined,
});
window.require('WAWebStreamModel').Stream.markUnavailable();
return true;
}
return false;
};
window.WWebJS.sendMessage = async (chat, content, options = {}) => {
const { getIsNewsletter, getIsBroadcast } =
window.require('WAWebChatGetters');
const isChannel = getIsNewsletter(chat);
const isStatus =
getIsBroadcast(chat) &&
(chat.id?.isStatus?.() === true ||
chat.id?._serialized === 'status@broadcast');
const { findLink } = window.require('WALinkify');
const hasInteractiveMedia = Boolean(
options.interactive && options.media,
);
let interactiveMediaPrep;
let mediaOptions = {};
if (options.media) {
if (hasInteractiveMedia) {
if (
isChannel ||
isStatus ||
options.sendMediaAsSticker ||
options.sendVideoAsGif ||
options.sendAudioAsVoice ||
options.isViewOnce
) {
throw new Error(
'[INTERACTIVE_INVALID_INPUT] Interactive header media supports only direct image, video or document messages.',
);
}
const file = window.WWebJS.mediaInfoToFile(options.media);
const OpaqueData = window.require('WAWebMediaOpaqueData');
const opaqueData = await OpaqueData.createFromData(
file,
options.media.mimetype,
);
const mediaParams = {
asSticker: false,
asGif: false,
isPtt: false,
asDocument: options.sendMediaAsDocument,
};
if (
options.sendMediaAsHd &&
file.type.indexOf('image/') === 0
) {
mediaParams.maxDimension = 2560;
}
interactiveMediaPrep = window
.require('WAWebPrepRawMedia')
.prepRawMedia(opaqueData, mediaParams);
if (
typeof interactiveMediaPrep?.waitForPrep !== 'function' ||
typeof interactiveMediaPrep?.sendToChat !== 'function'
) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The runtime media preparation ABI is unavailable.',
);
}
mediaOptions = await interactiveMediaPrep.waitForPrep();
if (!mediaOptions?.filehash) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] Interactive header media preparation did not produce a file hash.',
);
}
content = mediaOptions.preview;
} else {
mediaOptions =
options.sendMediaAsSticker && !isChannel && !isStatus
? await window.WWebJS.processStickerData(options.media)
: await window.WWebJS.processMediaData(options.media, {
forceSticker: options.sendMediaAsSticker,
forceGif: options.sendVideoAsGif,
forceVoice: options.sendAudioAsVoice,
forceDocument: options.sendMediaAsDocument,
forceMediaHd: options.sendMediaAsHd,
sendToChannel: isChannel,
sendToStatus: isStatus,
});
mediaOptions.caption = options.caption;
content = options.sendMediaAsSticker
? undefined
: mediaOptions.preview;
mediaOptions.isViewOnce = options.isViewOnce;
}
delete options.media;
delete options.sendMediaAsSticker;
}
let quotedMsgOptions = {};
if (options.quotedMessageId) {
let quotedMessage = window
.require('WAWebCollections')
.Msg.get(options.quotedMessageId);
!quotedMessage &&
(quotedMessage = (
await window
.require('WAWebCollections')
.Msg.getMessagesById([options.quotedMessageId])
)?.messages?.[0]);
if (quotedMessage) {
const ReplyUtils = window.require('WAWebMsgReply');
const canReply = ReplyUtils
? ReplyUtils.canReplyMsg(quotedMessage.unsafe())
: quotedMessage.canReply();
if (canReply) {
// Some WA Web builds require chat.id (Wid) for quoted context.
const quotedContextTarget = chat?.id || chat;
quotedMsgOptions =
quotedMessage.msgContextInfo(quotedContextTarget);
}
} else {
if (!options.ignoreQuoteErrors) {
throw new Error('Could not get the quoted message.');
}
}
delete options.ignoreQuoteErrors;
delete options.quotedMessageId;
}
if (options.mentionedJidList) {
options.mentionedJidList = options.mentionedJidList.map((id) =>
window.require('WAWebWidFactory').createWid(id),
);
options.mentionedJidList = options.mentionedJidList.filter(Boolean);
}
if (options.groupMentions) {
options.groupMentions = options.groupMentions.map((e) => ({
groupSubject: e.subject,
groupJid: window.require('WAWebWidFactory').createWid(e.id),
}));
}
let locationOptions = {};
if (options.location) {
let { latitude, longitude, description, url } = options.location;
url = findLink(url)?.href;
url && !description && (description = url);
locationOptions = {
type: 'location',
loc: description,
lat: latitude,
lng: longitude,
clientUrl: url,
};
delete options.location;
}
let pollOptions = {};
if (options.poll) {
const { pollName, pollOptions: _pollOptions } = options.poll;
const { allowMultipleAnswers, messageSecret } =
options.poll.options;
pollOptions = {
kind: 'pollCreation',
type: 'poll_creation',
pollName: pollName,
pollOptions: _pollOptions,
pollSelectableOptionsCount: allowMultipleAnswers ? 0 : 1,
messageSecret:
Array.isArray(messageSecret) && messageSecret.length === 32
? new Uint8Array(messageSecret)
: window.crypto.getRandomValues(new Uint8Array(32)),
};
delete options.poll;
}
let eventOptions = {};
if (options.event) {
const { name, startTimeTs, eventSendOptions } = options.event;
if (
eventSendOptions.callType != null &&
eventSendOptions.callType !== 'none'
) {
throw new Error('scheduled_event_calls_managed_externally');
}
const { messageSecret } = eventSendOptions;
eventOptions = {
type: 'event_creation',
eventName: name,
eventDescription: eventSendOptions.description,
eventStartTime: startTimeTs,
eventEndTime: eventSendOptions.endTimeTs,
eventLocation: eventSendOptions.location && {
degreesLatitude: 0,
degreesLongitude: 0,
name: eventSendOptions.location,
},
eventJoinLink: null,
isEventCanceled: eventSendOptions.isEventCanceled,
messageSecret:
Array.isArray(messageSecret) && messageSecret.length === 32
? new Uint8Array(messageSecret)
: window.crypto.getRandomValues(new Uint8Array(32)),
};
delete options.event;
}
let vcardOptions = {};
if (options.contactCard) {
let contact = await window
.require('WAWebCollections')
.Contact.find(options.contactCard);
vcardOptions = {
body: window
.require('WAWebFrontendVcardUtils')
.vcardFromContactModel(contact).vcard,
type: 'vcard',
vcardFormattedName: contact.formattedName,
};
delete options.contactCard;
} else if (options.contactCardList) {
let contacts = await Promise.all(
options.contactCardList.map((c) =>
window.require('WAWebCollections').Contact.find(c),
),
);
let vcards = contacts.map((c) =>
window
.require('WAWebFrontendVcardUtils')
.vcardFromContactModel(c),
);
vcardOptions = {
type: 'multi_vcard',
vcardList: vcards,
body: null,
};
delete options.contactCardList;
} else if (
options.parseVCards &&
typeof content === 'string' &&
content.startsWith('BEGIN:VCARD')
) {
delete options.parseVCards;
delete options.linkPreview;
try {
const parsed = window
.require('WAWebVcardParsingUtils')
.parseVcard(content);
if (parsed) {
vcardOptions = {
type: 'vcard',
vcardFormattedName: window
.require('WAWebVcardGetNameFromParsed')
.vcardGetNameFromParsed(parsed),
};
}
} catch (ignoredError) {
// not a vcard
}
}
if (options.linkPreview) {
delete options.linkPreview;
const link = findLink(content);
if (link) {
let preview = await window
.require('WAWebLinkPreviewChatAction')
.getLinkPreview(link);
if (preview && preview.data) {
preview = preview.data;
preview.preview = true;
preview.subtype = 'url';
options = { ...options, ...preview };
}
}
}
let buttonOptions = {};
if (options.buttons) {
let caption;
if (options.buttons.type === 'chat') {
content = options.buttons.body;
caption = content;
} else {
caption = options.caption ? options.caption : ' '; // Caption can't be empty
}
buttonOptions = {
productHeaderImageRejected: false,
isFromTemplate: false,
isDynamicReplyButtonsMsg: true,
title: options.buttons.title
? options.buttons.title
: undefined,
footer: options.buttons.footer
? options.buttons.footer
: undefined,
dynamicReplyButtons: options.buttons.buttons,
replyButtons: options.buttons.buttons,
caption: caption,
};
delete options.buttons;
}
let listOptions = {};
if (options.list) {
listOptions = {
type: 'list',
list: {
title: options.list.title,
description: options.list.description,
buttonText: options.list.buttonText,
footerText: options.list.footer,
listType: 1,
sections: options.list.sections,
},
body: options.list.description,
};
delete options.list;
}
let interactiveOptions = {};
if (options.interactive) {
const failInteractive = (message) => {
throw new Error(`[INTERACTIVE_INVALID_INPUT] ${message}`);
};
const interactive = options.interactive;
const hsmGating = window.require('WAWebHsmGatingUtils');
const interactiveUtils = window.require(
'WAWebBizTemplateAndInteractiveMessagesUtils',
);
const interactiveTypes = window.require(
'WAWebInteractiveMessageType',
);
const generator = window.require(
'WAWebMessagePluginGenerateProtobuf',
);
if (!hsmGating.interactiveNativeFlowMessagesEnabled()) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] Native-flow messages are disabled by the active WhatsApp Web runtime.',
);
}
if (
!interactive ||
interactive.type !== 'interactive' ||
interactive.interactiveType !== 'native_flow' ||
interactive.messageVersion !== 1 ||
interactive.messageParamsJson !== '{}' ||
!Array.isArray(interactive.buttons) ||
interactive.buttons.length < 1 ||
interactive.buttons.length > 3
) {
failInteractive('Invalid interactive-message envelope.');
}
if (
typeof interactive.body !== 'string' ||
!interactive.body.trim() ||
typeof interactive.fallbackText !== 'string' ||
!interactive.fallbackText.trim()
) {
failInteractive('Body and fallbackText are required.');
}
const supportedButtonNames = new Set(
interactiveUtils.supportedNativeFlowButtonNamesForInteractiveMsg ||
[],
);
const allowedButtonNames = new Set([
'quick_reply',
'cta_url',
'cta_call',
'cta_copy',
]);
const normalizedButtons = interactive.buttons.map((button) => {
if (
!button ||
!allowedButtonNames.has(button.name) ||
!supportedButtonNames.has(button.name) ||
typeof button.buttonParamsJson !== 'string' ||
new TextEncoder().encode(button.buttonParamsJson).length >
4096
) {
failInteractive('Unsupported or malformed button.');
}
let params;
try {
params = JSON.parse(button.buttonParamsJson);
} catch {
failInteractive(
'buttonParamsJson must contain valid JSON.',
);
}
if (
!params ||
Array.isArray(params) ||
typeof params !== 'object' ||
Object.keys(params).some((key) =>
['__proto__', 'prototype', 'constructor'].includes(key),
)
) {
failInteractive('Unsafe button parameters.');
}
const allowedParamKeys = {
quick_reply: new Set(['display_text', 'id']),
cta_url: new Set(['display_text', 'url']),
cta_call: new Set(['display_text', 'phone_number']),
cta_copy: new Set(['display_text', 'copy_code']),
}[button.name];
if (
Object.keys(params).some(
(key) => !allowedParamKeys.has(key),
) ||
typeof params.display_text !== 'string' ||
!params.display_text.trim()
) {
failInteractive('Unexpected button parameters.');
}
if (
button.name === 'quick_reply' &&
(typeof params.id !== 'string' || !params.id)
) {
failInteractive('Quick replies require an id.');
}
if (button.name === 'cta_url') {
let target;
try {
target = new URL(params.url);
} catch {
failInteractive(
'CTA URL must be an absolute HTTPS URL.',
);
}
if (
target.protocol !== 'https:' ||
target.username ||
target.password
) {
failInteractive(
'CTA URL must be an absolute HTTPS URL.',
);
}
}
if (
button.name === 'cta_call' &&
!/^\+[1-9]\d{7,14}$/u.test(params.phone_number || '')
) {
failInteractive('CTA call requires an E.164 phone number.');
}
if (
button.name === 'cta_copy' &&
(typeof params.copy_code !== 'string' ||
!params.copy_code.trim())
) {
failInteractive('CTA copy requires a non-empty copy code.');
}
return {
name: button.name,
buttonParamsJson: button.buttonParamsJson,
};
});
const hasQuickReplies = normalizedButtons.some(
(button) => button.name === 'quick_reply',
);
if (
hasQuickReplies &&
normalizedButtons.some(
(button) => button.name !== 'quick_reply',
)
) {
failInteractive(
'Quick replies cannot be mixed with CTA buttons.',
);
}
if (
interactiveUtils.buttonsViolateButtonImprovementsConstraints(
normalizedButtons.map((button) => ({
nativeFlowButton: button,
})),
)
) {
failInteractive(
'Buttons violate the active runtime constraints.',
);
}
const nativeFlowName =
interactiveUtils.getNativeFlowNameByButtonName(
normalizedButtons[0].name,
);
if (typeof nativeFlowName !== 'string' || !nativeFlowName) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The runtime cannot map this button type.',
);
}
if (
interactive.nativeFlowName &&
interactive.nativeFlowName !== nativeFlowName
) {
failInteractive(
'nativeFlowName does not match the button type.',
);
}
let interactiveHeader;
const header = interactive.header;
const hasMedia = Boolean(interactiveMediaPrep);
if (header || hasMedia) {
interactiveHeader = {
title: header?.title,
subtitle: header?.subtitle,
hasMediaAttachment: hasMedia,
};
}
if (hasMedia) {
const mediaTypeByMessageType = {
image: 'IMAGE',
video: 'VIDEO',
document: 'DOCUMENT',
};
const mediaType = mediaTypeByMessageType[mediaOptions.type];
if (!mediaType) {
failInteractive(
'Header media must resolve to image, video or document.',
);
}
const headerMediaTypes = window.require(
'WAWebInteractiveMessageHeaderMediaType',
).InteractiveMessageHeaderMediaType;
if (headerMediaTypes[mediaType] !== mediaType) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] Header media type is unavailable in this runtime.',
);
}
interactiveHeader.mediaType = mediaType;
}
interactiveOptions = {
type: 'interactive',
nativeFlowInteractiveMsg: true,
caption: interactive.body,
footer: interactive.footer,
interactiveType: interactiveTypes.NATIVE_FLOW,
nativeFlowName,
interactiveHeader,
interactivePayload: {
messageVersion: 1,
messageParamsJson: interactive.messageParamsJson || '{}',
buttons: normalizedButtons,
},
};
if (!hasMedia) {
let generated;
try {
generated = generator.generateProtobuf({
json: interactiveOptions,
contextInfo: undefined,
mediaMetadata: undefined,
});
} catch (error) {
throw new Error(
`[INTERACTIVE_UNAVAILABLE] The runtime generator rejected the message: ${error.message}`,
);
}
if (!generated?.interactiveMessage?.nativeFlowMessage) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The runtime did not generate a native-flow message.',
);
}
}
// Media message generators read `body` as the base64 thumbnail.
// The visible interactive copy already lives in `caption`; replacing
// the media preview with that copy makes encodeBytes fail before the
// fanout stanza is built.
content = hasMedia ? mediaOptions.preview : interactive.body;
delete options.interactive;
}
const botOptions = {};
if (options.invokedBotWid) {
botOptions.messageSecret = window.crypto.getRandomValues(
new Uint8Array(32),
);
botOptions.botMessageSecret = await window
.require('WAWebBotMessageSecret')
.genBotMsgSecretFromMsgSecret(botOptions.messageSecret);
botOptions.invokedBotWid = window
.require('WAWebWidFactory')
.createWid(options.invokedBotWid);
botOptions.botPersonaId = window
.require('WAWebBotProfileCollection')
.BotProfileCollection.get(options.invokedBotWid).personaId;
delete options.invokedBotWid;
}
const { getMaybeMeLidUser, getMaybeMePnUser } = window.require(
'WAWebUserPrefsMeUser',
);
const lidUser = getMaybeMeLidUser();
const meUser = getMaybeMePnUser();
const newId = await window.require('WAWebMsgKey').newId();
let from = chat.id.isLid() ? lidUser : meUser;
let participant;
if (typeof chat.id?.isGroup === 'function' && chat.id.isGroup()) {
from =
chat.groupMetadata && chat.groupMetadata.isLidAddressingMode
? lidUser
: meUser;
participant = window
.require('WAWebWidFactory')
.asUserWidOrThrow(from);
}
if (typeof chat.id?.isStatus === 'function' && chat.id.isStatus()) {
participant = window
.require('WAWebWidFactory')
.asUserWidOrThrow(from);
}
const newMsgKey = new (window.require('WAWebMsgKey'))({
from: from,
to: chat.id,
id: newId,
participant: participant,
selfDir: 'out',
});
const extraOptions = options.extraOptions || {};
delete options.extraOptions;
const ephemeralFields = window
.require('WAWebGetEphemeralFieldsMsgActionsUtils')
.getEphemeralFields(chat);
const message = {
...options,
id: newMsgKey,
ack: 0,
body: content,
from: from,
to: chat.id,
local: true,
self: 'out',
t: parseInt(new Date().getTime() / 1000),
isNewMsg: true,
type: 'chat',
...ephemeralFields,
...mediaOptions,
...(mediaOptions.toJSON ? mediaOptions.toJSON() : {}),
...quotedMsgOptions,
...locationOptions,
...pollOptions,
...eventOptions,
...vcardOptions,
...buttonOptions,
...listOptions,
...botOptions,
...extraOptions,
...interactiveOptions,
};
// Bot's won't reply if canonicalUrl is set (linking)
if (Object.keys(botOptions).length > 0) {
delete message.canonicalUrl;
}
const isInteractive = interactiveOptions.type === 'interactive';
const isLegacyList = listOptions.type === 'list';
const hasGuardedFanout = isInteractive || isLegacyList;
const confirmInteractiveAck = async (sentMessage) => {
const deadline =
Date.now() + INTERACTIVE_ACK_CONFIRMATION_TIMEOUT_MS;
let ack = Number(sentMessage?.ack);
while (ack !== -1 && !(ack >= 1) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50));
ack = Number(sentMessage?.ack);
}
if (ack === -1) {
throw new Error(
'[INTERACTIVE_REJECTED] WhatsApp Web returned ACK_ERROR.',
);
}
if (!(ack >= 1)) {
throw new Error(
'[INTERACTIVE_REJECTED] WhatsApp Web did not confirm the interactive message before the ACK deadline.',
);
}
};
if (isInteractive && interactiveMediaPrep) {
const fanoutReservation = reserveInteractiveFanout(
newId,
chat.id,
'native_flow',
);
const mediaSendOptions = {
...options,
...botOptions,
...extraOptions,
...ephemeralFields,
type: 'interactive',
useBasePropsType: true,
addEvenWhilePreparing: false,
caption: interactiveOptions.caption,
footer: interactiveOptions.footer,
interactiveType: interactiveOptions.interactiveType,
nativeFlowName: interactiveOptions.nativeFlowName,
nativeFlowInteractiveMsg: true,
interactiveHeader: interactiveOptions.interactiveHeader,
interactivePayload: interactiveOptions.interactivePayload,
// MediaPrep treats a top-level `quotedMsg` as a WA model and
// calls msgContextInfo() on it. At this point the quote has
// already been materialized, so pass those base-prop fields
// through the final product options spread instead.
productMsgOptions: {
...quotedMsgOptions,
id: newMsgKey,
},
};
for (const key of [
'sendMediaAsDocument',
'sendMediaAsHd',
'sendVideoAsGif',
'sendAudioAsVoice',
'isViewOnce',
'waitUntilMsgSent',
]) {
delete mediaSendOptions[key];
}
try {
const sendResult = await interactiveMediaPrep.sendToChat({
chat,
earlyUpload: undefined,
options: mediaSendOptions,
});
if (sendResult?.messageSendResult !== 'OK') {
throw (
fanoutReservation.reservation.error ||
new Error(
'[INTERACTIVE_REJECTED] WhatsApp Web returned a non-OK media send result.',
)
);
}
const sentMessage = sendResult.msg;
if (
!sentMessage ||
String(sentMessage.id?.id || '') !== String(newId)
) {
throw new Error(
'[INTERACTIVE_UNAVAILABLE] The runtime media send did not preserve the reserved message id.',
);
}
if (!fanoutReservation.reservation.applied) {
throw (
fanoutReservation.reservation.error ||
new Error(
'[INTERACTIVE_UNAVAILABLE] The pinned fanout adapter did not process the media message.',
)
);
}
await confirmInteractiveAck(sentMessage);
return sentMessage;
} finally {
fanoutReservation.release();
}
}
if (isChannel) {
const msg = new (window.require('WAWebCollections').Msg.modelClass)(
message,
);
const msgDataFromMsgModel = window
.require('WAWebMsgDataFromModel')
.msgDataFromMsgModel(msg);
const isMedia = Object.keys(mediaOptions).length > 0;
await window
.require('WAWebNewsletterUpdateMsgsRecordsJob')
.addNewsletterMsgsRecords([msgDataFromMsgModel]);
chat.msgs.add(msg);
chat.t = msg.t;
const sendChannelMsgResponse = await window
.require('WAWebNewsletterSendMessageJob')
.sendNewsletterMessageJob({
msg: msg,
type:
message.type === 'chat'
? 'text'
: isMedia
? 'media'
: 'pollCreation',
newsletterJid: chat.id.toJid(),
...(isMedia
? {
mediaMetadata: (() => {
try {
const metadata =
window.require('WAWebMediaMetadata');
if (
typeof metadata?.mediaMetadata ===
'function'
) {
return metadata.mediaMetadata(msg);
}
} catch (ignoredError) {
// Older cached builds exposed avParams on
// the model instead of the module helper.
}
return typeof msg.avParams === 'function'
? msg.avParams()
: undefined;
})(),
mediaHandle: isMedia
? mediaOptions.mediaHandle
: null,
}
: {}),
});
if (sendChannelMsgResponse.success) {
msg.t = sendChannelMsgResponse.ack.t;
msg.serverId = sendChannelMsgResponse.serverId;
}
msg.updateAck(1, true);
await window
.require('WAWebNewsletterUpdateMsgsRecordsJob')
.updateNewsletterMsgRecord(msg);
return msg;
}
if (isStatus) {
const { backgroundColor, fontStyle } = extraOptions;
const isMedia = Object.keys(mediaOptions).length > 0;
const mediaUpdate = (data) =>
window.require('WAWebMediaUpdateMsg')(data, mediaOptions);
const msg = new (window.require('WAWebCollections').Msg.modelClass)(
{
...message,
author: participant ? participant : null,
messageSecret: window.crypto.getRandomValues(
new Uint8Array(32),
),
cannotBeRanked: window
.require('WAWebStatusGatingUtils')
.canCheckStatusRankingPosterGating(),
},
);
// for text only
const statusOptions = {
color:
(backgroundColor &&
window.WWebJS.assertColor(backgroundColor)) ||
0xff7acca5,
font: (fontStyle >= 0 && fontStyle <= 7 && fontStyle) || 0,
text: msg.body,
};
await window
.require('WAWebSendStatusMsgAction')
[
isMedia
? 'sendStatusMediaMsgAction'
: 'sendStatusTextMsgAction'
](...(isMedia ? [msg, mediaUpdate] : [statusOptions]));
return msg;
}
const fanoutReservation = hasGuardedFanout
? reserveInteractiveFanout(
newId,
chat.id,
isLegacyList ? 'legacy_list' : 'native_flow',
)
: null;
try {
const [msgPromise, sendMsgResultPromise] = window
.require('WAWebSendMsgChatAction')
.addAndSendMsgToChat(chat, message);
await msgPromise;
if (options.waitUntilMsgSent || hasGuardedFanout) {
const sendResult = await sendMsgResultPromise;
if (
hasGuardedFanout &&
sendResult?.messageSendResult !== 'OK'
) {
throw (
fanoutReservation.reservation.error ||
new Error(
'[INTERACTIVE_REJECTED] WhatsApp Web returned a non-OK send result.',
)
);
}
if (
!hasGuardedFanout &&
options.waitUntilMsgSent &&
typeof sendResult?.messageSendResult === 'string' &&
sendResult.messageSendResult !== 'OK'
) {
const error = new Error(
`WhatsApp Web rejected the message: ${sendResult.messageSendResult}`,
);
error.code = 'MESSAGE_SEND_FAILED';
error.messageSendResult = sendResult.messageSendResult;
throw error;
}
}
if (hasGuardedFanout && !fanoutReservation.reservation.applied) {
throw (
fanoutReservation.reservation.error ||
new Error(
'[INTERACTIVE_UNAVAILABLE] The pinned fanout adapter did not process the message.',
)
);
}
} finally {
fanoutReservation?.release();
}
const serializedNewMsgKey = window.WWebJS.serializeMessageId(newMsgKey);
const messageStore = window.require('WAWebCollections').Msg;
const sentMessage =
(serializedNewMsgKey
? messageStore.get(serializedNewMsgKey)
: undefined) ||
chat.msgs
?.getModelsArray?.()
.find(
(candidate) =>
window.WWebJS.serializeMessageId(candidate?.id) ===
serializedNewMsgKey || candidate?.id?.id === newId,
);
if (hasGuardedFanout) {
await confirmInteractiveAck(sentMessage);
}
return sentMessage;
};
window.WWebJS.editMessage = async (msg, content, options = {}) => {
const extraOptions = options.extraOptions || {};
delete options.extraOptions;
if (options.mentionedJidList) {
options.mentionedJidList = options.mentionedJidList.map((id) =>
window.require('WAWebWidFactory').createWid(id),
);
options.mentionedJidList = options.mentionedJidList.filter(Boolean);
}
if (options.groupMentions) {
options.groupMentions = options.groupMentions.map((e) => ({
groupSubject: e.subject,
groupJid: window.require('WAWebWidFactory').createWid(e.id),
}));
}
if (options.linkPreview) {
const { findLink } = window.require('WALinkify');
delete options.linkPreview;
const link = findLink(content);
if (link) {
const preview = await window
.require('WAWebLinkPreviewChatAction')
.getLinkPreview(link);
preview.preview = true;
preview.subtype = 'url';
options = { ...options, ...preview };
}
}
const internalOptions = {
...options,
...extraOptions,
};
await window
.require('WAWebSendMessageEditAction')
.sendMessageEdit(msg, content, internalOptions);
const serializedMsgKey = window.WWebJS.serializeMessageId(msg.id);
return serializedMsgKey
? window.require('WAWebCollections').Msg.get(serializedMsgKey)
: undefined;
};
window.WWebJS.toStickerData = async (mediaInfo) => {
if (mediaInfo.mimetype == 'image/webp') return mediaInfo;
const file = window.WWebJS.mediaInfoToFile(mediaInfo);
const webpSticker = await window
.require('WAWebImageUtils')
.toWebpSticker(file);
const webpBuffer = await webpSticker.arrayBuffer();
const data = window.WWebJS.arrayBufferToBase64(webpBuffer);
return {
mimetype: 'image/webp',
data,
};
};
window.WWebJS.processStickerData = async (mediaInfo) => {
if (mediaInfo.mimetype !== 'image/webp')
throw new Error('Invalid media type');
const file = window.WWebJS.mediaInfoToFile(mediaInfo);
let filehash = await window.WWebJS.getFileHash(file);
let mediaKey = await window.WWebJS.generateHash(32);
const controller = new AbortController();
const uploadedInfo = await window
.require('WAWebUploadManager')
.encryptAndUpload({
blob: file,
type: 'sticker',
signal: controller.signal,
mediaKey,
uploadQpl: window
.require('WAWebStartMediaUploadQpl')
.startMediaUploadQpl({
entryPoint: 'MediaUpload',
}),
});
const stickerInfo = {
...uploadedInfo,
clientUrl: uploadedInfo.url,
deprecatedMms3Url: uploadedInfo.url,
uploadhash: uploadedInfo.encFilehash,
size: file.size,
type: 'sticker',
filehash,
};
return stickerInfo;
};
window.WWebJS.processMediaData = async (
mediaInfo,
{
forceSticker,
forceGif,
forceVoice,
forceDocument,
forceMediaHd,
sendToChannel,
sendToStatus,
},
) => {
const file = window.WWebJS.mediaInfoToFile(mediaInfo);
const OpaqueData = window.require('WAWebMediaOpaqueData');
const opaqueData = await OpaqueData.createFromData(
file,
mediaInfo.mimetype,
);
const mediaParams = {
asSticker: forceSticker,
asGif: forceGif,
isPtt: forceVoice,
asDocument: forceDocument,
};
if (forceMediaHd && file.type.indexOf('image/') === 0) {
mediaParams.maxDimension = 2560;
}
const mediaPrep = window
.require('WAWebPrepRawMedia')
.prepRawMedia(opaqueData, mediaParams);
const mediaData = await mediaPrep.waitForPrep();
const mediaObject = window
.require('WAWebMediaStorage')
.getOrCreateMediaObject(mediaData.filehash);
const mediaType = window.require('WAWebMmsMediaTypes').msgToMediaType({
type: mediaData.type,
isGif: mediaData.isGif,
isNewsletter: sendToChannel,
});
if (!mediaData.filehash) {
throw new Error('media-fault: sendToChat filehash undefined');
}
if (
(forceVoice && mediaData.type === 'ptt') ||
(sendToStatus && mediaData.type === 'audio')
) {
const waveform = mediaObject.contentInfo.waveform;
mediaData.waveform =
waveform || (await window.WWebJS.generateWaveform(file));
}
if (!(mediaData.mediaBlob instanceof OpaqueData)) {
mediaData.mediaBlob = await OpaqueData.createFromData(
mediaData.mediaBlob,
mediaData.mediaBlob.type,
);
}
mediaData.renderableUrl = mediaData.mediaBlob.url();
mediaObject.consolidate(mediaData.toJSON());
mediaData.mediaBlob.autorelease();
const shouldUseMediaCache = window
.require('WAWebMediaDataUtils')
.shouldUseMediaCache(
window.require('WAWebMmsMediaTypes').castToV4(mediaObject.type),
);
if (shouldUseMediaCache && mediaData.mediaBlob instanceof OpaqueData) {
const formData = mediaData.mediaBlob.formData();
window
.require('WAWebMediaInMemoryBlobCache')
.InMemoryMediaBlobCache.put(mediaObject.filehash, formData);
}
const dataToUpload = {
mimetype: mediaData.mimetype,
mediaObject,
mediaType,
...(sendToChannel
? {
calculateToken: window.require('WAMediaCalculateFilehash')
.getRandomFilehash,
}
: {}),
};
const { uploadMedia, uploadUnencryptedMedia } = window.require(
'WAWebMediaMmsV4Upload',
);
const uploadedMedia = !sendToChannel
? await uploadMedia(dataToUpload)
: await uploadUnencryptedMedia(dataToUpload);
const mediaEntry = uploadedMedia.mediaEntry;
if (!mediaEntry) {
throw new Error('upload failed: media entry was not created');
}
mediaData.set({
clientUrl: mediaEntry.mmsUrl,
deprecatedMms3Url: mediaEntry.deprecatedMms3Url,
directPath: mediaEntry.directPath,
mediaKey: mediaEntry.mediaKey,
mediaKeyTimestamp: mediaEntry.mediaKeyTimestamp,
filehash: mediaObject.filehash,
encFilehash: mediaEntry.encFilehash,
uploadhash: mediaEntry.uploadHash,
size: mediaObject.size,
streamingSidecar: mediaEntry.sidecar,
firstFrameSidecar: mediaEntry.firstFrameSidecar,
mediaHandle: sendToChannel ? mediaEntry.handle : null,
});
return mediaData;
};
const USER_JID_SUFFIX = '@s.whatsapp.net';
const isUserJid = (jid) =>
typeof jid === 'string' && jid.toLowerCase().endsWith(USER_JID_SUFFIX);
const toNonEmptyStringOrNull = (value) => {
if (typeof value !== 'string') return null;
const normalized = value.trim();
if (!normalized || normalized === '[object Object]') return null;
return normalized;
};
const serializeWidLike = (widLike) => {
if (widLike == null) return null;
if (typeof widLike === 'string') {
return toNonEmptyStringOrNull(widLike);
}
if (typeof widLike === 'object') {
const serialized =
toNonEmptyStringOrNull(widLike._serialized) ||
toNonEmptyStringOrNull(widLike.$1);
if (serialized) return serialized;
}
if (typeof widLike === 'object') {
const user = toNonEmptyStringOrNull(widLike.user);
const server = toNonEmptyStringOrNull(widLike.server);
if (user && server) return `${user}@${server.replace(/^@/, '')}`;
}
if (typeof widLike.toString === 'function') {
const serialized = widLike.toString();
return toNonEmptyStringOrNull(serialized);
}
return null;
};
const normalizeFromMe = (fromMe) => {
if (typeof fromMe === 'boolean') return fromMe;
if (fromMe === 1 || fromMe === '1' || fromMe === 'true') return true;
if (fromMe === 0 || fromMe === '0' || fromMe === 'false') return false;
return null;
};
const serializeMessageId = (messageId) => {
const direct = toNonEmptyStringOrNull(messageId);
if (direct) return direct;
if (!messageId || typeof messageId !== 'object') return null;
const serialized =
toNonEmptyStringOrNull(messageId._serialized) ||
toNonEmptyStringOrNull(messageId.$1);
if (serialized) return serialized;
if (typeof messageId.toString === 'function') {
const stringified = toNonEmptyStringOrNull(messageId.toString());
if (stringified) return stringified;
}
const fromMe = normalizeFromMe(messageId.fromMe);
const remote = serializeWidLike(messageId.remote);
const id = toNonEmptyStringOrNull(messageId.id);
if (fromMe === null || remote === null || id === null) return null;
const participant = serializeWidLike(messageId.participant);
return participant
? `${fromMe}_${remote}_${id}_${participant}`
: `${fromMe}_${remote}_${id}`;
};
window.WWebJS.serializeMessageId = serializeMessageId;
window.WWebJS.serializeWid = serializeWidLike;
const getMessageIdNameFromContact = (contact) => {
if (!contact || typeof contact !== 'object') return null;
try {
const ContactMethods = window.require('WAWebContactGetters');
const name = toNonEmptyStringOrNull(
ContactMethods?.getName?.(contact),
);
if (name) return name;
const pushname = toNonEmptyStringOrNull(
ContactMethods?.getPushname?.(contact),
);
if (pushname) return pushname;
const verifiedName = toNonEmptyStringOrNull(
ContactMethods?.getVerifiedName?.(contact),
);
if (verifiedName) return verifiedName;
} catch {
// Fallback to direct model fields below.
}
const name = toNonEmptyStringOrNull(contact.name);
if (name) return name;
const pushname = toNonEmptyStringOrNull(contact.pushname);
if (pushname) return pushname;
return toNonEmptyStringOrNull(contact.verifiedName);
};
window.WWebJS.getPreferredRemoteJid = (widLike) => {
if (widLike == null) return null;
let wid = widLike;
if (typeof wid === 'string') {
try {
wid = window.require('WAWebWidFactory').createWid(wid);
} catch {
wid = widLike;
}
}
if (wid && typeof wid === 'object' && wid.server === 'lid') {
try {
const apiContact = window.require('WAWebApiContact');
const phoneWid = apiContact?.getPhoneNumber?.(wid);
if (phoneWid) {
wid = phoneWid;
}
} catch {
// Keep the original wid to keep a deterministic null fallback.
}
}
const serializedWid = serializeWidLike(wid);
if (!serializedWid) return null;
try {
const userWid =
typeof wid === 'object' && wid?._serialized
? wid
: window
.require('WAWebWidFactory')
.createWid(serializedWid);
const userJid = window
.require('WAWebWidToJid')
?.widToUserJid?.(userWid);
const serializedUserJid = serializeWidLike(userJid);
if (isUserJid(serializedUserJid)) {
return serializedUserJid;
}
} catch {
// Keep strict behavior: unresolved values return null.
}
return isUserJid(serializedWid) ? serializedWid : null;
};
window.WWebJS.getMessageIdNameFromRemoteJid = (remoteJid) => {
if (!isUserJid(remoteJid)) return null;
try {
const { Contact: ContactCollection } =
window.require('WAWebCollections') || {};
if (
!ContactCollection ||
typeof ContactCollection.get !== 'function'
) {
return null;
}
let WidFactory = null;
try {
WidFactory = window.require('WAWebWidFactory');
} catch {
WidFactory = null;
}
let ApiContact = null;
try {
ApiContact = window.require('WAWebApiContact');
} catch {
ApiContact = null;
}
const candidateWids = [];
const candidateWidIds = new Set();
const candidateIds = new Set();
const enqueueCandidateWid = (wid) => {
if (!wid || typeof wid !== 'object') return;
const serialized = serializeWidLike(wid);
if (serialized && candidateWidIds.has(serialized)) return;
if (serialized) {
candidateWidIds.add(serialized);
}
candidateWids.push(wid);
};
const addCandidate = (widLike) => {
const serialized = serializeWidLike(widLike);
if (!serialized) return;
candidateIds.add(serialized);
if (serialized.toLowerCase().endsWith('@s.whatsapp.net')) {
candidateIds.add(
serialized.replace(/@s\.whatsapp\.net$/i, '@c.us'),
);
} else if (serialized.toLowerCase().endsWith('@c.us')) {
candidateIds.add(
serialized.replace(/@c\.us$/i, '@s.whatsapp.net'),
);
}
if (widLike && typeof widLike === 'object') {
enqueueCandidateWid(widLike);
}
};
addCandidate(remoteJid);
try {
addCandidate(WidFactory?.createWid?.(remoteJid));
} catch {
// Keep best-effort behavior when WidFactory cannot parse.
}
for (let index = 0; index < candidateWids.length; index++) {
const wid = candidateWids[index];
try {
addCandidate(ApiContact?.getAlternateUserWid?.(wid));
addCandidate(ApiContact?.getPhoneNumber?.(wid));
} catch {
// Keep deterministic null fallback.
}
}
for (const candidate of candidateIds) {
let contact = null;
try {
contact = ContactCollection.get(candidate);
} catch {
contact = null;
}
if (!contact) {
try {
const candidateWid = WidFactory?.createWid?.(candidate);
if (candidateWid) {
contact = ContactCollection.get(candidateWid);
}
} catch {
contact = null;
}
}
const name = getMessageIdNameFromContact(contact);
if (name) return name;
}
} catch {
// Keep strict behavior: unresolved values return null.
}
return null;
};
window.WWebJS.normalizeMessageId = (messageId) => {
if (!messageId || typeof messageId !== 'object') return messageId;
const remote = serializeWidLike(messageId.remote);
const serialized = serializeMessageId(messageId);
const remoteJid = window.WWebJS.getPreferredRemoteJid(
messageId.remoteJid ?? messageId.remote ?? remote ?? null,
);
const inputName = toNonEmptyStringOrNull(messageId.name);
const name = remoteJid
? window.WWebJS.getMessageIdNameFromRemoteJid(remoteJid) ||
inputName ||
null
: null;
return {
...messageId,
...(remote !== null ? { remote } : {}),
...(serialized !== null ? { _serialized: serialized } : {}),
remoteJid,
name,
};
};
window.WWebJS.getMessageModel = (message) => {
const msg = message.serialize();
const { findLinks } = window.require('WALinkify');
msg.isEphemeral = message.isEphemeral;
msg.isStatusV3 = message.isStatusV3;
msg.links = findLinks(
message.mediaObject ? message.caption : message.body,
).map((link) => ({
link: link.href,
isSuspicious: Boolean(
link.suspiciousCharacters && link.suspiciousCharacters.size,
),
}));
if (msg.buttons) {
msg.buttons = msg.buttons.serialize();
}
if (msg.dynamicReplyButtons) {
msg.dynamicReplyButtons = JSON.parse(
JSON.stringify(msg.dynamicReplyButtons),
);
}
if (msg.replyButtons) {
msg.replyButtons = JSON.parse(JSON.stringify(msg.replyButtons));
}
if (msg.id) {
msg.id = window.WWebJS.normalizeMessageId(msg.id);
}
const mentionedJidList = Array.isArray(msg.mentionedJidList)
? msg.mentionedJidList
: [];
msg.mentionedJidList = mentionedJidList
.map(serializeWidLike)
.filter(Boolean);
const nestedMessageIdFields = [
'latestEditMsgKey',
'protocolMessageKey',
'msgKey',
'parentMsgKey',
'pinParentKey',
];
for (const field of nestedMessageIdFields) {
if (msg[field] && typeof msg[field] === 'object') {
msg[field] = window.WWebJS.normalizeMessageId(msg[field]);
}
}
delete msg.pendingAckUpdate;
return msg;
};
window.WWebJS.getChat = async (chatId, { getAsModel = true } = {}) => {
const isChannel = /@\w*newsletter\b/.test(chatId);
const chatWid = window.require('WAWebWidFactory').createWid(chatId);
let chat;
if (isChannel) {
try {
chat = window
.require('WAWebCollections')
.WAWebNewsletterCollection.get(chatId);
if (!chat) {
await window
.require('WAWebLoadNewsletterPreviewChatAction')
.loadNewsletterPreviewChat(chatId);
chat = await window
.require('WAWebCollections')
.WAWebNewsletterCollection.find(chatWid);
}
} catch (ignoredError) {
chat = null;
}
} else {
chat =
window.require('WAWebCollections').Chat.get(chatWid) ||
(
await window
.require('WAWebFindChatAction')
.findOrCreateLatestChat(chatWid)
)?.chat;
}
return getAsModel && chat
? await window.WWebJS.getChatModel(chat, { isChannel: isChannel })
: chat;
};
window.WWebJS.getChannelMetadata = async (inviteCode) => {
const role = window
.require('WAWebNewsletterModelUtils')
.getRoleByIdentifier(inviteCode);
const response = await window
.require('WAWebNewsletterMetadataQueryJob')
.queryNewsletterMetadataByInviteCode(inviteCode, role);
const picUrl =
response.newsletterPictureMetadataMixin?.picture[0]
?.queryPictureDirectPathOrEmptyResponseMixinGroup.value
.directPath;
return {
id: response.idJid,
createdAtTs:
response.newsletterCreationTimeMetadataMixin.creationTimeValue,
titleMetadata: {
title: response.newsletterNameMetadataMixin.nameElementValue,
updatedAtTs:
response.newsletterNameMetadataMixin.nameUpdateTime,
},
descriptionMetadata: {
description:
response.newsletterDescriptionMetadataMixin
.descriptionQueryDescriptionResponseMixin.elementValue,
updatedAtTs:
response.newsletterDescriptionMetadataMixin
.descriptionQueryDescriptionResponseMixin.updateTime,
},
inviteLink: `https://whatsapp.com/channel/${response.newsletterInviteLinkMetadataMixin.inviteCode}`,
membershipType: role,
stateType: response.newsletterStateMetadataMixin.stateType,
pictureUrl: picUrl ? `https://pps.whatsapp.net${picUrl}` : null,
subscribersCount:
response.newsletterSubscribersMetadataMixin.subscribersCount,
isVerified:
response.newsletterVerificationMetadataMixin
.verificationState === 'verified',
};
};
window.WWebJS.getChats = async () => {
const chats = window.require('WAWebCollections').Chat.getModelsArray();
const chatPromises = chats.map((chat) =>
window.WWebJS.getChatModel(chat),
);
return await Promise.all(chatPromises);
};
window.WWebJS.getChannels = async () => {
const channels = window
.require('WAWebCollections')
.WAWebNewsletterCollection.getModelsArray();
const channelPromises = channels?.map((channel) =>
window.WWebJS.getChatModel(channel, { isChannel: true }),
);
return await Promise.all(channelPromises);
};
window.WWebJS.getChatModel = async (chat, { isChannel = false } = {}) => {
if (!chat) return null;
const model = chat.serialize();
model.isGroup = false;
model.isMuted = chat.mute?.expiration !== 0;
if (isChannel) {
model.isChannel = window
.require('WAWebChatGetters')
.getIsNewsletter(chat);
} else {
model.formattedTitle = chat.formattedTitle;
}
if (chat.groupMetadata) {
model.isGroup = true;
const chatWid = window
.require('WAWebWidFactory')
.createWid(window.WWebJS.serializeWid(chat.id));
const groupMetadata =
window.require('WAWebCollections').GroupMetadata ||
window.require('WAWebCollections').WAWebGroupMetadataCollection;
await groupMetadata.update(chatWid);
const { toPn } = window.require('WAWebLidMigrationUtils');
const serializedMetadata = chat.groupMetadata.serialize();
for (const p of serializedMetadata.participants || []) {
p.id = toPn(p.id) ?? p.id;
}
model.groupMetadata = serializedMetadata;
model.isReadOnly = chat.groupMetadata.announce;
}
if (chat.newsletterMetadata) {
const newsletterMetadata =
window.require('WAWebCollections')
.NewsletterMetadataCollection ||
window.require('WAWebCollections')
.WAWebNewsletterMetadataCollection;
await newsletterMetadata.update(chat.id);
model.channelMetadata = chat.newsletterMetadata.serialize();
model.channelMetadata.createdAtTs =
chat.newsletterMetadata.creationTime;
}
model.lastMessage = null;
if (model.msgs && model.msgs.length) {
const serializedLastReceivedKey = window.WWebJS.serializeMessageId(
chat.lastReceivedKey,
);
const lastMessage = serializedLastReceivedKey
? window
.require('WAWebCollections')
.Msg.get(serializedLastReceivedKey) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([serializedLastReceivedKey])
)?.messages?.[0]
: null;
lastMessage &&
(model.lastMessage =
window.WWebJS.getMessageModel(lastMessage));
}
delete model.msgs;
delete model.msgUnsyncedButtonReplyMsgs;
delete model.unsyncedButtonReplies;
return model;
};
window.WWebJS.getContactModel = (contact) => {
let res = contact.serialize();
const wid = window
.require('WAWebWidFactory')
.createWidFromWidLike(contact.id);
if (wid.isLid() && contact.phoneNumber) {
res.id = contact.phoneNumber;
}
res.isBusiness =
contact.isBusiness === undefined ? false : contact.isBusiness;
if (contact.businessProfile) {
res.businessProfile = contact.businessProfile.serialize();
}
res.isBlocked = contact.isContactBlocked;
if (!res.isBlocked) {
const alt = window
.require('WAWebApiContact')
.getAlternateUserWid(wid);
if (alt) {
res.isBlocked = !!window
.require('WAWebCollections')
.Blocklist.get(alt);
}
}
const ContactMethods = window.require('WAWebContactGetters');
res.isMe = ContactMethods.getIsMe(contact);
res.isUser = ContactMethods.getIsUser(contact);
res.isGroup = ContactMethods.getIsGroup(contact);
res.isWAContact = ContactMethods.getIsWAContact(contact);
res.userid = ContactMethods.getUserid(contact);
res.verifiedName = ContactMethods.getVerifiedName(contact);
res.verifiedLevel = ContactMethods.getVerifiedLevel(contact);
res.statusMute = ContactMethods.getStatusMute(contact);
res.name = ContactMethods.getName(contact);
res.shortName = ContactMethods.getShortName(contact);
res.pushname = ContactMethods.getPushname(contact);
const { getIsMyContact } = window.require(
'WAWebFrontendContactGetters',
);
res.isMyContact = getIsMyContact(contact);
res.isEnterprise = ContactMethods.getIsEnterprise(contact);
return res;
};
window.WWebJS.getContact = async (contactId) => {
const contactWid = window
.require('WAWebWidFactory')
.createWid(contactId);
const contact = await window
.require('WAWebCollections')
.Contact.find(contactWid);
if (contact.isBusiness || contact.isEnterprise) {
const bizProfile = await window
.require('WAWebCollections')
.BusinessProfile.find(contactWid);
bizProfile.profileOptions && (contact.businessProfile = bizProfile);
}
return window.WWebJS.getContactModel(contact);
};
window.WWebJS.getContacts = () => {
const contacts = window
.require('WAWebCollections')
.Contact.getModelsArray();
return Promise.all(
contacts.map(async (contact) => {
if (contact.isBusiness || contact.isEnterprise) {
await window
.require('WAWebCollections')
.BusinessProfile.find(contact.id)
.catch(() => {});
}
return window.WWebJS.getContactModel(contact);
}),
);
};
window.WWebJS.mediaInfoToFile = ({ data, mimetype, filename }) => {
const binaryData = window.atob(data);
const buffer = new ArrayBuffer(binaryData.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < binaryData.length; i++) {
view[i] = binaryData.charCodeAt(i);
}
const blob = new Blob([buffer], { type: mimetype });
return new File([blob], filename, {
type: mimetype,
lastModified: Date.now(),
});
};
/**
* Resolves the media blob and metadata for a message.
* Shared by downloadMedia and downloadMediaStream.
* @param {string} msgId
* @returns {Promise<{blob: Blob, mimetype: string, filename: string, filesize: number}|null>}
*/
window.WWebJS.resolveMediaBlob = async (msgId) => {
const { Msg } = window.require('WAWebCollections');
const msg =
Msg.get(msgId) ||
(await Msg.getMessagesById([msgId]))?.messages?.[0];
if (
!msg ||
!msg.mediaData ||
msg.mediaData.mediaStage === 'REUPLOADING'
) {
return null;
}
// Use WA Web media type mapper, including newsletter-* media types.
const mediaType = window
.require('WAWebMmsMediaTypes')
.getMsgMediaType(msg);
const needsCrypto = window
.require('WAWebMediaCryptoEligibilityUtils')
.isMediaCryptoExpectedForMediaType(mediaType);
const canDirectDownload = needsCrypto
? msg.directPath && msg.mediaKey
: !!msg.directPath;
const isNonChatJid = /@(broadcast|newsletter)\b/.test(
msg.id.remote._serialized || msg.id.remote,
);
if (!isNonChatJid) {
// Always call internal downloadMedia for regular chats; cache eviction
// can leave stage=RESOLVED with empty InMemoryMediaBlobCache.
await msg.downloadMedia({
downloadEvenIfExpensive: true,
rmrReason: 1,
isUserInitiated: true,
});
} else if (!canDirectDownload) {
// For status/newsletter messages, internal resolve can fail; without a
// direct download path, media is unavailable.
return null;
}
if (
msg.mediaData.mediaStage.includes('ERROR') ||
msg.mediaData.mediaStage === 'FETCHING'
) {
return null;
}
const cached = window
.require('WAWebMediaInMemoryBlobCache')
.InMemoryMediaBlobCache.get(msg.mediaObject?.filehash);
let blob;
if (cached) {
blob = cached;
} else if (msg.mediaObject?.mediaBlob) {
blob = msg.mediaObject.mediaBlob.forceToBlob();
} else if (canDirectDownload) {
try {
const mockQpl = {
addAnnotations: function () {
return this;
},
addPoint: function () {
return this;
},
};
const mediaData = await window
.require('WAWebDownloadManager')
.downloadManager.downloadAndMaybeDecrypt({
directPath: msg.directPath,
encFilehash: msg.encFilehash,
filehash: msg.filehash,
mediaKey: msg.mediaKey,
mediaKeyTimestamp: msg.mediaKeyTimestamp,
type: mediaType,
signal: new AbortController().signal,
downloadQpl: mockQpl,
});
blob = new Blob([mediaData], { type: msg.mimetype });
} catch (ignoredError) {
blob = null;
}
}
if (!blob) return null;
return {
blob,
mimetype: msg.mimetype,
filename: msg.filename,
filesize: msg.size,
};
};
window.WWebJS.arrayBufferToBase64 = (arrayBuffer) => {
let binary = '';
const bytes = new Uint8Array(arrayBuffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
};
window.WWebJS.arrayBufferToBase64Async = (arrayBuffer) =>
new Promise((resolve, reject) => {
const blob = new Blob([arrayBuffer], {
type: 'application/octet-stream',
});
const fileReader = new FileReader();
fileReader.onload = () => {
const [, data] = fileReader.result.split(',');
resolve(data);
};
fileReader.onerror = (e) => reject(e);
fileReader.readAsDataURL(blob);
});
window.WWebJS.getFileHash = async (data) => {
let buffer = await data.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
return btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
};
window.WWebJS.generateHash = async (length) => {
var result = '';
var characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(
Math.floor(Math.random() * charactersLength),
);
}
return result;
};
window.WWebJS.generateWaveform = async (audioFile) => {
try {
const audioData = await audioFile.arrayBuffer();
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(audioData);
const rawData = audioBuffer.getChannelData(0);
const samples = 64;
const blockSize = Math.floor(rawData.length / samples);
const filteredData = [];
for (let i = 0; i < samples; i++) {
const blockStart = blockSize * i;
let sum = 0;
for (let j = 0; j < blockSize; j++) {
sum = sum + Math.abs(rawData[blockStart + j]);
}
filteredData.push(sum / blockSize);
}
const multiplier = Math.pow(Math.max(...filteredData), -1);
const normalizedData = filteredData.map((n) => n * multiplier);
const waveform = new Uint8Array(
normalizedData.map((n) => Math.floor(100 * n)),
);
return waveform;
} catch (ignoredError) {
return undefined;
}
};
window.WWebJS.sendClearChat = async (chatId) => {
let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
if (chat !== undefined) {
await window.require('WAWebChatClearBridge').sendClear(chat, false);
return true;
}
return false;
};
window.WWebJS.sendDeleteChat = async (chatId) => {
let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
if (chat !== undefined) {
await window.require('WAWebDeleteChatAction').sendDelete(chat);
return true;
}
return false;
};
window.WWebJS.sendChatstate = async (state, chatId) => {
chatId = window.require('WAWebWidFactory').createWid(chatId);
const ChatState = window.require('WAWebChatStateBridge');
switch (state) {
case 'typing':
await ChatState.sendChatStateComposing(chatId);
break;
case 'recording':
await ChatState.sendChatStateRecording(chatId);
break;
case 'stop':
await ChatState.sendChatStatePaused(chatId);
break;
default:
throw 'Invalid chatstate';
}
return true;
};
window.WWebJS.getLabelModel = (label) => {
let res = label.serialize();
res.hexColor = label.hexColor;
return res;
};
window.WWebJS.getLabels = () => {
const labels = window
.require('WAWebCollections')
.Label.getModelsArray();
return labels.map((label) => window.WWebJS.getLabelModel(label));
};
window.WWebJS.getLabel = (labelId) => {
const label = window.require('WAWebCollections').Label.get(labelId);
return window.WWebJS.getLabelModel(label);
};
window.WWebJS.getChatLabels = async (chatId) => {
const chat = await window.WWebJS.getChat(chatId);
return (chat.labels || []).map((id) => window.WWebJS.getLabel(id));
};
window.WWebJS.getOrderDetail = async (orderId, token, chatId) => {
const chatWid = window.require('WAWebWidFactory').createWid(chatId);
return window
.require('WAWebBizOrderBridge')
.queryOrder(chatWid, orderId, 80, 80, token);
};
window.WWebJS.getProductMetadata = async (productId) => {
let sellerId = window.require('WAWebConnModel').Conn.wid;
let product = await window
.require('WAWebBizProductCatalogBridge')
.queryProduct(sellerId, productId);
if (product && product.data) {
return product.data;
}
return undefined;
};
window.WWebJS.cropAndResizeImage = async (media, options = {}) => {
if (!media.mimetype.includes('image'))
throw new Error('Media is not an image');
if (options.mimetype && !options.mimetype.includes('image'))
delete options.mimetype;
options = Object.assign(
{
size: 640,
mimetype: media.mimetype,
quality: 0.75,
asDataUrl: false,
},
options,
);
const img = await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = `data:${media.mimetype};base64,${media.data}`;
});
const sl = Math.min(img.width, img.height);
const sx = Math.floor((img.width - sl) / 2);
const sy = Math.floor((img.height - sl) / 2);
const canvas = document.createElement('canvas');
canvas.width = options.size;
canvas.height = options.size;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, sx, sy, sl, sl, 0, 0, options.size, options.size);
const dataUrl = canvas.toDataURL(options.mimetype, options.quality);
if (options.asDataUrl) return dataUrl;
return Object.assign(media, {
mimetype: options.mimetype,
data: dataUrl.replace(`data:${options.mimetype};base64,`, ''),
});
};
window.WWebJS.setPicture = async (chatId, media) => {
const thumbnail = await window.WWebJS.cropAndResizeImage(media, {
asDataUrl: true,
mimetype: 'image/jpeg',
size: 96,
});
const profilePic = await window.WWebJS.cropAndResizeImage(media, {
asDataUrl: true,
mimetype: 'image/jpeg',
size: 640,
});
const chatWid = window.require('WAWebWidFactory').createWid(chatId);
try {
const collection =
window
.require('WAWebCollections')
.ProfilePicThumb.get(chatId) ||
(await window
.require('WAWebCollections')
.ProfilePicThumb.find(chatId));
if (!collection?.canSet()) return false;
const res = await window
.require('WAWebContactProfilePicThumbBridge')
.sendSetPicture(chatWid, thumbnail, profilePic);
return res ? res.status === 200 : false;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
};
window.WWebJS.deletePicture = async (chatid) => {
const chatWid = window.require('WAWebWidFactory').createWid(chatid);
try {
const collection = window
.require('WAWebCollections')
.ProfilePicThumb.get(chatid);
if (!collection.canDelete()) return;
const res = await window
.require('WAWebContactProfilePicThumbBridge')
.requestDeletePicture(chatWid);
return res ? res.status === 200 : false;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
};
window.WWebJS.getProfilePicThumbToBase64 = async (chatWid) => {
const profilePicCollection = await window
.require('WAWebCollections')
.ProfilePicThumb.find(chatWid);
const _readImageAsBase64 = (imageBlob) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = function () {
const base64Image = reader.result;
if (base64Image == null) {
resolve(undefined);
} else {
const base64Data = base64Image.toString().split(',')[1];
resolve(base64Data);
}
};
reader.readAsDataURL(imageBlob);
});
};
if (profilePicCollection?.img) {
try {
const response = await fetch(profilePicCollection.img);
if (response.ok) {
const imageBlob = await response.blob();
if (imageBlob) {
const base64Image = await _readImageAsBase64(imageBlob);
return base64Image;
}
}
} catch (ignoredError) {
/* empty */
}
}
return undefined;
};
window.WWebJS.getAddParticipantsRpcResult = async (
groupWid,
participantWid,
) => {
const iqTo = window.require('WAWebWidToJid').widToGroupJid(groupWid);
const participantArgs = [
{
participantJid: window
.require('WAWebWidToJid')
.widToUserJid(participantWid),
},
];
let rpcResult, resultArgs;
const data = {
name: undefined,
code: undefined,
inviteV4Code: undefined,
inviteV4CodeExp: undefined,
};
try {
rpcResult = await window
.require('WASmaxGroupsAddParticipantsRPC')
.sendAddParticipantsRPC({ participantArgs, iqTo });
resultArgs =
rpcResult.value.addParticipant[0]
.addParticipantsParticipantAddedOrNonRegisteredWaUserParticipantErrorLidResponseMixinGroup
.value.addParticipantsParticipantMixins;
} catch (ignoredError) {
data.code = 400;
return data;
}
if (rpcResult.name === 'AddParticipantsResponseSuccess') {
const code = resultArgs?.value.error || '200';
data.name = resultArgs?.name;
data.code = +code;
data.inviteV4Code = resultArgs?.value.addRequestCode;
data.inviteV4CodeExp =
resultArgs?.value.addRequestExpiration?.toString();
} else if (rpcResult.name === 'AddParticipantsResponseClientError') {
const { code: code } =
rpcResult.value.errorAddParticipantsClientErrors.value;
data.code = +code;
} else if (rpcResult.name === 'AddParticipantsResponseServerError') {
const { code: code } = rpcResult.value.errorServerErrors.value;
data.code = +code;
}
return data;
};
window.WWebJS.membershipRequestAction = async (
groupId,
action,
requesterIds,
sleep,
) => {
const groupWid = window.require('WAWebWidFactory').createWid(groupId);
const group = await window
.require('WAWebCollections')
.Chat.find(groupWid);
const toApprove = action === 'Approve';
let membershipRequests;
let response;
let result = [];
await window
.require('WAWebGroupQueryJob')
.queryAndUpdateGroupMetadataById({ id: groupId });
if (!requesterIds?.length) {
membershipRequests =
group.groupMetadata.membershipApprovalRequests._models.map(
({ id }) => id,
);
} else {
!Array.isArray(requesterIds) && (requesterIds = [requesterIds]);
membershipRequests = requesterIds.map((r) =>
window.require('WAWebWidFactory').createWid(r),
);
}
if (!membershipRequests.length) return [];
const participantArgs = membershipRequests.map((m) => ({
participantArgs: [
{
participantJid: window
.require('WAWebWidToJid')
.widToUserJid(m),
},
],
}));
const groupJid = window
.require('WAWebWidToJid')
.widToGroupJid(groupWid);
const _getSleepTime = (sleep) => {
if (
!Array.isArray(sleep) ||
(sleep.length === 2 && sleep[0] === sleep[1])
) {
return sleep;
}
if (sleep.length === 1) {
return sleep[0];
}
sleep[1] - sleep[0] < 100 &&
(sleep[0] = sleep[1]) &&
(sleep[1] += 100);
return (
Math.floor(Math.random() * (sleep[1] - sleep[0] + 1)) + sleep[0]
);
};
const membReqResCodes = {
default: `An unknown error occupied while ${toApprove ? 'approving' : 'rejecting'} the participant membership request`,
400: 'ParticipantNotFoundError',
401: 'ParticipantNotAuthorizedError',
403: 'ParticipantForbiddenError',
404: 'ParticipantRequestNotFoundError',
408: 'ParticipantTemporarilyBlockedError',
409: 'ParticipantConflictError',
412: 'ParticipantParentLinkedGroupsResourceConstraintError',
500: 'ParticipantResourceConstraintError',
};
try {
for (const participant of participantArgs) {
response = await window
.require('WASmaxGroupsMembershipRequestsActionRPC')
.sendMembershipRequestsActionRPC({
iqTo: groupJid,
[toApprove ? 'approveArgs' : 'rejectArgs']: participant,
});
if (
response.name === 'MembershipRequestsActionResponseSuccess'
) {
const value = toApprove
? response.value.membershipRequestsActionApprove
: response.value.membershipRequestsActionReject;
if (value?.participant) {
const [_] = value.participant.map((p) => {
const error = toApprove
? value.participant[0]
.membershipRequestsActionAcceptParticipantMixins
?.value.error
: value.participant[0]
.membershipRequestsActionRejectParticipantMixins
?.value.error;
return {
requesterId: window.WWebJS.serializeWid(
window
.require('WAWebWidFactory')
.createWid(p.jid),
),
...(error
? {
error: +error,
message:
membReqResCodes[error] ||
membReqResCodes.default,
}
: {
message: `${toApprove ? 'Approved' : 'Rejected'} successfully`,
}),
};
});
_ && result.push(_);
}
} else {
result.push({
requesterId: window.WWebJS.serializeWid(
window
.require('WAWebJidToWid')
.userJidToUserWid(
participant.participantArgs[0]
.participantJid,
),
),
message: 'ServerStatusCodeError',
});
}
sleep &&
participantArgs.length > 1 &&
participantArgs.indexOf(participant) !==
participantArgs.length - 1 &&
(await new Promise((resolve) =>
setTimeout(resolve, _getSleepTime(sleep)),
));
}
return result;
} catch (ignoredError) {
return [];
}
};
window.WWebJS.subscribeToUnsubscribeFromChannel = async (
channelId,
action,
options = {},
) => {
const channel = await window.WWebJS.getChat(channelId, {
getAsModel: false,
});
if (!channel || channel.newsletterMetadata.membershipType === 'owner')
return false;
options = {
eventSurface: 3,
deleteLocalModels: options.deleteLocalModels ?? true,
};
try {
if (action === 'Subscribe') {
await window
.require('WAWebNewsletterSubscribeAction')
.subscribeToNewsletterAction(channel, options);
} else if (action === 'Unsubscribe') {
await window
.require('WAWebNewsletterUnsubscribeAction')
.unsubscribeFromNewsletterAction(channel, options);
} else return false;
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
};
window.WWebJS.pinUnpinMsgAction = async (msgId, action, duration) => {
const message =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (!message) return false;
if (typeof duration !== 'number') return false;
const pinConstants = window.require('WAWebPinMsgConstants');
const originalFunction = pinConstants.getPinExpiryDuration;
pinConstants.getPinExpiryDuration = () => duration;
try {
const response = await window
.require('WAWebSendPinMessageAction')
.sendPinInChatMsg(message, action, duration);
return response.messageSendResult === 'OK';
} finally {
pinConstants.getPinExpiryDuration = originalFunction;
}
};
window.WWebJS.getStatusModel = (status) => {
const res = status.serialize();
delete res._msgs;
return res;
};
window.WWebJS.getAllStatuses = () => {
const statuses = window
.require('WAWebCollections')
.Status.getModelsArray();
return statuses.map((status) => window.WWebJS.getStatusModel(status));
};
window.WWebJS.enforceLidAndPnRetrieval = async (userId) => {
const wid = window.require('WAWebWidFactory').createWid(userId);
const isLid = wid.server === 'lid';
let lid = isLid
? wid
: window.require('WAWebApiContact').getCurrentLid(wid);
let phone = isLid
? window.require('WAWebApiContact').getPhoneNumber(wid)
: wid;
if (!isLid && !lid) {
const queryResult = await window
.require('WAWebQueryExistsJob')
.queryWidExists(wid);
if (!queryResult?.wid) return {};
lid = window.require('WAWebApiContact').getCurrentLid(wid);
}
if (isLid && !phone) {
const queryResult = await window
.require('WAWebQueryExistsJob')
.queryWidExists(wid);
if (!queryResult?.wid) return {};
phone = window.require('WAWebApiContact').getPhoneNumber(wid);
}
return { lid, phone };
};
window.WWebJS.assertColor = (hex) => {
let color;
if (typeof hex === 'number') {
color = hex > 0 ? hex : 0xffffffff + parseInt(hex) + 1;
} else if (typeof hex === 'string') {
let number = hex.trim().replace('#', '');
if (number.length <= 6) {
number = 'FF' + number.padStart(6, '0');
}
color = parseInt(number, 16);
} else {
throw 'Invalid hex color';
}
return color;
};
};