7e88ddb858
Files fixed: - messages-recv.ts: 58 assertions → null guards, meId extraction, optional chaining - process-message.ts: 38 assertions → remoteJid/participant guards, proto field checks - chat-utils.ts: 31 assertions → proto field validation with Boom errors - messages-send.ts: 20 assertions → meId extraction, participant guards - chats.ts: 15 assertions → me guard, firstChild guard, jid guards - messages.ts: 14 assertions → originalFilePath guard, key guards - groups.ts: 12 assertions → attrs fallbacks with ?? - validate-connection.ts: 9 assertions → grouped proto field validation - generics.ts: 1 assertion → versionLine guard - history.ts: 2 assertions → key guards - libsignal.ts: 1 assertion → deviceId guard - sender-key-message.ts: 3 assertions → proto guards - UsyncBotProfileProtocol.ts: 2 assertions → attrs guards - example.ts: 3 assertions → optional chaining https://claude.ai/code/session_01E2cfX1N3sJgCJBTvzGazSG
786 lines
24 KiB
TypeScript
786 lines
24 KiB
TypeScript
import { proto } from '../../WAProto/index.js'
|
|
import type {
|
|
AuthenticationCreds,
|
|
BaileysEventEmitter,
|
|
CacheStore,
|
|
Chat,
|
|
GroupMetadata,
|
|
GroupParticipant,
|
|
LIDMapping,
|
|
ParticipantAction,
|
|
RequestJoinAction,
|
|
RequestJoinMethod,
|
|
SignalKeyStoreWithTransaction,
|
|
SignalRepositoryWithLIDStore,
|
|
SocketConfig,
|
|
WAMessage,
|
|
WAMessageKey
|
|
} from '../Types'
|
|
import { metrics, recordHistorySyncMessages } from './prometheus-metrics.js'
|
|
import { WAMessageStubType } from '../Types'
|
|
import { getContentType, normalizeMessageContent } from '../Utils/messages'
|
|
import {
|
|
areJidsSameUser,
|
|
isHostedLidUser,
|
|
isHostedPnUser,
|
|
isJidBroadcast,
|
|
isJidStatusBroadcast,
|
|
isLidUser,
|
|
jidDecode,
|
|
jidEncode,
|
|
jidNormalizedUser
|
|
} from '../WABinary'
|
|
import { aesDecryptGCM, hmacSign } from './crypto'
|
|
import { getKeyAuthor, toNumber } from './generics'
|
|
import { downloadAndProcessHistorySyncNotification } from './history'
|
|
import type { ILogger } from './logger'
|
|
|
|
type ProcessMessageContext = {
|
|
shouldProcessHistoryMsg: boolean
|
|
placeholderResendCache?: CacheStore
|
|
creds: AuthenticationCreds
|
|
keyStore: SignalKeyStoreWithTransaction
|
|
ev: BaileysEventEmitter
|
|
logger?: ILogger
|
|
options: RequestInit
|
|
signalRepository: SignalRepositoryWithLIDStore
|
|
getMessage: SocketConfig['getMessage']
|
|
}
|
|
|
|
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])
|
|
|
|
/** Cleans a received message to further processing */
|
|
export const cleanMessage = (message: WAMessage, meId: string, meLid: string) => {
|
|
// ensure remoteJid and participant doesn't have device or agent in it
|
|
const remoteJid = message.key.remoteJid ?? ''
|
|
if (isHostedPnUser(remoteJid) || isHostedLidUser(remoteJid)) {
|
|
message.key.remoteJid = jidEncode(
|
|
jidDecode(remoteJid)?.user ?? '',
|
|
isHostedPnUser(remoteJid) ? 's.whatsapp.net' : 'lid'
|
|
)
|
|
} else {
|
|
message.key.remoteJid = jidNormalizedUser(remoteJid)
|
|
}
|
|
|
|
const participantJid = message.key.participant ?? ''
|
|
if (isHostedPnUser(participantJid) || isHostedLidUser(participantJid)) {
|
|
message.key.participant = jidEncode(
|
|
jidDecode(participantJid)?.user ?? '',
|
|
isHostedPnUser(participantJid) ? 's.whatsapp.net' : 'lid'
|
|
)
|
|
} else {
|
|
message.key.participant = jidNormalizedUser(participantJid)
|
|
}
|
|
|
|
const content = normalizeMessageContent(message.message)
|
|
// if the message has a reaction, ensure fromMe & remoteJid are from our perspective
|
|
if (content?.reactionMessage) {
|
|
const reactionKey = content.reactionMessage.key
|
|
if (reactionKey) {
|
|
normaliseKey(reactionKey)
|
|
}
|
|
}
|
|
|
|
if (content?.pollUpdateMessage) {
|
|
const pollCreationKey = content.pollUpdateMessage.pollCreationMessageKey
|
|
if (pollCreationKey) {
|
|
normaliseKey(pollCreationKey)
|
|
}
|
|
}
|
|
|
|
function normaliseKey(msgKey: WAMessageKey) {
|
|
// if the reaction is from another user
|
|
// we've to correctly map the key to this user's perspective
|
|
if (!message.key.fromMe) {
|
|
// if the sender believed the message being reacted to is not from them
|
|
// we've to correct the key to be from them, or some other participant
|
|
msgKey.fromMe = !msgKey.fromMe
|
|
? areJidsSameUser(msgKey.participant || (msgKey.remoteJid ?? ''), meId) ||
|
|
areJidsSameUser(msgKey.participant || (msgKey.remoteJid ?? ''), meLid)
|
|
: // if the message being reacted to, was from them
|
|
// fromMe automatically becomes false
|
|
false
|
|
// set the remoteJid to being the same as the chat the message came from
|
|
// TODO: investigate inconsistencies
|
|
msgKey.remoteJid = message.key.remoteJid
|
|
// set participant of the message
|
|
msgKey.participant = msgKey.participant || message.key.participant
|
|
}
|
|
}
|
|
}
|
|
|
|
export const normalizeMessageJids = async (
|
|
message: WAMessage,
|
|
signalRepository: SignalRepositoryWithLIDStore,
|
|
logger?: ILogger
|
|
): Promise<void> => {
|
|
const resolveLidToPn = async (jid: string | undefined | null): Promise<string | undefined> => {
|
|
if (!jid) {
|
|
return undefined
|
|
}
|
|
|
|
if (isLidUser(jid) || isHostedLidUser(jid)) {
|
|
const pn = await signalRepository.lidMapping.getPNForLID(jid)
|
|
if (pn) {
|
|
logger?.debug({ lid: jid, pn }, 'Resolved LID to PN for inbound message')
|
|
} else {
|
|
logger?.debug({ lid: jid }, 'PN not found for inbound LID, keeping LID')
|
|
}
|
|
return pn || jid
|
|
}
|
|
|
|
return jid
|
|
}
|
|
|
|
// Execute both lookups in parallel instead of sequentially to reduce latency
|
|
const [resolvedRemoteJid, resolvedParticipant] = await Promise.all([
|
|
resolveLidToPn(message.key.remoteJid),
|
|
resolveLidToPn(message.key.participant)
|
|
])
|
|
|
|
if (resolvedRemoteJid) {
|
|
message.key.remoteJid = resolvedRemoteJid
|
|
}
|
|
|
|
if (resolvedParticipant) {
|
|
message.key.participant = resolvedParticipant
|
|
}
|
|
}
|
|
|
|
// TODO: target:audit AUDIT THIS FUNCTION AGAIN
|
|
export const isRealMessage = (message: WAMessage) => {
|
|
const normalizedContent = normalizeMessageContent(message.message)
|
|
const hasSomeContent = !!getContentType(normalizedContent)
|
|
const stubType = message.messageStubType ?? 0
|
|
return (
|
|
(!!normalizedContent ||
|
|
REAL_MSG_STUB_TYPES.has(stubType) ||
|
|
REAL_MSG_REQ_ME_STUB_TYPES.has(stubType)) &&
|
|
hasSomeContent &&
|
|
!normalizedContent?.protocolMessage &&
|
|
!normalizedContent?.reactionMessage &&
|
|
!normalizedContent?.pollUpdateMessage
|
|
)
|
|
}
|
|
|
|
export const shouldIncrementChatUnread = (message: WAMessage) => !message.key.fromMe && !message.messageStubType
|
|
|
|
/**
|
|
* Get the ID of the chat from the given key.
|
|
* Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
|
|
*/
|
|
export const getChatId = ({ remoteJid, participant, fromMe }: WAMessageKey) => {
|
|
const jid = remoteJid ?? ''
|
|
if (isJidBroadcast(jid) && !isJidStatusBroadcast(jid) && !fromMe) {
|
|
return participant ?? ''
|
|
}
|
|
|
|
return jid
|
|
}
|
|
|
|
type PollContext = {
|
|
/** normalised jid of the person that created the poll */
|
|
pollCreatorJid: string
|
|
/** ID of the poll creation message */
|
|
pollMsgId: string
|
|
/** poll creation message enc key */
|
|
pollEncKey: Uint8Array
|
|
/** jid of the person that voted */
|
|
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
|
|
* @param ctx additional info about the poll required for decryption
|
|
* @returns list of SHA256 options
|
|
*/
|
|
export function decryptPollVote(
|
|
{ encPayload, encIv }: proto.Message.IPollEncValue,
|
|
{ pollCreatorJid, pollMsgId, pollEncKey, voterJid }: PollContext
|
|
) {
|
|
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}`)
|
|
|
|
if (!encPayload || !encIv) {
|
|
throw new Error('Missing encPayload or encIv for poll vote decryption')
|
|
}
|
|
|
|
const decrypted = aesDecryptGCM(encPayload, decKey, encIv, aad)
|
|
return proto.Message.PollVoteMessage.decode(decrypted)
|
|
|
|
function toBinary(txt: string) {
|
|
return Buffer.from(txt)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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}`)
|
|
|
|
if (!encPayload || !encIv) {
|
|
throw new Error('Missing encPayload or encIv for event response decryption')
|
|
}
|
|
|
|
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,
|
|
{
|
|
shouldProcessHistoryMsg,
|
|
placeholderResendCache,
|
|
ev,
|
|
creds,
|
|
signalRepository,
|
|
keyStore,
|
|
logger,
|
|
options,
|
|
getMessage
|
|
}: ProcessMessageContext
|
|
) => {
|
|
const meUser = creds.me
|
|
if (!meUser) {
|
|
return
|
|
}
|
|
|
|
const meId = meUser.id
|
|
const { accountSettings } = creds
|
|
|
|
const chat: Partial<Chat> = { id: jidNormalizedUser(getChatId(message.key)) }
|
|
const isRealMsg = isRealMessage(message)
|
|
|
|
if (isRealMsg) {
|
|
chat.messages = [{ message }]
|
|
chat.conversationTimestamp = toNumber(message.messageTimestamp)
|
|
// only increment unread count if not CIPHERTEXT and from another person
|
|
if (shouldIncrementChatUnread(message)) {
|
|
chat.unreadCount = (chat.unreadCount || 0) + 1
|
|
}
|
|
}
|
|
|
|
const content = normalizeMessageContent(message.message)
|
|
|
|
// unarchive chat if it's a real message, or someone reacted to our message
|
|
// and we've the unarchive chats setting on
|
|
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
|
|
if (!histNotification) {
|
|
break
|
|
}
|
|
const process = shouldProcessHistoryMsg
|
|
const isLatest = !creds.processedHistoryMessages?.length
|
|
|
|
logger?.info(
|
|
{
|
|
histNotification,
|
|
process,
|
|
id: message.key.id,
|
|
isLatest
|
|
},
|
|
'got history notification'
|
|
)
|
|
|
|
if (process) {
|
|
// TODO: investigate
|
|
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, logger)
|
|
|
|
// Emit LID-PN mappings from history sync
|
|
// This is how WhatsApp Web learns mappings for chats with non-contacts
|
|
if (data.lidPnMappings?.length) {
|
|
logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync')
|
|
try {
|
|
const result = await signalRepository.lidMapping.storeLIDPNMappings(data.lidPnMappings)
|
|
logger?.debug(
|
|
{ stored: result.stored, skipped: result.skipped, errors: result.errors },
|
|
'stored LID-PN mappings from history sync'
|
|
)
|
|
if (result.stored > 0) {
|
|
logger?.info(
|
|
{ stored: result.stored },
|
|
'fallback LID mappings are now available from history sync'
|
|
)
|
|
}
|
|
} catch (error) {
|
|
logger?.warn({ error }, 'Failed to store LID-PN mappings from history sync')
|
|
}
|
|
|
|
// Emit all mappings at once for better performance
|
|
if (data.lidPnMappings.length > 0) {
|
|
ev.emit('lid-mapping.update', data.lidPnMappings)
|
|
}
|
|
}
|
|
|
|
ev.emit('messaging-history.set', {
|
|
...data,
|
|
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
|
|
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
|
|
})
|
|
|
|
// Record history sync metrics
|
|
if (data.messages?.length) {
|
|
recordHistorySyncMessages(data.messages.length)
|
|
}
|
|
}
|
|
|
|
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: string[] = []
|
|
for (const { keyData, keyId } of keys) {
|
|
const keyIdValue = keyId?.keyId
|
|
if (!keyIdValue) {
|
|
continue
|
|
}
|
|
|
|
const strKeyId = Buffer.from(keyIdValue).toString('base64')
|
|
newKeys.push(strKeyId)
|
|
|
|
if (keyData) {
|
|
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) {
|
|
if (response.stanzaId) {
|
|
await placeholderResendCache?.del(response.stanzaId)
|
|
}
|
|
// TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.).
|
|
const { peerDataOperationResult } = response
|
|
if (!peerDataOperationResult) {
|
|
break
|
|
}
|
|
|
|
let recoveredCount = 0
|
|
for (const result of peerDataOperationResult) {
|
|
const { placeholderMessageResendResponse: retryResponse } = result
|
|
//eslint-disable-next-line max-depth
|
|
if (retryResponse) {
|
|
if (!retryResponse.webMessageInfoBytes) {
|
|
continue
|
|
}
|
|
|
|
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes)
|
|
|
|
// Track CTWA message recovery success
|
|
recoveredCount++
|
|
logger?.info(
|
|
{
|
|
msgId: webMessageInfo.key?.id,
|
|
remoteJid: webMessageInfo.key?.remoteJid,
|
|
requestId: response.stanzaId
|
|
},
|
|
'CTWA: Successfully recovered message via placeholder resend'
|
|
)
|
|
|
|
// wait till another upsert event is available, don't want it to be part of the PDO response message
|
|
// TODO: parse through proper message handling utilities (to add relevant key fields)
|
|
ev.emit('messages.upsert', {
|
|
messages: [webMessageInfo as WAMessage],
|
|
type: 'notify',
|
|
requestId: response.stanzaId ?? ''
|
|
})
|
|
}
|
|
}
|
|
|
|
// Update metrics for recovered messages
|
|
if (recoveredCount > 0) {
|
|
metrics.ctwaMessagesRecovered.inc(recoveredCount)
|
|
metrics.ctwaRecoveryRequests.inc({ status: 'success' })
|
|
logger?.debug(
|
|
{ recoveredCount, requestId: response.stanzaId },
|
|
'CTWA: Placeholder resend response processed'
|
|
)
|
|
}
|
|
}
|
|
|
|
break
|
|
case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT:
|
|
ev.emit('messages.update', [
|
|
{
|
|
// flip the sender / fromMe properties because they're in the perspective of the sender
|
|
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.GROUP_MEMBER_LABEL_CHANGE:
|
|
const labelAssociationMsg = protocolMsg.memberLabel
|
|
if (labelAssociationMsg?.label) {
|
|
ev.emit('group.member-tag.update', {
|
|
groupId: chat.id ?? '',
|
|
label: labelAssociationMsg.label,
|
|
participant: message.key.participant ?? '',
|
|
participantAlt: message.key.participantAlt ?? '',
|
|
messageTimestamp: Number(message.messageTimestamp)
|
|
})
|
|
}
|
|
|
|
break
|
|
case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC:
|
|
const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload
|
|
if (!encodedPayload) {
|
|
break
|
|
}
|
|
|
|
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 reactionKey = content.reactionMessage.key
|
|
if (!reactionKey) {
|
|
return
|
|
}
|
|
|
|
const reaction: proto.IReaction = {
|
|
...content.reactionMessage,
|
|
key: message.key
|
|
}
|
|
ev.emit('messages.reaction', [
|
|
{
|
|
reaction,
|
|
key: reactionKey
|
|
}
|
|
])
|
|
} else if (content?.encEventResponseMessage) {
|
|
const encEventResponse = content.encEventResponseMessage
|
|
const creationMsgKey = encEventResponse.eventCreationMessageKey
|
|
if (!creationMsgKey) {
|
|
return
|
|
}
|
|
|
|
// 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
|
|
if (!eventCreatorPn) {
|
|
return
|
|
}
|
|
|
|
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)
|
|
let participants: GroupParticipant[]
|
|
const emitParticipantsUpdate = (action: ParticipantAction) =>
|
|
ev.emit('group-participants.update', {
|
|
id: jid,
|
|
author: message.key.participant!,
|
|
authorPn: message.key.participantAlt!,
|
|
participants,
|
|
action
|
|
})
|
|
const emitGroupUpdate = (update: Partial<GroupMetadata>) => {
|
|
ev.emit('groups.update', [
|
|
{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt }
|
|
])
|
|
}
|
|
|
|
const emitGroupRequestJoin = (participant: LIDMapping, action: RequestJoinAction, method: RequestJoinMethod) => {
|
|
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)) // ADD SUPPORT FOR LID
|
|
|
|
switch (message.messageStubType) {
|
|
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
|
|
participants = message.messageStubParameters.map((a: any) => JSON.parse(a as string)) || []
|
|
emitParticipantsUpdate('modify')
|
|
break
|
|
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
|
|
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
|
|
participants = message.messageStubParameters.map((a: any) => JSON.parse(a as string)) || []
|
|
emitParticipantsUpdate('remove')
|
|
// mark the chat read only if you left the group
|
|
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: any) => JSON.parse(a as string)) || []
|
|
if (participantsIncludesMe()) {
|
|
chat.readOnly = false
|
|
}
|
|
|
|
emitParticipantsUpdate('add')
|
|
break
|
|
case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE:
|
|
participants = message.messageStubParameters.map((a: any) => JSON.parse(a as string)) || []
|
|
emitParticipantsUpdate('demote')
|
|
break
|
|
case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE:
|
|
participants = message.messageStubParameters.map((a: any) => JSON.parse(a as string)) || []
|
|
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: // TODO: Add other events
|
|
const participant = JSON.parse(message.messageStubParameters?.[0]) as LIDMapping
|
|
const action = message.messageStubParameters?.[1] as RequestJoinAction
|
|
const method = message.messageStubParameters?.[2] as RequestJoinMethod
|
|
emitGroupRequestJoin(participant, action, method)
|
|
break
|
|
}
|
|
} /* else if(content?.pollUpdateMessage) {
|
|
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey!
|
|
// we need to fetch the poll creation message to get the poll enc key
|
|
// TODO: make standalone, remove getMessage reference
|
|
// TODO: Remove entirely
|
|
const pollMsg = await getMessage(creationMsgKey)
|
|
if(pollMsg) {
|
|
const meIdNormalised = jidNormalizedUser(meId)
|
|
const pollCreatorJid = getKeyAuthor(creationMsgKey, meIdNormalised)
|
|
const voterJid = getKeyAuthor(message.key, meIdNormalised)
|
|
const pollEncKey = pollMsg.messageContextInfo?.messageSecret!
|
|
|
|
try {
|
|
const voteMsg = decryptPollVote(
|
|
content.pollUpdateMessage.vote!,
|
|
{
|
|
pollEncKey,
|
|
pollCreatorJid,
|
|
pollMsgId: creationMsgKey.id!,
|
|
voterJid,
|
|
}
|
|
)
|
|
ev.emit('messages.update', [
|
|
{
|
|
key: creationMsgKey,
|
|
update: {
|
|
pollUpdates: [
|
|
{
|
|
pollUpdateMessageKey: message.key,
|
|
vote: voteMsg,
|
|
senderTimestampMs: (content.pollUpdateMessage.senderTimestampMs! as Long).toNumber(),
|
|
}
|
|
]
|
|
}
|
|
}
|
|
])
|
|
} catch(err) {
|
|
logger?.warn(
|
|
{ err, creationMsgKey },
|
|
'failed to decrypt poll vote'
|
|
)
|
|
}
|
|
} else {
|
|
logger?.warn(
|
|
{ creationMsgKey },
|
|
'poll creation message not found, cannot decrypt update'
|
|
)
|
|
}
|
|
} */
|
|
|
|
if (Object.keys(chat).length > 1) {
|
|
ev.emit('chats.update', [chat])
|
|
}
|
|
}
|
|
|
|
export default processMessage
|