@baileys-md/baileys
Version:
Baileys WhatsApp API
342 lines (332 loc) • 14.2 kB
JavaScript
//========================================//
import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, jidDecode, jidEncode, jidNormalizedUser } from "../WABinary/index.js";
import { getContentType, normalizeMessageContent } from "../Utils/messages.js";
import { downloadAndProcessHistorySyncNotification } from "./history.js";
import { WAMessageStubType } from "../Types/index.js";
import { aesDecryptGCM, hmacSign } from "./crypto.js";
import { proto } from "../../WAProto/index.js";
import { toNumber } from "./generics.js";
const REAL_MSG_STUB_TYPES = new Set([
WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
WAMessageStubType.CALL_MISSED_GROUP_VOICE,
WAMessageStubType.CALL_MISSED_VIDEO,
WAMessageStubType.CALL_MISSED_VOICE
]);
const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD]);
export const cleanMessage = (message, meId, meLid) => {
if (isHostedPnUser(message.key.remoteJid) || isHostedLidUser(message.key.remoteJid)) {
message.key.remoteJid = jidEncode(jidDecode(message.key?.remoteJid)?.user, isHostedPnUser(message.key.remoteJid) ? "s.whatsapp.net" : "lid");
}
else {
message.key.remoteJid = jidNormalizedUser(message.key.remoteJid);
}
if (isHostedPnUser(message.key.participant) || isHostedLidUser(message.key.participant)) {
message.key.participant = jidEncode(jidDecode(message.key.participant)?.user, isHostedPnUser(message.key.participant) ? "s.whatsapp.net" : "lid");
}
else {
message.key.participant = jidNormalizedUser(message.key.participant);
}
const content = normalizeMessageContent(message.message);
if (content?.reactionMessage) {
normaliseKey(content.reactionMessage.key);
}
if (content?.pollUpdateMessage) {
normaliseKey(content.pollUpdateMessage.pollCreationMessageKey);
}
function normaliseKey(msgKey) {
if (!message.key.fromMe) {
msgKey.fromMe = !msgKey.fromMe
? areJidsSameUser(msgKey.participant || msgKey.remoteJid, meId) ||
areJidsSameUser(msgKey.participant || msgKey.remoteJid, meLid)
:
false;
msgKey.remoteJid = message.key.remoteJid;
msgKey.participant = msgKey.participant || message.key.participant;
}
}
};
export const isRealMessage = (message) => {
const normalizedContent = normalizeMessageContent(message.message);
const hasSomeContent = !!getContentType(normalizedContent);
return ((!!normalizedContent ||
REAL_MSG_STUB_TYPES.has(message.messageStubType) ||
REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType)) &&
hasSomeContent &&
!normalizedContent?.protocolMessage &&
!normalizedContent?.reactionMessage &&
!normalizedContent?.pollUpdateMessage);
};
export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !message.messageStubType;
export const getChatId = ({ remoteJid, participant, fromMe }) => {
if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) {
return participant;
}
return remoteJid;
};
export function decryptPollVote({ encPayload, encIv }, { pollCreatorJid, pollMsgId, pollEncKey, voterJid }) {
const sign = Buffer.concat([
toBinary(pollMsgId),
toBinary(pollCreatorJid),
toBinary(voterJid),
toBinary("Poll Vote"),
new Uint8Array([1])
]);
const key0 = hmacSign(pollEncKey, new Uint8Array(32), "sha256");
const decKey = hmacSign(sign, key0, "sha256");
const aad = toBinary(`${pollMsgId}\u0000${voterJid}`);
const decrypted = aesDecryptGCM(encPayload, decKey, encIv, aad);
return proto.Message.PollVoteMessage.decode(decrypted);
function toBinary(txt) {
return Buffer.from(txt);
}
}
const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options }) => {
const meId = creds.me.id;
const { accountSettings } = creds;
const chat = { id: jidNormalizedUser(getChatId(message.key)) };
const isRealMsg = isRealMessage(message);
if (isRealMsg) {
chat.messages = [{ message }];
chat.conversationTimestamp = toNumber(message.messageTimestamp);
if (shouldIncrementChatUnread(message)) {
chat.unreadCount = (chat.unreadCount || 0) + 1;
}
}
const content = normalizeMessageContent(message.message);
if ((isRealMsg || content?.reactionMessage?.key?.fromMe) && accountSettings?.unarchiveChats) {
chat.archived = false;
chat.readOnly = false;
}
const protocolMsg = content?.protocolMessage;
if (protocolMsg) {
switch (protocolMsg.type) {
case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
const histNotification = protocolMsg.historySyncNotification;
const process = shouldProcessHistoryMsg;
const isLatest = !creds.processedHistoryMessages?.length;
logger?.info({
histNotification,
process,
id: message.key.id,
isLatest
}, "got history notification");
if (process) {
if (histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
ev.emit("creds.update", {
processedHistoryMessages: [
...(creds.processedHistoryMessages || []),
{ key: message.key, messageTimestamp: message.messageTimestamp }
]
});
}
const data = await downloadAndProcessHistorySyncNotification(histNotification, options);
ev.emit("messaging-history.set", {
...data,
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
});
}
break;
case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE:
const keys = protocolMsg.appStateSyncKeyShare.keys;
if (keys?.length) {
let newAppStateSyncKeyId = "";
await keyStore.transaction(async () => {
const newKeys = [];
for (const { keyData, keyId } of keys) {
const strKeyId = Buffer.from(keyId.keyId).toString("base64");
newKeys.push(strKeyId);
await keyStore.set({ "app-state-sync-key": { [strKeyId]: keyData } });
newAppStateSyncKeyId = strKeyId;
}
logger?.info({ newAppStateSyncKeyId, newKeys }, "injecting new app state sync keys");
}, meId);
ev.emit("creds.update", { myAppStateKeyId: newAppStateSyncKeyId });
}
else {
logger?.info({ protocolMsg }, "recv app state sync with 0 keys");
}
break;
case proto.Message.ProtocolMessage.Type.REVOKE:
ev.emit("messages.update", [
{
key: {
...message.key,
id: protocolMsg.key.id
},
update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key }
}
]);
break;
case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING:
Object.assign(chat, {
ephemeralSettingTimestamp: toNumber(message.messageTimestamp),
ephemeralExpiration: protocolMsg.ephemeralExpiration || null
});
break;
case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
const response = protocolMsg.peerDataOperationRequestResponseMessage;
if (response) {
await placeholderResendCache?.del(response.stanzaId);
const { peerDataOperationResult } = response;
for (const result of peerDataOperationResult) {
const { placeholderMessageResendResponse: retryResponse } = result;
if (retryResponse) {
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes);
setTimeout(() => {
ev.emit("messages.upsert", {
messages: [webMessageInfo],
type: "notify",
requestId: response.stanzaId
});
}, 500);
}
}
}
break;
case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT:
ev.emit("messages.update", [
{
key: { ...message.key, id: protocolMsg.key?.id },
update: {
message: {
editedMessage: {
message: protocolMsg.editedMessage
}
},
messageTimestamp: protocolMsg.timestampMs
? Math.floor(toNumber(protocolMsg.timestampMs) / 1000)
: message.messageTimestamp
}
}
]);
break;
case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC:
const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload;
const { pnToLidMappings, chatDbMigrationTimestamp } = proto.LIDMigrationMappingSyncPayload.decode(encodedPayload);
logger?.debug({ pnToLidMappings, chatDbMigrationTimestamp }, "got lid mappings and chat db migration timestamp");
const pairs = [];
for (const { pn, latestLid, assignedLid } of pnToLidMappings) {
const lid = latestLid || assignedLid;
pairs.push({ lid: `${lid}@lid`, pn: `${pn}@s.whatsapp.net` });
}
await signalRepository.lidMapping.storeLIDPNMappings(pairs);
if (pairs.length) {
for (const { pn, lid } of pairs) {
await signalRepository.migrateSession(pn, lid);
}
}
}
}
else if (content?.reactionMessage) {
const reaction = {
...content.reactionMessage,
key: message.key
};
ev.emit("messages.reaction", [
{
reaction,
key: content.reactionMessage?.key
}
]);
}
else if (message.messageStubType) {
const jid = message.key?.remoteJid;
let participants;
const emitParticipantsUpdate = (action) => ev.emit("group-participants.update", {
id: jid,
author: message.key.participant,
authorPn: message.key.participantAlt,
participants,
action
});
const emitGroupUpdate = (update) => {
ev.emit("groups.update", [
{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt }
]);
};
const emitGroupRequestJoin = (participant, action, method) => {
ev.emit("group.join-request", {
id: jid,
author: message.key.participant,
authorPn: message.key.participantAlt,
participant: participant.lid,
participantPn: participant.pn,
action,
method: method
});
};
const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid.phoneNumber));
switch (message.messageStubType) {
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
emitParticipantsUpdate("modify");
break;
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
emitParticipantsUpdate("remove");
if (participantsIncludesMe()) {
chat.readOnly = true;
}
break;
case WAMessageStubType.GROUP_PARTICIPANT_ADD:
case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
if (participantsIncludesMe()) {
chat.readOnly = false;
}
emitParticipantsUpdate("add");
break;
case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE:
participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
emitParticipantsUpdate("demote");
break;
case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE:
participants = message.messageStubParameters.map((a) => JSON.parse(a)) || [];
emitParticipantsUpdate("promote");
break;
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
const announceValue = message.messageStubParameters?.[0];
emitGroupUpdate({ announce: announceValue === "true" || announceValue === "on" });
break;
case WAMessageStubType.GROUP_CHANGE_RESTRICT:
const restrictValue = message.messageStubParameters?.[0];
emitGroupUpdate({ restrict: restrictValue === "true" || restrictValue === "on" });
break;
case WAMessageStubType.GROUP_CHANGE_SUBJECT:
const name = message.messageStubParameters?.[0];
chat.name = name;
emitGroupUpdate({ subject: name });
break;
case WAMessageStubType.GROUP_CHANGE_DESCRIPTION:
const description = message.messageStubParameters?.[0];
chat.description = description;
emitGroupUpdate({ desc: description });
break;
case WAMessageStubType.GROUP_CHANGE_INVITE_LINK:
const code = message.messageStubParameters?.[0];
emitGroupUpdate({ inviteCode: code });
break;
case WAMessageStubType.GROUP_MEMBER_ADD_MODE:
const memberAddValue = message.messageStubParameters?.[0];
emitGroupUpdate({ memberAddMode: memberAddValue === "all_member_add" });
break;
case WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE:
const approvalMode = message.messageStubParameters?.[0];
emitGroupUpdate({ joinApprovalMode: approvalMode === "on" });
break;
case WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD:
const participant = JSON.parse(message.messageStubParameters?.[0]);
const action = message.messageStubParameters?.[1];
const method = message.messageStubParameters?.[2];
emitGroupRequestJoin(participant, action, method);
break;
}
}
if (Object.keys(chat).length > 1) {
ev.emit("chats.update", [chat]);
}
};
export default processMessage;