import { EnvelopePlus } from './types'; import { SignalService } from '../protobuf'; import { removeFromCache } from './cache'; import { getEnvelopeId } from './common'; import _ from 'lodash'; import ByteBuffer from 'bytebuffer'; import { handleMessageEvent, isMessageEmpty, processDecrypted, updateProfile, } from './dataMessage'; import { handleContacts } from './multidevice'; import { MultiDeviceProtocol } from '../session/protocols'; import { BlockedNumberController } from '../util'; import { ConversationController } from '../session/conversations'; export async function handleSyncMessage( envelope: EnvelopePlus, syncMessage: SignalService.ISyncMessage ): Promise { const { textsecure } = window; // We should only accept sync messages from our devices const ourNumber = textsecure.storage.user.getNumber(); const ourDevices = await MultiDeviceProtocol.getAllDevices(ourNumber); const validSyncSender = ourDevices.some( device => device.key === envelope.source ); if (!validSyncSender) { throw new Error( "Received sync message from a device we aren't paired with" ); } // remove empty fields (generated by ts even if they should be null) if (syncMessage.openGroups && !syncMessage.openGroups.length) { syncMessage.openGroups = null; } if (syncMessage.read && !syncMessage.read.length) { syncMessage.read = null; } if (syncMessage.sent) { const sentMessage = syncMessage.sent; const message = sentMessage.message as SignalService.IDataMessage; const to = message.group ? `group(${message.group.id})` : sentMessage.destination; window.log.info( 'sent message to', to, _.toNumber(sentMessage.timestamp), 'from', getEnvelopeId(envelope) ); await handleSentMessage(envelope, sentMessage); } else if (syncMessage.contacts) { return handleContacts(envelope, syncMessage.contacts); } else if (syncMessage.groups) { await handleGroupsSync(envelope, syncMessage.groups); } else if (syncMessage.openGroups) { await handleOpenGroups(envelope, syncMessage.openGroups); } else if (syncMessage.blocked) { await handleBlocked(envelope, syncMessage.blocked); } else if (syncMessage.request) { window.log.info('Got SyncMessage Request'); await removeFromCache(envelope); } else if (syncMessage.read && syncMessage.read.length) { window.log.info('read messages from', getEnvelopeId(envelope)); await handleRead(envelope, syncMessage.read); } else if (syncMessage.verified) { window.log.info('Got syncMessage.verified. Dropping it...'); await removeFromCache(envelope); return; } else if (syncMessage.configuration) { window.log.info('Got syncMessage.configuration. Dropping it...'); await removeFromCache(envelope); return; } throw new Error('Got empty SyncMessage'); } // handle a SYNC message for a message // sent by another device async function handleSentMessage( envelope: EnvelopePlus, sentMessage: SignalService.SyncMessage.ISent ) { const { destination, timestamp, expirationStartTimestamp, unidentifiedStatus, message: msg, } = sentMessage; if (!msg) { window.log('Inner message is missing in a sync message'); await removeFromCache(envelope); return; } if (isMessageEmpty(msg as SignalService.DataMessage)) { window.log.info('dropping empty message synced'); await removeFromCache(envelope); return; } if (msg.mediumGroupUpdate) { throw new Error('Got a medium group update. This should not happen'); } const message = await processDecrypted(envelope, msg); const primaryDevicePubKey = window.storage.get('primaryDevicePubKey'); // handle profileKey and avatar updates if (envelope.source === primaryDevicePubKey) { const { profileKey, profile } = message; const primaryConversation = ConversationController.getInstance().get( primaryDevicePubKey ); if (profile) { await updateProfile(primaryConversation, profile, profileKey); } } const ev: any = new Event('sent'); ev.confirm = removeFromCache.bind(null, envelope); ev.data = { destination, timestamp: _.toNumber(timestamp), device: envelope.sourceDevice, unidentifiedStatus, message, }; if (expirationStartTimestamp) { ev.data.expirationStartTimestamp = _.toNumber(expirationStartTimestamp); } await handleMessageEvent(ev); } // This doesn't have to be async... function handleAttachment(attachment: any) { return { ...attachment, data: ByteBuffer.wrap(attachment.data).toArrayBuffer(), // ByteBuffer to ArrayBuffer }; } async function handleOpenGroups( envelope: EnvelopePlus, openGroups: Array ) { const groupsArray = openGroups.map(openGroup => openGroup.url); openGroups.forEach(({ url, channelId }) => { window.attemptConnection(url, channelId); }); await removeFromCache(envelope); } async function handleBlocked( envelope: EnvelopePlus, blocked: SignalService.SyncMessage.IBlocked ) { window.log.info('Setting these numbers as blocked:', blocked.numbers); // blocked.numbers contains numbers if (blocked.numbers) { const currentlyBlockedNumbers = BlockedNumberController.getBlockedNumbers(); const toRemoveFromBlocked = _.difference( currentlyBlockedNumbers, blocked.numbers ); const toAddToBlocked = _.difference( blocked.numbers, currentlyBlockedNumbers ); async function markConvoBlocked(block: boolean, n: string) { const conv = ConversationController.getInstance().get(n); if (conv) { if (conv.isPrivate()) { await BlockedNumberController.setBlocked(n, block); } else { window.log.warn('Ignoring block/unblock for group:', n); } await conv.commit(); } else { window.log.warn('Did not find corresponding conversation to block', n); } } await Promise.all(toAddToBlocked.map(async n => markConvoBlocked(true, n))); await Promise.all( toRemoveFromBlocked.map(async n => markConvoBlocked(false, n)) ); } await removeFromCache(envelope); } async function onReadSync(readAt: any, sender: any, timestamp: any) { window.log.info('read sync', sender, timestamp); const receipt = window.Whisper.ReadSyncs.add({ sender, timestamp, read_at: readAt, }); await window.Whisper.ReadSyncs.onReceipt(receipt); } async function handleRead( envelope: EnvelopePlus, readArray: Array ) { const results = []; for (const read of readArray) { const promise = onReadSync( _.toNumber(envelope.timestamp), read.sender, _.toNumber(read.timestamp) ); results.push(promise); } await Promise.all(results); await removeFromCache(envelope); } async function handleConfiguration( envelope: EnvelopePlus, configuration: SignalService.SyncMessage.IConfiguration ) { window.log.info('got configuration sync message'); const { storage } = window; const { readReceipts, typingIndicators, unidentifiedDeliveryIndicators, linkPreviews, } = configuration; storage.put('read-receipt-setting', readReceipts); if ( unidentifiedDeliveryIndicators === true || unidentifiedDeliveryIndicators === false ) { storage.put( 'unidentifiedDeliveryIndicators', unidentifiedDeliveryIndicators ); } if (typingIndicators === true || typingIndicators === false) { storage.put('typing-indicators-setting', typingIndicators); } if (linkPreviews === true || linkPreviews === false) { storage.put('link-preview-setting', linkPreviews); } await removeFromCache(envelope); } async function handleGroupsSync( envelope: EnvelopePlus, groups: SignalService.SyncMessage.IGroups ) { window.log.warn('FIXME group sync is not currently doing anything'); // const attachmentPointer = handleAttachment(groups); // const groupBuffer = new window.GroupBuffer(attachmentPointer.data); // let groupDetails = groupBuffer.next(); // const promises = []; // while (groupDetails !== undefined) { // groupDetails.id = groupDetails.id.toBinary(); // const promise = updateOrCreateGroupFromSync(groupDetails).catch( // (e: any) => { // window.log.error('error processing group', e); // } // ); // promises.push(promise); // groupDetails = groupBuffer.next(); // } // Note: we do not return here because we don't want to block the next message on // this attachment download and a lot of processing of that attachment. // void Promise.all(promises); await removeFromCache(envelope); }