import _ from 'lodash'; import { Data, hasSyncedInitialConfigurationItem } from '../data/data'; import { joinOpenGroupV2WithUIEvents, parseOpenGroupV2, } from '../session/apis/open_group_api/opengroupV2/JoinOpenGroupV2'; import { getOpenGroupV2ConversationId } from '../session/apis/open_group_api/utils/OpenGroupUtils'; import { SignalService } from '../protobuf'; import { getConversationController } from '../session/conversations'; import { UserUtils } from '../session/utils'; import { toHex } from '../session/utils/String'; import { configurationMessageReceived, trigger } from '../shims/events'; import { BlockedNumberController } from '../util'; import { removeFromCache } from './cache'; import { handleNewClosedGroup } from './closedGroups'; import { EnvelopePlus } from './types'; import { ConversationInteraction } from '../interactions'; import { getLastProfileUpdateTimestamp, setLastProfileUpdateTimestamp } from '../util/storage'; import { appendFetchAvatarAndProfileJob, updateOurProfileSync } from './userProfileImageUpdates'; import { ConversationTypeEnum } from '../models/conversationAttributes'; async function handleOurProfileUpdate( sentAt: number | Long, configMessage: SignalService.ConfigurationMessage ) { const latestProfileUpdateTimestamp = getLastProfileUpdateTimestamp(); if (!latestProfileUpdateTimestamp || sentAt > latestProfileUpdateTimestamp) { window?.log?.info( `Handling our profileUdpate ourLastUpdate:${latestProfileUpdateTimestamp}, envelope sent at: ${sentAt}` ); const { profileKey, profilePicture, displayName } = configMessage; const lokiProfile = { displayName, profilePicture, }; await updateOurProfileSync(lokiProfile, profileKey); await setLastProfileUpdateTimestamp(_.toNumber(sentAt)); // do not trigger a signin by linking if the display name is empty if (displayName) { trigger(configurationMessageReceived, displayName); } else { window?.log?.warn('Got a configuration message but the display name is empty'); } } } async function handleGroupsAndContactsFromConfigMessage( envelope: EnvelopePlus, configMessage: SignalService.ConfigurationMessage ) { const envelopeTimestamp = _.toNumber(envelope.timestamp); const lastConfigUpdate = await Data.getItemById(hasSyncedInitialConfigurationItem); const lastConfigTimestamp = lastConfigUpdate?.timestamp; const isNewerConfig = !lastConfigTimestamp || (lastConfigTimestamp && lastConfigTimestamp < envelopeTimestamp); if (!isNewerConfig) { window?.log?.info('Received outdated configuration message... Dropping message.'); return; } await Data.createOrUpdateItem({ id: 'hasSyncedInitialConfigurationItem', value: true, timestamp: envelopeTimestamp, }); // we only want to apply changes to closed groups if we never got them // new opengroups get added when we get a new closed group message from someone, or a sync'ed message from outself creating the group if (!lastConfigTimestamp) { await handleClosedGroupsFromConfig(configMessage.closedGroups, envelope); } handleOpenGroupsFromConfig(configMessage.openGroups); if (configMessage.contacts?.length) { await Promise.all(configMessage.contacts.map(async c => handleContactFromConfig(c, envelope))); } } /** * Trigger a join for all open groups we are not already in. * @param openGroups string array of open group urls */ const handleOpenGroupsFromConfig = (openGroups: Array) => { const numberOpenGroup = openGroups?.length || 0; for (let i = 0; i < numberOpenGroup; i++) { const currentOpenGroupUrl = openGroups[i]; const parsedRoom = parseOpenGroupV2(currentOpenGroupUrl); if (!parsedRoom) { continue; } const roomConvoId = getOpenGroupV2ConversationId(parsedRoom.serverUrl, parsedRoom.roomId); if (!getConversationController().get(roomConvoId)) { window?.log?.info( `triggering join of public chat '${currentOpenGroupUrl}' from ConfigurationMessage` ); void joinOpenGroupV2WithUIEvents(currentOpenGroupUrl, false, true); } } }; /** * Trigger a join for all closed groups which doesn't exist yet * @param openGroups string array of open group urls */ const handleClosedGroupsFromConfig = async ( closedGroups: Array, envelope: EnvelopePlus ) => { const numberClosedGroup = closedGroups?.length || 0; window?.log?.info( `Received ${numberClosedGroup} closed group on configuration. Creating them... ` ); await Promise.all( closedGroups.map(async c => { const groupUpdate = new SignalService.DataMessage.ClosedGroupControlMessage({ type: SignalService.DataMessage.ClosedGroupControlMessage.Type.NEW, encryptionKeyPair: c.encryptionKeyPair, name: c.name, admins: c.admins, members: c.members, publicKey: c.publicKey, }); try { // TODO we should not drop the envelope from cache as long as we are still handling a new closed group from that same envelope // check the removeFromCache inside handleNewClosedGroup() await handleNewClosedGroup(envelope, groupUpdate); } catch (e) { window?.log?.warn('failed to handle a new closed group from configuration message'); } }) ); }; /** * Handles adding of a contact and setting approval/block status * @param contactReceived Contact to sync */ const handleContactFromConfig = async ( contactReceived: SignalService.ConfigurationMessage.IContact, envelope: EnvelopePlus ) => { try { if (!contactReceived.publicKey?.length) { return; } const contactConvo = await getConversationController().getOrCreateAndWait( toHex(contactReceived.publicKey), ConversationTypeEnum.PRIVATE ); const profileInDataMessage: SignalService.DataMessage.ILokiProfile = { displayName: contactReceived.name, profilePicture: contactReceived.profilePicture, }; const existingActiveAt = contactConvo.get('active_at'); if (!existingActiveAt || existingActiveAt === 0) { contactConvo.set('active_at', _.toNumber(envelope.timestamp)); } // checking for existence of field on protobuf if (contactReceived.isApproved === true) { if (!contactConvo.isApproved()) { await contactConvo.setIsApproved(Boolean(contactReceived.isApproved)); await contactConvo.addOutgoingApprovalMessage(_.toNumber(envelope.timestamp)); } if (contactReceived.didApproveMe === true) { // checking for existence of field on message await contactConvo.setDidApproveMe(Boolean(contactReceived.didApproveMe)); } } // only set for explicit true/false values incase outdated sender doesn't have the fields if (contactReceived.isBlocked === true) { if (contactConvo.isIncomingRequest()) { // handling case where restored device's declined message requests were getting restored await ConversationInteraction.deleteAllMessagesByConvoIdNoConfirmation(contactConvo.id); } await BlockedNumberController.block(contactConvo.id); } else if (contactReceived.isBlocked === false) { await BlockedNumberController.unblock(contactConvo.id); } void appendFetchAvatarAndProfileJob( contactConvo, profileInDataMessage, contactReceived.profileKey ); } catch (e) { window?.log?.warn('failed to handle a new closed group from configuration message'); } }; export async function handleConfigurationMessage( envelope: EnvelopePlus, configurationMessage: SignalService.ConfigurationMessage ): Promise { window?.log?.info('Handling configuration message'); const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); if (!ourPubkey) { return; } if (envelope.source !== ourPubkey) { window?.log?.info('Dropping configuration change from someone else than us.'); return removeFromCache(envelope); } await handleOurProfileUpdate(envelope.timestamp, configurationMessage); await handleGroupsAndContactsFromConfigMessage(envelope, configurationMessage); await removeFromCache(envelope); }