diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index f9a767ca..c938c645 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -63,7 +63,8 @@ export const makeChatsSocket = (config: SocketConfig) => { fireInitQueries, appStateMacVerification, shouldIgnoreJid, - shouldSyncHistoryMessage + shouldSyncHistoryMessage, + getMessage } = config const sock = makeSocket(config) const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError } = sock @@ -1086,7 +1087,8 @@ export const makeChatsSocket = (config: SocketConfig) => { creds: authState.creds, keyStore: authState.keys, logger, - options: config.options + options: config.options, + getMessage }) ]) diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index ff914514..d308e62e 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -45,7 +45,7 @@ export const BufferJSON = { } export const getKeyAuthor = (key: WAMessageKey | undefined | null, meId = 'me') => - (key?.fromMe ? meId : key?.participant || key?.remoteJid) || '' + (key?.fromMe ? meId : key?.participantAlt || key?.remoteJidAlt || key?.participant || key?.remoteJid) || '' export const writeRandomPadMax16 = (msg: Uint8Array) => { const pad = randomBytes(1) diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index f2085657..a92aa6ed 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -847,6 +847,19 @@ export const updateMessageWithPollUpdate = (msg: Pick, msg.pollUpdates = reactions } +/** Update the message with a new event response */ +export const updateMessageWithEventResponse = ( + msg: Pick, + update: proto.IEventResponse +) => { + const authorID = getKeyAuthor(update.eventResponseMessageKey) + + const responses = (msg.eventResponses || []).filter(r => getKeyAuthor(r.eventResponseMessageKey) !== authorID) + responses.push(update) + + msg.eventResponses = responses +} + type VoteAggregation = { name: string voters: string[] @@ -903,6 +916,41 @@ export function getAggregateVotesInPollMessage( return Object.values(voteHashMap) } +type ResponseAggregation = { + response: string + responders: string[] +} + +/** + * Aggregates all event responses in an event message. + * @param msg the event creation message + * @param meId your jid + * @returns A list of response types & their responders + */ +export function getAggregateResponsesInEventMessage( + { eventResponses }: Pick, + meId?: string +) { + const responseTypes = ['GOING', 'NOT_GOING', 'MAYBE'] + const responseMap: { [_: string]: ResponseAggregation } = {} + + for (const type of responseTypes) { + responseMap[type] = { + response: type, + responders: [] + } + } + + for (const update of eventResponses || []) { + const responseType = (update as any).eventResponse || 'UNKNOWN' + if (responseType !== 'UNKNOWN' && responseMap[responseType]) { + responseMap[responseType].responders.push(getKeyAuthor(update.eventResponseMessageKey, meId)) + } + } + + return Object.values(responseMap) +} + /** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */ export const aggregateMessageKeysNotFromMe = (keys: WAMessageKey[]) => { const keyMap: { [id: string]: { jid: string; participant: string | undefined; messageIds: string[] } } = {} diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index b939d7a3..e6988480 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -12,6 +12,7 @@ import type { RequestJoinMethod, SignalKeyStoreWithTransaction, SignalRepositoryWithLIDStore, + SocketConfig, WAMessage, WAMessageKey } from '../Types' @@ -23,12 +24,13 @@ import { isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, + isLidUser, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary' import { aesDecryptGCM, hmacSign } from './crypto' -import { toNumber } from './generics' +import { getKeyAuthor, toNumber } from './generics' import { downloadAndProcessHistorySyncNotification } from './history' import type { ILogger } from './logger' @@ -41,6 +43,7 @@ type ProcessMessageContext = { logger?: ILogger options: RequestInit signalRepository: SignalRepositoryWithLIDStore + getMessage: SocketConfig['getMessage'] } const REAL_MSG_STUB_TYPES = new Set([ @@ -144,6 +147,17 @@ type PollContext = { voterJid: string } +type EventContext = { + /** normalised jid of the person that created the event */ + eventCreatorJid: string + /** ID of the event creation message */ + eventMsgId: string + /** event creation message enc key */ + eventEncKey: Uint8Array + /** jid of the person that responded */ + responderJid: string +} + /** * Decrypt a poll vote * @param vote encrypted vote @@ -174,6 +188,36 @@ export function decryptPollVote( } } +/** + * Decrypt an event response + * @param response encrypted event response + * @param ctx additional info about the event required for decryption + * @returns event response message + */ +export function decryptEventResponse( + { encPayload, encIv }: proto.Message.IPollEncValue, + { eventCreatorJid, eventMsgId, eventEncKey, responderJid }: EventContext +) { + const sign = Buffer.concat([ + toBinary(eventMsgId), + toBinary(eventCreatorJid), + toBinary(responderJid), + toBinary('Event Response'), + new Uint8Array([1]) + ]) + + const key0 = hmacSign(eventEncKey, new Uint8Array(32), 'sha256') + const decKey = hmacSign(sign, key0, 'sha256') + const aad = toBinary(`${eventMsgId}\u0000${responderJid}`) + + const decrypted = aesDecryptGCM(encPayload!, decKey, encIv!, aad) + return proto.Message.EventResponseMessage.decode(decrypted) + + function toBinary(txt: string) { + return Buffer.from(txt) + } +} + const processMessage = async ( message: WAMessage, { @@ -184,7 +228,8 @@ const processMessage = async ( signalRepository, keyStore, logger, - options + options, + getMessage }: ProcessMessageContext ) => { const meId = creds.me!.id @@ -363,6 +408,60 @@ const processMessage = async ( key: content.reactionMessage?.key! } ]) + } else if (content?.encEventResponseMessage) { + const encEventResponse = content.encEventResponseMessage + const creationMsgKey = encEventResponse.eventCreationMessageKey! + + // we need to fetch the event creation message to get the event enc key + const eventMsg = await getMessage(creationMsgKey) + if (eventMsg) { + try { + const meIdNormalised = jidNormalizedUser(meId) + + // all jids need to be PN + const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid! + const eventCreatorPn = isLidUser(eventCreatorKey) + ? await signalRepository.lidMapping.getPNForLID(eventCreatorKey) + : eventCreatorKey + const eventCreatorJid = getKeyAuthor( + { remoteJid: jidNormalizedUser(eventCreatorPn!), fromMe: meIdNormalised === eventCreatorPn }, + meIdNormalised + ) + + const responderJid = getKeyAuthor(message.key, meIdNormalised) + const eventEncKey = eventMsg?.messageContextInfo?.messageSecret + + if (!eventEncKey) { + logger?.warn({ creationMsgKey }, 'event response: missing messageSecret for decryption') + } else { + const responseMsg = decryptEventResponse(encEventResponse, { + eventEncKey, + eventCreatorJid, + eventMsgId: creationMsgKey.id!, + responderJid + }) + + const eventResponse = { + eventResponseMessageKey: message.key, + senderTimestampMs: responseMsg.timestampMs!, + response: responseMsg + } + + ev.emit('messages.update', [ + { + key: creationMsgKey, + update: { + eventResponses: [eventResponse] + } + } + ]) + } + } catch (err) { + logger?.warn({ err, creationMsgKey }, 'failed to decrypt event response') + } + } else { + logger?.warn({ creationMsgKey }, 'event creation message not found, cannot decrypt response') + } } else if (message.messageStubType) { const jid = message.key?.remoteJid! //let actor = whatsappID (message.participant)