UNPKG

naruyaizumi

Version:

A WebSockets library for interacting with WhatsApp Web

1,105 lines (1,104 loc) 39.3 kB
import NodeCache from "@cacheable/node-cache"; import { Boom } from "@hapi/boom"; import { proto } from "../../WAProto/index.js"; import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from "../Defaults/index.js"; import { ALL_WA_PATCH_NAMES } from "../Types/index.js"; import { SyncState } from "../Types/State.js"; import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction, } from "../Utils/index.js"; import { makeMutex } from "../Utils/make-mutex.js"; import processMessage from "../Utils/process-message.js"; import { getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET, } from "../WABinary/index.js"; import { USyncQuery, USyncUser } from "../WAUSync/index.js"; import { makeSocket } from "./socket.js"; const MAX_SYNC_ATTEMPTS = 2; export const makeChatsSocket = (config) => { const { logger, markOnlineOnConnect, fireInitQueries, appStateMacVerification, shouldIgnoreJid, shouldSyncHistoryMessage, getMessage, } = config; const sock = makeSocket(config); const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, } = sock; let privacySettings; let syncState = SyncState.Connecting; /** this mutex ensures that the notifications (receipts, messages etc.) are processed in order */ const processingMutex = makeMutex(); // Timeout for AwaitingInitialSync state let awaitingSyncTimeout; const placeholderResendCache = config.placeholderResendCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour useClones: false, }); if (!config.placeholderResendCache) { config.placeholderResendCache = placeholderResendCache; } /** helper function to fetch the given app state sync key */ const getAppStateSyncKey = async (keyId) => { const { [keyId]: key } = await authState.keys.get("app-state-sync-key", [keyId]); return key; }; const fetchPrivacySettings = async (force = false) => { if (!privacySettings || force) { const { content } = await query({ tag: "iq", attrs: { xmlns: "privacy", to: S_WHATSAPP_NET, type: "get", }, content: [{ tag: "privacy", attrs: {} }], }); privacySettings = reduceBinaryNodeToDictionary(content?.[0], "category"); } return privacySettings; }; /** helper function to run a privacy IQ query */ const privacyQuery = async (name, value) => { await query({ tag: "iq", attrs: { xmlns: "privacy", to: S_WHATSAPP_NET, type: "set", }, content: [ { tag: "privacy", attrs: {}, content: [ { tag: "category", attrs: { name, value }, }, ], }, ], }); }; const updateMessagesPrivacy = async (value) => { await privacyQuery("messages", value); }; const updateCallPrivacy = async (value) => { await privacyQuery("calladd", value); }; const updateLastSeenPrivacy = async (value) => { await privacyQuery("last", value); }; const updateOnlinePrivacy = async (value) => { await privacyQuery("online", value); }; const updateProfilePicturePrivacy = async (value) => { await privacyQuery("profile", value); }; const updateStatusPrivacy = async (value) => { await privacyQuery("status", value); }; const updateReadReceiptsPrivacy = async (value) => { await privacyQuery("readreceipts", value); }; const updateGroupsAddPrivacy = async (value) => { await privacyQuery("groupadd", value); }; const updateDefaultDisappearingMode = async (duration) => { await query({ tag: "iq", attrs: { xmlns: "disappearing_mode", to: S_WHATSAPP_NET, type: "set", }, content: [ { tag: "disappearing_mode", attrs: { duration: duration.toString(), }, }, ], }); }; const getBotListV2 = async () => { const resp = await query({ tag: "iq", attrs: { xmlns: "bot", to: S_WHATSAPP_NET, type: "get", }, content: [ { tag: "bot", attrs: { v: "2", }, }, ], }); const botNode = getBinaryNodeChild(resp, "bot"); const botList = []; for (const section of getBinaryNodeChildren(botNode, "section")) { if (section.attrs.type === "all") { for (const bot of getBinaryNodeChildren(section, "bot")) { botList.push({ jid: bot.attrs.jid, personaId: bot.attrs["persona_id"], }); } } } return botList; }; const fetchStatus = async (...jids) => { const usyncQuery = new USyncQuery().withStatusProtocol(); for (const jid of jids) { usyncQuery.withUser(new USyncUser().withId(jid)); } const result = await sock.executeUSyncQuery(usyncQuery); if (result) { return result.list; } }; const fetchDisappearingDuration = async (...jids) => { const usyncQuery = new USyncQuery().withDisappearingModeProtocol(); for (const jid of jids) { usyncQuery.withUser(new USyncUser().withId(jid)); } const result = await sock.executeUSyncQuery(usyncQuery); if (result) { return result.list; } }; /** update the profile picture for yourself or a group */ const updateProfilePicture = async (jid, content, dimensions) => { let targetJid; if (!jid) { throw new Boom( "Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update" ); } if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id)) { targetJid = jidNormalizedUser(jid); // in case it is someone other than us } else { targetJid = undefined; } const { img } = await generateProfilePicture(content, dimensions); await query({ tag: "iq", attrs: { to: S_WHATSAPP_NET, type: "set", xmlns: "w:profile:picture", ...(targetJid ? { target: targetJid } : {}), }, content: [ { tag: "picture", attrs: { type: "image" }, content: img, }, ], }); }; /** remove the profile picture for yourself or a group */ const removeProfilePicture = async (jid) => { let targetJid; if (!jid) { throw new Boom( "Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update" ); } if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id)) { targetJid = jidNormalizedUser(jid); // in case it is someone other than us } else { targetJid = undefined; } await query({ tag: "iq", attrs: { to: S_WHATSAPP_NET, type: "set", xmlns: "w:profile:picture", ...(targetJid ? { target: targetJid } : {}), }, }); }; /** update the profile status for yourself */ const updateProfileStatus = async (status) => { await query({ tag: "iq", attrs: { to: S_WHATSAPP_NET, type: "set", xmlns: "status", }, content: [ { tag: "status", attrs: {}, content: Buffer.from(status, "utf-8"), }, ], }); }; const updateProfileName = async (name) => { await chatModify({ pushNameSetting: name }, ""); }; const fetchBlocklist = async () => { const result = await query({ tag: "iq", attrs: { xmlns: "blocklist", to: S_WHATSAPP_NET, type: "get", }, }); const listNode = getBinaryNodeChild(result, "list"); return getBinaryNodeChildren(listNode, "item").map((n) => n.attrs.jid); }; const updateBlockStatus = async (jid, action) => { await query({ tag: "iq", attrs: { xmlns: "blocklist", to: S_WHATSAPP_NET, type: "set", }, content: [ { tag: "item", attrs: { action, jid, }, }, ], }); }; const getBusinessProfile = async (jid) => { const results = await query({ tag: "iq", attrs: { to: "s.whatsapp.net", xmlns: "w:biz", type: "get", }, content: [ { tag: "business_profile", attrs: { v: "244" }, content: [ { tag: "profile", attrs: { jid }, }, ], }, ], }); const profileNode = getBinaryNodeChild(results, "business_profile"); const profiles = getBinaryNodeChild(profileNode, "profile"); if (profiles) { const address = getBinaryNodeChild(profiles, "address"); const description = getBinaryNodeChild(profiles, "description"); const website = getBinaryNodeChild(profiles, "website"); const email = getBinaryNodeChild(profiles, "email"); const category = getBinaryNodeChild( getBinaryNodeChild(profiles, "categories"), "category" ); const businessHours = getBinaryNodeChild(profiles, "business_hours"); const businessHoursConfig = businessHours ? getBinaryNodeChildren(businessHours, "business_hours_config") : undefined; const websiteStr = website?.content?.toString(); return { wid: profiles.attrs?.jid, address: address?.content?.toString(), description: description?.content?.toString() || "", website: websiteStr ? [websiteStr] : [], email: email?.content?.toString(), category: category?.content?.toString(), business_hours: { timezone: businessHours?.attrs?.timezone, business_config: businessHoursConfig?.map(({ attrs }) => attrs), }, }; } }; const cleanDirtyBits = async (type, fromTimestamp) => { logger.info({ fromTimestamp }, "clean dirty bits " + type); await sendNode({ tag: "iq", attrs: { to: S_WHATSAPP_NET, type: "set", xmlns: "urn:xmpp:whatsapp:dirty", id: generateMessageTag(), }, content: [ { tag: "clean", attrs: { type, ...(fromTimestamp ? { timestamp: fromTimestamp.toString() } : null), }, }, ], }); }; const newAppStateChunkHandler = (isInitialSync) => { return { onMutation(mutation) { processSyncAction( mutation, ev, authState.creds.me, isInitialSync ? { accountSettings: authState.creds.accountSettings } : undefined, logger ); }, }; }; const resyncAppState = ev.createBufferedFunction(async (collections, isInitialSync) => { // we use this to determine which events to fire // otherwise when we resync from scratch -- all notifications will fire const initialVersionMap = {}; const globalMutationMap = {}; await authState.keys.transaction(async () => { const collectionsToHandle = new Set(collections); // in case something goes wrong -- ensure we don't enter a loop that cannot be exited from const attemptsMap = {}; // keep executing till all collections are done // sometimes a single patch request will not return all the patches (God knows why) // so we fetch till they're all done (this is determined by the "has_more_patches" flag) while (collectionsToHandle.size) { const states = {}; const nodes = []; for (const name of collectionsToHandle) { const result = await authState.keys.get("app-state-sync-version", [name]); let state = result[name]; if (state) { if (typeof initialVersionMap[name] === "undefined") { initialVersionMap[name] = state.version; } } else { state = newLTHashState(); } states[name] = state; logger.info(`resyncing ${name} from v${state.version}`); nodes.push({ tag: "collection", attrs: { name, version: state.version.toString(), // return snapshot if being synced from scratch return_snapshot: (!state.version).toString(), }, }); } const result = await query({ tag: "iq", attrs: { to: S_WHATSAPP_NET, xmlns: "w:sync:app:state", type: "set", }, content: [ { tag: "sync", attrs: {}, content: nodes, }, ], }); // extract from binary node const decoded = await extractSyncdPatches(result, config?.options); for (const key in decoded) { const name = key; const { patches, hasMorePatches, snapshot } = decoded[name]; try { if (snapshot) { const { state: newState, mutationMap } = await decodeSyncdSnapshot( name, snapshot, getAppStateSyncKey, initialVersionMap[name], appStateMacVerification.snapshot ); states[name] = newState; Object.assign(globalMutationMap, mutationMap); logger.info( `restored state of ${name} from snapshot to v${newState.version} with mutations` ); await authState.keys.set({ "app-state-sync-version": { [name]: newState }, }); } // only process if there are syncd patches if (patches.length) { const { state: newState, mutationMap } = await decodePatches( name, patches, states[name], getAppStateSyncKey, config.options, initialVersionMap[name], logger, appStateMacVerification.patch ); await authState.keys.set({ "app-state-sync-version": { [name]: newState }, }); logger.info(`synced ${name} to v${newState.version}`); initialVersionMap[name] = newState.version; Object.assign(globalMutationMap, mutationMap); } if (hasMorePatches) { logger.info(`${name} has more patches...`); } else { // collection is done with sync collectionsToHandle.delete(name); } } catch (error) { // if retry attempts overshoot // or key not found const isIrrecoverableError = attemptsMap[name] >= MAX_SYNC_ATTEMPTS || error.output?.statusCode === 404 || error.name === "TypeError"; logger.info( { name, error: error.stack }, `failed to sync state from version${isIrrecoverableError ? "" : ", removing and trying from scratch"}` ); await authState.keys.set({ "app-state-sync-version": { [name]: null } }); // increment number of retries attemptsMap[name] = (attemptsMap[name] || 0) + 1; if (isIrrecoverableError) { // stop retrying collectionsToHandle.delete(name); } } } } }, authState?.creds?.me?.id || "resync-app-state"); const { onMutation } = newAppStateChunkHandler(isInitialSync); for (const key in globalMutationMap) { onMutation(globalMutationMap[key]); } }); /** * fetch the profile picture of a user/group * type = "preview" for a low res picture * type = "image for the high res picture" */ const profilePictureUrl = async (jid, type = "preview", timeoutMs) => { // TOOD: Add support for tctoken, existingID, and newsletter + group options jid = jidNormalizedUser(jid); const result = await query( { tag: "iq", attrs: { target: jid, to: S_WHATSAPP_NET, type: "get", xmlns: "w:profile:picture", }, content: [{ tag: "picture", attrs: { type, query: "url" } }], }, timeoutMs ); const child = getBinaryNodeChild(result, "picture"); return child?.attrs?.url; }; const createCallLink = async (type, event, timeoutMs) => { const result = await query( { tag: "call", attrs: { id: generateMessageTag(), to: "@call", }, content: [ { tag: "link_create", attrs: { media: type }, content: event ? [{ tag: "event", attrs: { start_time: String(event.startTime) } }] : undefined, }, ], }, timeoutMs ); const child = getBinaryNodeChild(result, "link_create"); return child?.attrs?.token; }; const sendPresenceUpdate = async (type, toJid) => { const me = authState.creds.me; if (type === "available" || type === "unavailable") { if (!me.name) { logger.warn("no name present, ignoring presence update request..."); return; } ev.emit("connection.update", { isOnline: type === "available" }); await sendNode({ tag: "presence", attrs: { name: me.name.replace(/@/g, ""), type, }, }); } else { const { server } = jidDecode(toJid); const isLid = server === "lid"; await sendNode({ tag: "chatstate", attrs: { from: isLid ? me.lid : me.id, to: toJid, }, content: [ { tag: type === "recording" ? "composing" : type, attrs: type === "recording" ? { media: "audio" } : {}, }, ], }); } }; /** * @param toJid the jid to subscribe to * @param tcToken token for subscription, use if present */ const presenceSubscribe = (toJid, tcToken) => sendNode({ tag: "presence", attrs: { to: toJid, id: generateMessageTag(), type: "subscribe", }, content: tcToken ? [ { tag: "tctoken", attrs: {}, content: tcToken, }, ] : undefined, }); const handlePresenceUpdate = ({ tag, attrs, content }) => { let presence; const jid = attrs.from; const participant = attrs.participant || attrs.from; if (shouldIgnoreJid(jid) && jid !== S_WHATSAPP_NET) { return; } if (tag === "presence") { presence = { lastKnownPresence: attrs.type === "unavailable" ? "unavailable" : "available", lastSeen: attrs.last && attrs.last !== "deny" ? +attrs.last : undefined, }; } else if (Array.isArray(content)) { const [firstChild] = content; let type = firstChild.tag; if (type === "paused") { type = "available"; } if (firstChild.attrs?.media === "audio") { type = "recording"; } presence = { lastKnownPresence: type }; } else { logger.error({ tag, attrs, content }, "recv invalid presence node"); } if (presence) { ev.emit("presence.update", { id: jid, presences: { [participant]: presence } }); } }; const appPatch = async (patchCreate) => { const name = patchCreate.type; const myAppStateKeyId = authState.creds.myAppStateKeyId; if (!myAppStateKeyId) { throw new Boom("App state key not present!", { statusCode: 400 }); } let initial; let encodeResult; await processingMutex.mutex(async () => { await authState.keys.transaction(async () => { logger.debug({ patch: patchCreate }, "applying app patch"); await resyncAppState([name], false); const { [name]: currentSyncVersion } = await authState.keys.get( "app-state-sync-version", [name] ); initial = currentSyncVersion || newLTHashState(); encodeResult = await encodeSyncdPatch( patchCreate, myAppStateKeyId, initial, getAppStateSyncKey ); const { patch, state } = encodeResult; const node = { tag: "iq", attrs: { to: S_WHATSAPP_NET, type: "set", xmlns: "w:sync:app:state", }, content: [ { tag: "sync", attrs: {}, content: [ { tag: "collection", attrs: { name, version: (state.version - 1).toString(), return_snapshot: "false", }, content: [ { tag: "patch", attrs: {}, content: proto.SyncdPatch.encode(patch).finish(), }, ], }, ], }, ], }; await query(node); await authState.keys.set({ "app-state-sync-version": { [name]: state } }); }, authState?.creds?.me?.id || "app-patch"); }); if (config.emitOwnEvents) { const { onMutation } = newAppStateChunkHandler(false); const { mutationMap } = await decodePatches( name, [{ ...encodeResult.patch, version: { version: encodeResult.state.version } }], initial, getAppStateSyncKey, config.options, undefined, logger ); for (const key in mutationMap) { onMutation(mutationMap[key]); } } }; /** sending non-abt props may fix QR scan fail if server expects */ const fetchProps = async () => { //TODO: implement both protocol 1 and protocol 2 prop fetching, specially for abKey for WM const resultNode = await query({ tag: "iq", attrs: { to: S_WHATSAPP_NET, xmlns: "w", type: "get", }, content: [ { tag: "props", attrs: { protocol: "2", hash: authState?.creds?.lastPropHash || "", }, }, ], }); const propsNode = getBinaryNodeChild(resultNode, "props"); let props = {}; if (propsNode) { if (propsNode.attrs?.hash) { // on some clients, the hash is returning as undefined authState.creds.lastPropHash = propsNode?.attrs?.hash; ev.emit("creds.update", authState.creds); } props = reduceBinaryNodeToDictionary(propsNode, "prop"); } logger.debug("fetched props"); return props; }; /** * modify a chat -- mark unread, read etc. * lastMessages must be sorted in reverse chronologically * requires the last messages till the last message received; required for archive & unread */ const chatModify = (mod, jid) => { const patch = chatModificationToAppPatch(mod, jid); return appPatch(patch); }; /** * Enable/Disable link preview privacy, not related to baileys link preview generation */ const updateDisableLinkPreviewsPrivacy = (isPreviewsDisabled) => { return chatModify( { disableLinkPreviews: { isPreviewsDisabled }, }, "" ); }; /** * Star or Unstar a message */ const star = (jid, messages, star) => { return chatModify( { star: { messages, star, }, }, jid ); }; /** * Add or Edit Contact */ const addOrEditContact = (jid, contact) => { return chatModify( { contact, }, jid ); }; /** * Remove Contact */ const removeContact = (jid) => { return chatModify( { contact: null, }, jid ); }; /** * Adds label */ const addLabel = (jid, labels) => { return chatModify( { addLabel: { ...labels, }, }, jid ); }; /** * Adds label for the chats */ const addChatLabel = (jid, labelId) => { return chatModify( { addChatLabel: { labelId, }, }, jid ); }; /** * Removes label for the chat */ const removeChatLabel = (jid, labelId) => { return chatModify( { removeChatLabel: { labelId, }, }, jid ); }; /** * Adds label for the message */ const addMessageLabel = (jid, messageId, labelId) => { return chatModify( { addMessageLabel: { messageId, labelId, }, }, jid ); }; /** * Removes label for the message */ const removeMessageLabel = (jid, messageId, labelId) => { return chatModify( { removeMessageLabel: { messageId, labelId, }, }, jid ); }; /** * Add or Edit Quick Reply */ const addOrEditQuickReply = (quickReply) => { return chatModify( { quickReply, }, "" ); }; /** * Remove Quick Reply */ const removeQuickReply = (timestamp) => { return chatModify( { quickReply: { timestamp, deleted: true }, }, "" ); }; /** * queries need to be fired on connection open * help ensure parity with WA Web * */ const executeInitQueries = async () => { await Promise.all([fetchProps(), fetchBlocklist(), fetchPrivacySettings()]); }; const upsertMessage = ev.createBufferedFunction(async (msg, type) => { ev.emit("messages.upsert", { messages: [msg], type }); if (!!msg.pushName) { let jid = msg.key.fromMe ? authState.creds.me.id : msg.key.participant || msg.key.remoteJid; jid = jidNormalizedUser(jid); if (!msg.key.fromMe) { ev.emit("contacts.update", [ { id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName }, ]); } // update our pushname too if (msg.key.fromMe && msg.pushName && authState.creds.me?.name !== msg.pushName) { ev.emit("creds.update", { me: { ...authState.creds.me, name: msg.pushName } }); } } const historyMsg = getHistoryMsg(msg.message); const shouldProcessHistoryMsg = historyMsg ? shouldSyncHistoryMessage(historyMsg) && PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType) : false; // State machine: decide on sync and flush if (historyMsg && syncState === SyncState.AwaitingInitialSync) { if (awaitingSyncTimeout) { clearTimeout(awaitingSyncTimeout); awaitingSyncTimeout = undefined; } if (shouldProcessHistoryMsg) { syncState = SyncState.Syncing; logger.info("Transitioned to Syncing state"); // Let doAppStateSync handle the final flush after it's done } else { syncState = SyncState.Online; logger.info( "History sync skipped, transitioning to Online state and flushing buffer" ); ev.flush(); } } const doAppStateSync = async () => { if (syncState === SyncState.Syncing) { logger.info("Doing app state sync"); await resyncAppState(ALL_WA_PATCH_NAMES, true); // Sync is complete, go online and flush everything syncState = SyncState.Online; logger.info( "App state sync complete, transitioning to Online state and flushing buffer" ); ev.flush(); const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1; ev.emit("creds.update", { accountSyncCounter }); } }; await Promise.all([ (async () => { if (shouldProcessHistoryMsg) { await doAppStateSync(); } })(), processMessage(msg, { signalRepository, shouldProcessHistoryMsg, placeholderResendCache, ev, creds: authState.creds, keyStore: authState.keys, logger, options: config.options, getMessage, }), ]); // If the app state key arrives and we are waiting to sync, trigger the sync now. if (msg.message?.protocolMessage?.appStateSyncKeyShare && syncState === SyncState.Syncing) { logger.info("App state sync key arrived, triggering app state sync"); await doAppStateSync(); } }); ws.on("CB:presence", handlePresenceUpdate); ws.on("CB:chatstate", handlePresenceUpdate); ws.on("CB:ib,,dirty", async (node) => { const { attrs } = getBinaryNodeChild(node, "dirty"); const type = attrs.type; switch (type) { case "account_sync": if (attrs.timestamp) { let { lastAccountSyncTimestamp } = authState.creds; if (lastAccountSyncTimestamp) { await cleanDirtyBits("account_sync", lastAccountSyncTimestamp); } lastAccountSyncTimestamp = +attrs.timestamp; ev.emit("creds.update", { lastAccountSyncTimestamp }); } break; case "groups": // handled in groups.ts break; default: logger.info({ node }, "received unknown sync"); break; } }); ev.on("connection.update", ({ connection, receivedPendingNotifications }) => { if (connection === "open") { if (fireInitQueries) { executeInitQueries().catch((error) => onUnexpectedError(error, "init queries")); } sendPresenceUpdate(markOnlineOnConnect ? "available" : "unavailable").catch((error) => onUnexpectedError(error, "presence update requests") ); } if (!receivedPendingNotifications || syncState !== SyncState.Connecting) { return; } syncState = SyncState.AwaitingInitialSync; logger.info("Connection is now AwaitingInitialSync, buffering events"); ev.buffer(); const willSyncHistory = shouldSyncHistoryMessage( proto.Message.HistorySyncNotification.create({ syncType: proto.HistorySync.HistorySyncType.RECENT, }) ); if (!willSyncHistory) { logger.info( "History sync is disabled by config, not waiting for notification. Transitioning to Online." ); syncState = SyncState.Online; setTimeout(() => ev.flush(), 0); return; } logger.info("History sync is enabled, awaiting notification with a 20s timeout."); if (awaitingSyncTimeout) { clearTimeout(awaitingSyncTimeout); } awaitingSyncTimeout = setTimeout(() => { if (syncState === SyncState.AwaitingInitialSync) { // TODO: investigate logger.warn( "Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer" ); syncState = SyncState.Online; ev.flush(); } }, 20000); }); return { ...sock, createCallLink, getBotListV2, processingMutex, fetchPrivacySettings, upsertMessage, appPatch, sendPresenceUpdate, presenceSubscribe, profilePictureUrl, fetchBlocklist, fetchStatus, fetchDisappearingDuration, updateProfilePicture, removeProfilePicture, updateProfileStatus, updateProfileName, updateBlockStatus, updateDisableLinkPreviewsPrivacy, updateCallPrivacy, updateMessagesPrivacy, updateLastSeenPrivacy, updateOnlinePrivacy, updateProfilePicturePrivacy, updateStatusPrivacy, updateReadReceiptsPrivacy, updateGroupsAddPrivacy, updateDefaultDisappearingMode, getBusinessProfile, resyncAppState, chatModify, cleanDirtyBits, addOrEditContact, removeContact, addLabel, addChatLabel, removeChatLabel, addMessageLabel, removeMessageLabel, star, addOrEditQuickReply, removeQuickReply, }; }; //# sourceMappingURL=chats.js.map