Source: util/Injected/Utils.js

'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;
    };
};