naruyaizumi
Version:
A WebSockets library for interacting with WhatsApp Web
1,105 lines (1,104 loc) • 39.3 kB
JavaScript
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