naruyaizumi
Version:
A WebSockets library for interacting with WhatsApp Web
878 lines (877 loc) • 32.6 kB
JavaScript
import { Boom } from "@hapi/boom";
import { proto } from "../../WAProto/index.js";
import { LabelAssociationType } from "../Types/LabelAssociation.js";
import {
getBinaryNodeChild,
getBinaryNodeChildren,
isJidGroup,
jidNormalizedUser,
} from "../WABinary/index.js";
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from "./crypto.js";
import { toNumber } from "./generics.js";
import { LT_HASH_ANTI_TAMPERING } from "./lt-hash.js";
import { downloadContentFromMessage } from "./messages-media.js";
const mutationKeys = async (keydata) => {
const expanded = await hkdf(keydata, 160, { info: "WhatsApp Mutation Keys" });
return {
indexKey: expanded.slice(0, 32),
valueEncryptionKey: expanded.slice(32, 64),
valueMacKey: expanded.slice(64, 96),
snapshotMacKey: expanded.slice(96, 128),
patchMacKey: expanded.slice(128, 160),
};
};
const generateMac = (operation, data, keyId, key) => {
const getKeyData = () => {
let r;
switch (operation) {
case proto.SyncdMutation.SyncdOperation.SET:
r = 0x01;
break;
case proto.SyncdMutation.SyncdOperation.REMOVE:
r = 0x02;
break;
}
const buff = Buffer.from([r]);
return Buffer.concat([buff, Buffer.from(keyId, "base64")]);
};
const keyData = getKeyData();
const last = Buffer.alloc(8); // 8 bytes
last.set([keyData.length], last.length - 1);
const total = Buffer.concat([keyData, data, last]);
const hmac = hmacSign(total, key, "sha512");
return hmac.slice(0, 32);
};
const to64BitNetworkOrder = (e) => {
const buff = Buffer.alloc(8);
buff.writeUint32BE(e, 4);
return buff;
};
const makeLtHashGenerator = ({ indexValueMap, hash }) => {
indexValueMap = { ...indexValueMap };
const addBuffs = [];
const subBuffs = [];
return {
mix: ({ indexMac, valueMac, operation }) => {
const indexMacBase64 = Buffer.from(indexMac).toString("base64");
const prevOp = indexValueMap[indexMacBase64];
if (operation === proto.SyncdMutation.SyncdOperation.REMOVE) {
if (!prevOp) {
throw new Boom("tried remove, but no previous op", {
data: { indexMac, valueMac },
});
}
// remove from index value mac, since this mutation is erased
delete indexValueMap[indexMacBase64];
} else {
addBuffs.push(new Uint8Array(valueMac).buffer);
// add this index into the history map
indexValueMap[indexMacBase64] = { valueMac };
}
if (prevOp) {
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer);
}
},
finish: async () => {
const hashArrayBuffer = new Uint8Array(hash).buffer;
const result = await LT_HASH_ANTI_TAMPERING.subtractThenAdd(
hashArrayBuffer,
addBuffs,
subBuffs
);
const buffer = Buffer.from(result);
return {
hash: buffer,
indexValueMap,
};
},
};
};
const generateSnapshotMac = (lthash, version, name, key) => {
const total = Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, "utf-8")]);
return hmacSign(total, key, "sha256");
};
const generatePatchMac = (snapshotMac, valueMacs, version, type, key) => {
const total = Buffer.concat([
snapshotMac,
...valueMacs,
to64BitNetworkOrder(version),
Buffer.from(type, "utf-8"),
]);
return hmacSign(total, key);
};
export const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} });
export const encodeSyncdPatch = async (
{ type, index, syncAction, apiVersion, operation },
myAppStateKeyId,
state,
getAppStateSyncKey
) => {
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined;
if (!key) {
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 });
}
const encKeyId = Buffer.from(myAppStateKeyId, "base64");
state = { ...state, indexValueMap: { ...state.indexValueMap } };
const indexBuffer = Buffer.from(JSON.stringify(index));
const dataProto = proto.SyncActionData.fromObject({
index: indexBuffer,
value: syncAction,
padding: new Uint8Array(0),
version: apiVersion,
});
const encoded = proto.SyncActionData.encode(dataProto).finish();
const keyValue = await mutationKeys(key.keyData);
const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey);
const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey);
const indexMac = hmacSign(indexBuffer, keyValue.indexKey);
// update LT hash
const generator = makeLtHashGenerator(state);
generator.mix({ indexMac, valueMac, operation });
Object.assign(state, await generator.finish());
state.version += 1;
const snapshotMac = generateSnapshotMac(
state.hash,
state.version,
type,
keyValue.snapshotMacKey
);
const patch = {
patchMac: generatePatchMac(
snapshotMac,
[valueMac],
state.version,
type,
keyValue.patchMacKey
),
snapshotMac: snapshotMac,
keyId: { id: encKeyId },
mutations: [
{
operation: operation,
record: {
index: {
blob: indexMac,
},
value: {
blob: Buffer.concat([encValue, valueMac]),
},
keyId: { id: encKeyId },
},
},
],
};
const base64Index = indexMac.toString("base64");
state.indexValueMap[base64Index] = { valueMac };
return { patch, state };
};
export const decodeSyncdMutations = async (
msgMutations,
initialState,
getAppStateSyncKey,
onMutation,
validateMacs
) => {
const ltGenerator = makeLtHashGenerator(initialState);
// indexKey used to HMAC sign record.index.blob
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
for (const msgMutation of msgMutations) {
// if it's a syncdmutation, get the operation property
// otherwise, if it's only a record -- it'll be a SET mutation
const operation =
"operation" in msgMutation
? msgMutation.operation
: proto.SyncdMutation.SyncdOperation.SET;
const record =
"record" in msgMutation && !!msgMutation.record ? msgMutation.record : msgMutation;
const key = await getKey(record.keyId.id);
const content = Buffer.from(record.value.blob);
const encContent = content.slice(0, -32);
const ogValueMac = content.slice(-32);
if (validateMacs) {
const contentHmac = generateMac(
operation,
encContent,
record.keyId.id,
key.valueMacKey
);
if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
throw new Boom("HMAC content verification failed");
}
}
const result = aesDecrypt(encContent, key.valueEncryptionKey);
const syncAction = proto.SyncActionData.decode(result);
if (validateMacs) {
const hmac = hmacSign(syncAction.index, key.indexKey);
if (Buffer.compare(hmac, record.index.blob) !== 0) {
throw new Boom("HMAC index verification failed");
}
}
const indexStr = Buffer.from(syncAction.index).toString();
onMutation({ syncAction, index: JSON.parse(indexStr) });
ltGenerator.mix({
indexMac: record.index.blob,
valueMac: ogValueMac,
operation: operation,
});
}
return await ltGenerator.finish();
async function getKey(keyId) {
const base64Key = Buffer.from(keyId).toString("base64");
const keyEnc = await getAppStateSyncKey(base64Key);
if (!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
statusCode: 404,
data: { msgMutations },
});
}
return mutationKeys(keyEnc.keyData);
}
};
export const decodeSyncdPatch = async (
msg,
name,
initialState,
getAppStateSyncKey,
onMutation,
validateMacs
) => {
if (validateMacs) {
const base64Key = Buffer.from(msg.keyId.id).toString("base64");
const mainKeyObj = await getAppStateSyncKey(base64Key);
if (!mainKeyObj) {
throw new Boom(`failed to find key "${base64Key}" to decode patch`, {
statusCode: 404,
data: { msg },
});
}
const mainKey = await mutationKeys(mainKeyObj.keyData);
const mutationmacs = msg.mutations.map((mutation) => mutation.record.value.blob.slice(-32));
const patchMac = generatePatchMac(
msg.snapshotMac,
mutationmacs,
toNumber(msg.version.version),
name,
mainKey.patchMacKey
);
if (Buffer.compare(patchMac, msg.patchMac) !== 0) {
throw new Boom("Invalid patch mac");
}
}
const result = await decodeSyncdMutations(
msg.mutations,
initialState,
getAppStateSyncKey,
onMutation,
validateMacs
);
return result;
};
export const extractSyncdPatches = async (result, options) => {
const syncNode = getBinaryNodeChild(result, "sync");
const collectionNodes = getBinaryNodeChildren(syncNode, "collection");
const final = {};
await Promise.all(
collectionNodes.map(async (collectionNode) => {
const patchesNode = getBinaryNodeChild(collectionNode, "patches");
const patches = getBinaryNodeChildren(patchesNode || collectionNode, "patch");
const snapshotNode = getBinaryNodeChild(collectionNode, "snapshot");
const syncds = [];
const name = collectionNode.attrs.name;
const hasMorePatches = collectionNode.attrs.has_more_patches === "true";
let snapshot = undefined;
if (snapshotNode && !!snapshotNode.content) {
if (!Buffer.isBuffer(snapshotNode)) {
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content));
}
const blobRef = proto.ExternalBlobReference.decode(snapshotNode.content);
const data = await downloadExternalBlob(blobRef, options);
snapshot = proto.SyncdSnapshot.decode(data);
}
for (let { content } of patches) {
if (content) {
if (!Buffer.isBuffer(content)) {
content = Buffer.from(Object.values(content));
}
const syncd = proto.SyncdPatch.decode(content);
if (!syncd.version) {
syncd.version = { version: +collectionNode.attrs.version + 1 };
}
syncds.push(syncd);
}
}
final[name] = { patches: syncds, hasMorePatches, snapshot };
})
);
return final;
};
export const downloadExternalBlob = async (blob, options) => {
const stream = await downloadContentFromMessage(blob, "md-app-state", { options });
const bufferArray = [];
for await (const chunk of stream) {
bufferArray.push(chunk);
}
return Buffer.concat(bufferArray);
};
export const downloadExternalPatch = async (blob, options) => {
const buffer = await downloadExternalBlob(blob, options);
const syncData = proto.SyncdMutations.decode(buffer);
return syncData;
};
export const decodeSyncdSnapshot = async (
name,
snapshot,
getAppStateSyncKey,
minimumVersionNumber,
validateMacs = true
) => {
const newState = newLTHashState();
newState.version = toNumber(snapshot.version.version);
const mutationMap = {};
const areMutationsRequired =
typeof minimumVersionNumber === "undefined" || newState.version > minimumVersionNumber;
const { hash, indexValueMap } = await decodeSyncdMutations(
snapshot.records,
newState,
getAppStateSyncKey,
areMutationsRequired
? (mutation) => {
const index = mutation.syncAction.index?.toString();
mutationMap[index] = mutation;
}
: () => {},
validateMacs
);
newState.hash = hash;
newState.indexValueMap = indexValueMap;
if (validateMacs) {
const base64Key = Buffer.from(snapshot.keyId.id).toString("base64");
const keyEnc = await getAppStateSyncKey(base64Key);
if (!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`);
}
const result = await mutationKeys(keyEnc.keyData);
const computedSnapshotMac = generateSnapshotMac(
newState.hash,
newState.version,
name,
result.snapshotMacKey
);
if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) {
throw new Boom(
`failed to verify LTHash at ${newState.version} of ${name} from snapshot`
);
}
}
return {
state: newState,
mutationMap,
};
};
export const decodePatches = async (
name,
syncds,
initial,
getAppStateSyncKey,
options,
minimumVersionNumber,
logger,
validateMacs = true
) => {
const newState = {
...initial,
indexValueMap: { ...initial.indexValueMap },
};
const mutationMap = {};
for (const syncd of syncds) {
const { version, keyId, snapshotMac } = syncd;
if (syncd.externalMutations) {
logger?.trace({ name, version }, "downloading external patch");
const ref = await downloadExternalPatch(syncd.externalMutations, options);
logger?.debug(
{ name, version, mutations: ref.mutations.length },
"downloaded external patch"
);
syncd.mutations?.push(...ref.mutations);
}
const patchVersion = toNumber(version.version);
newState.version = patchVersion;
const shouldMutate =
typeof minimumVersionNumber === "undefined" || patchVersion > minimumVersionNumber;
const decodeResult = await decodeSyncdPatch(
syncd,
name,
newState,
getAppStateSyncKey,
shouldMutate
? (mutation) => {
const index = mutation.syncAction.index?.toString();
mutationMap[index] = mutation;
}
: () => {},
true
);
newState.hash = decodeResult.hash;
newState.indexValueMap = decodeResult.indexValueMap;
if (validateMacs) {
const base64Key = Buffer.from(keyId.id).toString("base64");
const keyEnc = await getAppStateSyncKey(base64Key);
if (!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`);
}
const result = await mutationKeys(keyEnc.keyData);
const computedSnapshotMac = generateSnapshotMac(
newState.hash,
newState.version,
name,
result.snapshotMacKey
);
if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) {
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`);
}
}
// clear memory used up by the mutations
syncd.mutations = [];
}
return { state: newState, mutationMap };
};
export const chatModificationToAppPatch = (mod, jid) => {
const OP = proto.SyncdMutation.SyncdOperation;
const getMessageRange = (lastMessages) => {
let messageRange;
if (Array.isArray(lastMessages)) {
const lastMsg = lastMessages[lastMessages.length - 1];
messageRange = {
lastMessageTimestamp: lastMsg?.messageTimestamp,
messages: lastMessages?.length
? lastMessages.map((m) => {
if (!m.key?.id || !m.key?.remoteJid) {
throw new Boom("Incomplete key", { statusCode: 400, data: m });
}
if (isJidGroup(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) {
throw new Boom("Expected not from me message to have participant", {
statusCode: 400,
data: m,
});
}
if (!m.messageTimestamp || !toNumber(m.messageTimestamp)) {
throw new Boom("Missing timestamp in last message list", {
statusCode: 400,
data: m,
});
}
if (m.key.participant) {
m.key.participant = jidNormalizedUser(m.key.participant);
}
return m;
})
: undefined,
};
} else {
messageRange = lastMessages;
}
return messageRange;
};
let patch;
if ("mute" in mod) {
patch = {
syncAction: {
muteAction: {
muted: !!mod.mute,
muteEndTimestamp: mod.mute || undefined,
},
},
index: ["mute", jid],
type: "regular_high",
apiVersion: 2,
operation: OP.SET,
};
} else if ("archive" in mod) {
patch = {
syncAction: {
archiveChatAction: {
archived: !!mod.archive,
messageRange: getMessageRange(mod.lastMessages),
},
},
index: ["archive", jid],
type: "regular_low",
apiVersion: 3,
operation: OP.SET,
};
} else if ("markRead" in mod) {
patch = {
syncAction: {
markChatAsReadAction: {
read: mod.markRead,
messageRange: getMessageRange(mod.lastMessages),
},
},
index: ["markChatAsRead", jid],
type: "regular_low",
apiVersion: 3,
operation: OP.SET,
};
} else if ("deleteForMe" in mod) {
const { timestamp, key, deleteMedia } = mod.deleteForMe;
patch = {
syncAction: {
deleteMessageForMeAction: {
deleteMedia,
messageTimestamp: timestamp,
},
},
index: ["deleteMessageForMe", jid, key.id, key.fromMe ? "1" : "0", "0"],
type: "regular_high",
apiVersion: 3,
operation: OP.SET,
};
} else if ("clear" in mod) {
patch = {
syncAction: {
clearChatAction: {
messageRange: getMessageRange(mod.lastMessages),
},
},
index: [
"clearChat",
jid,
"1" /*the option here is 0 when keep starred messages is enabled*/,
"0",
],
type: "regular_high",
apiVersion: 6,
operation: OP.SET,
};
} else if ("pin" in mod) {
patch = {
syncAction: {
pinAction: {
pinned: !!mod.pin,
},
},
index: ["pin_v1", jid],
type: "regular_low",
apiVersion: 5,
operation: OP.SET,
};
} else if ("contact" in mod) {
patch = {
syncAction: {
contactAction: mod.contact || {},
},
index: ["contact", jid],
type: "critical_unblock_low",
apiVersion: 2,
operation: mod.contact ? OP.SET : OP.REMOVE,
};
} else if ("disableLinkPreviews" in mod) {
patch = {
syncAction: {
privacySettingDisableLinkPreviewsAction: mod.disableLinkPreviews || {},
},
index: ["setting_disableLinkPreviews"],
type: "regular",
apiVersion: 8,
operation: OP.SET,
};
} else if ("star" in mod) {
const key = mod.star.messages[0];
patch = {
syncAction: {
starAction: {
starred: !!mod.star.star,
},
},
index: ["star", jid, key.id, key.fromMe ? "1" : "0", "0"],
type: "regular_low",
apiVersion: 2,
operation: OP.SET,
};
} else if ("delete" in mod) {
patch = {
syncAction: {
deleteChatAction: {
messageRange: getMessageRange(mod.lastMessages),
},
},
index: ["deleteChat", jid, "1"],
type: "regular_high",
apiVersion: 6,
operation: OP.SET,
};
} else if ("pushNameSetting" in mod) {
patch = {
syncAction: {
pushNameSetting: {
name: mod.pushNameSetting,
},
},
index: ["setting_pushName"],
type: "critical_block",
apiVersion: 1,
operation: OP.SET,
};
} else if ("quickReply" in mod) {
patch = {
syncAction: {
quickReplyAction: {
count: 0,
deleted: mod.quickReply.deleted || false,
keywords: [],
message: mod.quickReply.message || "",
shortcut: mod.quickReply.shortcut || "",
},
},
index: [
"quick_reply",
mod.quickReply.timestamp || String(Math.floor(Date.now() / 1000)),
],
type: "regular",
apiVersion: 2,
operation: OP.SET,
};
} else if ("addLabel" in mod) {
patch = {
syncAction: {
labelEditAction: {
name: mod.addLabel.name,
color: mod.addLabel.color,
predefinedId: mod.addLabel.predefinedId,
deleted: mod.addLabel.deleted,
},
},
index: ["label_edit", mod.addLabel.id],
type: "regular",
apiVersion: 3,
operation: OP.SET,
};
} else if ("addChatLabel" in mod) {
patch = {
syncAction: {
labelAssociationAction: {
labeled: true,
},
},
index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid],
type: "regular",
apiVersion: 3,
operation: OP.SET,
};
} else if ("removeChatLabel" in mod) {
patch = {
syncAction: {
labelAssociationAction: {
labeled: false,
},
},
index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid],
type: "regular",
apiVersion: 3,
operation: OP.SET,
};
} else if ("addMessageLabel" in mod) {
patch = {
syncAction: {
labelAssociationAction: {
labeled: true,
},
},
index: [
LabelAssociationType.Message,
mod.addMessageLabel.labelId,
jid,
mod.addMessageLabel.messageId,
"0",
"0",
],
type: "regular",
apiVersion: 3,
operation: OP.SET,
};
} else if ("removeMessageLabel" in mod) {
patch = {
syncAction: {
labelAssociationAction: {
labeled: false,
},
},
index: [
LabelAssociationType.Message,
mod.removeMessageLabel.labelId,
jid,
mod.removeMessageLabel.messageId,
"0",
"0",
],
type: "regular",
apiVersion: 3,
operation: OP.SET,
};
} else {
throw new Boom("not supported");
}
patch.syncAction.timestamp = Date.now();
return patch;
};
export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) => {
const isInitialSync = !!initialSyncOpts;
const accountSettings = initialSyncOpts?.accountSettings;
logger?.trace({ syncAction, initialSync: !!initialSyncOpts }, "processing sync action");
const {
syncAction: { value: action },
index: [type, id, msgId, fromMe],
} = syncAction;
if (action?.muteAction) {
ev.emit("chats.update", [
{
id,
muteEndTime: action.muteAction?.muted
? toNumber(action.muteAction.muteEndTimestamp)
: null,
conditional: getChatUpdateConditional(id, undefined),
},
]);
} else if (action?.archiveChatAction || type === "archive" || type === "unarchive") {
// okay so we've to do some annoying computation here
// when we're initially syncing the app state
// there are a few cases we need to handle
// 1. if the account unarchiveChats setting is true
// a. if the chat is archived, and no further messages have been received -- simple, keep archived
// b. if the chat was archived, and the user received messages from the other person afterwards
// then the chat should be marked unarchved --
// we compare the timestamp of latest message from the other person to determine this
// 2. if the account unarchiveChats setting is false -- then it doesn't matter,
// it'll always take an app state action to mark in unarchived -- which we'll get anyway
const archiveAction = action?.archiveChatAction;
const isArchived = archiveAction ? archiveAction.archived : type === "archive";
// // basically we don't need to fire an "archive" update if the chat is being marked unarchvied
// // this only applies for the initial sync
// if(isInitialSync && !isArchived) {
// isArchived = false
// }
const msgRange = !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange;
// logger?.debug({ chat: id, syncAction }, 'message range archive')
ev.emit("chats.update", [
{
id,
archived: isArchived,
conditional: getChatUpdateConditional(id, msgRange),
},
]);
} else if (action?.markChatAsReadAction) {
const markReadAction = action.markChatAsReadAction;
// basically we don't need to fire an "read" update if the chat is being marked as read
// because the chat is read by default
// this only applies for the initial sync
const isNullUpdate = isInitialSync && markReadAction.read;
ev.emit("chats.update", [
{
id,
unreadCount: isNullUpdate ? null : !!markReadAction?.read ? 0 : -1,
conditional: getChatUpdateConditional(id, markReadAction?.messageRange),
},
]);
} else if (action?.deleteMessageForMeAction || type === "deleteMessageForMe") {
ev.emit("messages.delete", {
keys: [
{
remoteJid: id,
id: msgId,
fromMe: fromMe === "1",
},
],
});
} else if (action?.contactAction) {
ev.emit("contacts.upsert", [
{
id: id,
name: action.contactAction.fullName,
lid: action.contactAction.lidJid || undefined,
phoneNumber: action.contactAction.pnJid || undefined,
},
]);
} else if (action?.pushNameSetting) {
const name = action?.pushNameSetting?.name;
if (name && me?.name !== name) {
ev.emit("creds.update", { me: { ...me, name } });
}
} else if (action?.pinAction) {
ev.emit("chats.update", [
{
id,
pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null,
conditional: getChatUpdateConditional(id, undefined),
},
]);
} else if (action?.unarchiveChatsSetting) {
const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats;
ev.emit("creds.update", { accountSettings: { unarchiveChats } });
logger?.info(`archive setting updated => '${action.unarchiveChatsSetting.unarchiveChats}'`);
if (accountSettings) {
accountSettings.unarchiveChats = unarchiveChats;
}
} else if (action?.starAction || type === "star") {
let starred = action?.starAction?.starred;
if (typeof starred !== "boolean") {
starred = syncAction.index[syncAction.index.length - 1] === "1";
}
ev.emit("messages.update", [
{
key: { remoteJid: id, id: msgId, fromMe: fromMe === "1" },
update: { starred },
},
]);
} else if (action?.deleteChatAction || type === "deleteChat") {
if (!isInitialSync) {
ev.emit("chats.delete", [id]);
}
} else if (action?.labelEditAction) {
const { name, color, deleted, predefinedId } = action.labelEditAction;
ev.emit("labels.edit", {
id: id,
name: name,
color: color,
deleted: deleted,
predefinedId: predefinedId ? String(predefinedId) : undefined,
});
} else if (action?.labelAssociationAction) {
ev.emit("labels.association", {
type: action.labelAssociationAction.labeled ? "add" : "remove",
association:
type === LabelAssociationType.Chat
? {
type: LabelAssociationType.Chat,
chatId: syncAction.index[2],
labelId: syncAction.index[1],
}
: {
type: LabelAssociationType.Message,
chatId: syncAction.index[2],
messageId: syncAction.index[3],
labelId: syncAction.index[1],
},
});
} else {
logger?.debug({ syncAction, id }, "unprocessable update");
}
function getChatUpdateConditional(id, msgRange) {
return isInitialSync
? (data) => {
const chat = data.historySets.chats[id] || data.chatUpserts[id];
if (chat) {
return msgRange ? isValidPatchBasedOnMessageRange(chat, msgRange) : true;
}
}
: undefined;
}
function isValidPatchBasedOnMessageRange(chat, msgRange) {
const lastMsgTimestamp = Number(
msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0
);
const chatLastMsgTimestamp = Number(chat?.lastMessageRecvTimestamp || 0);
return lastMsgTimestamp >= chatLastMsgTimestamp;
}
};
//# sourceMappingURL=chat-utils.js.map