You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
484 lines
17 KiB
TypeScript
484 lines
17 KiB
TypeScript
import _, { isEmpty, isEqual } from 'lodash';
|
|
import { ConfigDumpData } from '../data/configDump/configDump';
|
|
import { Data, hasSyncedInitialConfigurationItem } from '../data/data';
|
|
import { ConversationInteraction } from '../interactions';
|
|
import { ConversationTypeEnum } from '../models/conversationAttributes';
|
|
import { SignalService } from '../protobuf';
|
|
import {
|
|
joinOpenGroupV2WithUIEvents,
|
|
parseOpenGroupV2,
|
|
} from '../session/apis/open_group_api/opengroupV2/JoinOpenGroupV2';
|
|
import { getOpenGroupV2ConversationId } from '../session/apis/open_group_api/utils/OpenGroupUtils';
|
|
import { getConversationController } from '../session/conversations';
|
|
import { IncomingMessage } from '../session/messages/incoming/IncomingMessage';
|
|
import { ProfileManager } from '../session/profile_manager/ProfileManager';
|
|
import { UserUtils } from '../session/utils';
|
|
import { ConfigurationSync } from '../session/utils/job_runners/jobs/ConfigurationSyncJob';
|
|
import { IncomingConfResult, LibSessionUtil } from '../session/utils/libsession/libsession_utils';
|
|
import { toHex } from '../session/utils/String';
|
|
import { configurationMessageReceived, trigger } from '../shims/events';
|
|
import { BlockedNumberController } from '../util';
|
|
import { getLastProfileUpdateTimestamp, setLastProfileUpdateTimestamp } from '../util/storage';
|
|
import {
|
|
ContactsWrapperActions,
|
|
GenericWrapperActions,
|
|
UserConfigWrapperActions,
|
|
} from '../webworker/workers/browser/libsession_worker_interface';
|
|
import { removeFromCache } from './cache';
|
|
import { handleNewClosedGroup } from './closedGroups';
|
|
import { EnvelopePlus } from './types';
|
|
|
|
async function mergeConfigsWithIncomingUpdates(
|
|
incomingConfig: IncomingMessage<SignalService.ISharedConfigMessage>
|
|
): Promise<{ kind: SignalService.SharedConfigMessage.Kind; result: IncomingConfResult }> {
|
|
const { kind } = incomingConfig.message;
|
|
|
|
const toMerge = [incomingConfig.message.data];
|
|
const wrapperId = LibSessionUtil.kindToVariant(kind);
|
|
try {
|
|
window.log.warn(`${wrapperId} right before merge of ${toMerge.length} arrays`);
|
|
await GenericWrapperActions.merge(wrapperId, toMerge);
|
|
window.log.warn(`${wrapperId} right after merge.`);
|
|
const needsPush = await GenericWrapperActions.needsPush(wrapperId);
|
|
const needsDump = await GenericWrapperActions.needsDump(wrapperId);
|
|
window.log.info(`${wrapperId} needsPush:${needsPush} needsDump:${needsDump} `);
|
|
|
|
const messageHashes = [incomingConfig.messageHash];
|
|
const latestSentTimestamp = incomingConfig.envelopeTimestamp;
|
|
|
|
const incomingConfResult: IncomingConfResult = {
|
|
latestSentTimestamp,
|
|
messageHashes,
|
|
needsDump,
|
|
needsPush,
|
|
};
|
|
|
|
return { kind, result: incomingConfResult };
|
|
} catch (e) {
|
|
window.log.error('mergeConfigsWithIncomingUpdates failed with', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function handleUserProfileUpdate(result: IncomingConfResult): Promise<IncomingConfResult> {
|
|
const updatedUserName = await UserConfigWrapperActions.getName();
|
|
if (!result.needsDump) {
|
|
return result;
|
|
}
|
|
|
|
const updatedProfilePicture = await UserConfigWrapperActions.getProfilePicture();
|
|
const picUpdate = !isEmpty(updatedProfilePicture.key) && !isEmpty(updatedProfilePicture.url);
|
|
await ProfileManager.updateOurProfileSync(
|
|
updatedUserName,
|
|
picUpdate ? updatedProfilePicture.url : null,
|
|
picUpdate ? updatedProfilePicture.key : null
|
|
);
|
|
return result;
|
|
}
|
|
|
|
// tslint:disable-next-line: cyclomatic-complexity
|
|
async function handleContactsUpdate(result: IncomingConfResult): Promise<IncomingConfResult> {
|
|
if (!result.needsDump) {
|
|
return result;
|
|
}
|
|
const us = UserUtils.getOurPubKeyStrFromCache();
|
|
|
|
const allContacts = await ContactsWrapperActions.getAll();
|
|
|
|
for (let index = 0; index < allContacts.length; index++) {
|
|
const wrapperConvo = allContacts[index];
|
|
|
|
if (wrapperConvo.id === us) {
|
|
// our profile update comes from our userProfile, not from the contacts wrapper.
|
|
continue;
|
|
}
|
|
|
|
const existingConvo = await getConversationController().getOrCreateAndWait(
|
|
wrapperConvo.id,
|
|
ConversationTypeEnum.PRIVATE
|
|
);
|
|
if (wrapperConvo.id && existingConvo) {
|
|
let changes = false;
|
|
|
|
// the display name set is handled in `updateProfileOfContact`
|
|
if (wrapperConvo.nickname !== existingConvo.getNickname()) {
|
|
await existingConvo.setNickname(wrapperConvo.nickname || null, false);
|
|
changes = true;
|
|
}
|
|
|
|
if (!wrapperConvo.hidden && !existingConvo.isHidden()) {
|
|
existingConvo.set({ hidden: false });
|
|
changes = true;
|
|
}
|
|
|
|
if (Boolean(wrapperConvo.approved) !== Boolean(existingConvo.isApproved())) {
|
|
await existingConvo.setIsApproved(Boolean(wrapperConvo.approved), false);
|
|
changes = true;
|
|
}
|
|
|
|
if (Boolean(wrapperConvo.approvedMe) !== Boolean(existingConvo.didApproveMe())) {
|
|
await existingConvo.setDidApproveMe(Boolean(wrapperConvo.approvedMe), false);
|
|
changes = true;
|
|
}
|
|
|
|
if (Boolean(wrapperConvo.approvedMe) !== Boolean(existingConvo.didApproveMe())) {
|
|
await existingConvo.setDidApproveMe(Boolean(wrapperConvo.approvedMe), false);
|
|
changes = true;
|
|
}
|
|
|
|
//TODO priority means more than just isPinned but has an order logic in it too
|
|
const shouldBePinned = wrapperConvo.priority > 0;
|
|
if (shouldBePinned !== Boolean(existingConvo.isPinned())) {
|
|
await existingConvo.setIsPinned(shouldBePinned, false);
|
|
changes = true;
|
|
}
|
|
|
|
const convoBlocked = wrapperConvo.blocked || false;
|
|
await BlockedNumberController.setBlocked(wrapperConvo.id, convoBlocked);
|
|
|
|
// make sure to write the changes to the database now as the `AvatarDownloadJob` below might take some time before getting run
|
|
if (changes) {
|
|
await existingConvo.commit();
|
|
}
|
|
|
|
// we still need to handle the `name` (synchronous) and the `profilePicture` (asynchronous)
|
|
await ProfileManager.updateProfileOfContact(
|
|
existingConvo.id,
|
|
wrapperConvo.name,
|
|
wrapperConvo.profilePicture?.url || null,
|
|
wrapperConvo.profilePicture?.key || null
|
|
);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function processMergingResults(
|
|
envelope: EnvelopePlus,
|
|
result: { kind: SignalService.SharedConfigMessage.Kind; result: IncomingConfResult }
|
|
) {
|
|
const pubkey = envelope.source;
|
|
|
|
const { kind, result: incomingResult } = result;
|
|
|
|
if (!incomingResult) {
|
|
await removeFromCache(envelope);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let finalResult = incomingResult;
|
|
|
|
switch (kind) {
|
|
case SignalService.SharedConfigMessage.Kind.USER_PROFILE:
|
|
finalResult = await handleUserProfileUpdate(incomingResult);
|
|
break;
|
|
case SignalService.SharedConfigMessage.Kind.CONTACTS:
|
|
finalResult = await handleContactsUpdate(incomingResult);
|
|
break;
|
|
default:
|
|
throw new Error(`processMergingResults unknown kind of contact : ${kind}`);
|
|
}
|
|
const variant = LibSessionUtil.kindToVariant(kind);
|
|
// We need to get the existing message hashes and combine them with the latest from the
|
|
// service node to ensure the next push will properly clean up old messages
|
|
const oldMessagesHashes = await ConfigDumpData.getCombinedHashesByVariantAndPubkey(
|
|
variant,
|
|
envelope.source
|
|
);
|
|
|
|
const allMessageHashes = [...oldMessagesHashes, ...finalResult.messageHashes];
|
|
|
|
const finalResultsHashes = new Set(finalResult.messageHashes);
|
|
|
|
// lodash does deep compare of Sets
|
|
const messageHashesChanged = !isEqual(oldMessagesHashes, finalResultsHashes);
|
|
|
|
if (finalResult.needsDump) {
|
|
// The config data had changes so regenerate the dump and save it
|
|
|
|
const dump = await GenericWrapperActions.dump(variant);
|
|
await ConfigDumpData.saveConfigDump({
|
|
data: dump,
|
|
publicKey: pubkey,
|
|
variant,
|
|
combinedMessageHashes: allMessageHashes,
|
|
});
|
|
} else if (messageHashesChanged) {
|
|
// The config data didn't change but there were different messages on the service node
|
|
// so just update the message hashes so the next sync can properly remove any old ones
|
|
await ConfigDumpData.saveCombinedMessageHashesForMatching({
|
|
publicKey: pubkey,
|
|
variant,
|
|
combinedMessageHashes: allMessageHashes,
|
|
});
|
|
}
|
|
|
|
await removeFromCache(envelope);
|
|
} catch (e) {
|
|
window.log.error(`processMergingResults failed with ${e.message}`);
|
|
await removeFromCache(envelope);
|
|
return;
|
|
}
|
|
|
|
// Now that the local state has been updated, trigger a config sync (this will push any
|
|
// pending updates and properly update the state)
|
|
if (result.result.needsPush) {
|
|
await ConfigurationSync.queueNewJobIfNeeded();
|
|
}
|
|
}
|
|
|
|
async function handleConfigMessageViaLibSession(
|
|
envelope: EnvelopePlus,
|
|
configMessage: IncomingMessage<SignalService.ISharedConfigMessage>
|
|
) {
|
|
// TODO: Remove this once `useSharedUtilForUserConfig` is permanent
|
|
if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) {
|
|
await removeFromCache(envelope);
|
|
return;
|
|
}
|
|
|
|
if (!configMessage) {
|
|
await removeFromCache(envelope);
|
|
|
|
return;
|
|
}
|
|
|
|
window?.log?.info('Handling our sharedConfig message via libsession_util.');
|
|
|
|
const incomingMergeResult = await mergeConfigsWithIncomingUpdates(configMessage);
|
|
|
|
await processMergingResults(envelope, incomingMergeResult);
|
|
}
|
|
|
|
async function handleOurProfileUpdateLegacy(
|
|
sentAt: number | Long,
|
|
configMessage: SignalService.ConfigurationMessage
|
|
) {
|
|
// this call won't be needed with the new sharedUtilLibrary
|
|
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
|
|
return;
|
|
}
|
|
const latestProfileUpdateTimestamp = getLastProfileUpdateTimestamp();
|
|
if (!latestProfileUpdateTimestamp || sentAt > latestProfileUpdateTimestamp) {
|
|
window?.log?.info(
|
|
`Handling our profileUdpate ourLastUpdate:${latestProfileUpdateTimestamp}, envelope sent at: ${sentAt}`
|
|
);
|
|
const { profileKey, profilePicture, displayName } = configMessage;
|
|
|
|
await ProfileManager.updateOurProfileSync(displayName, profilePicture, 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 handleGroupsAndContactsFromConfigMessageLegacy(
|
|
envelope: EnvelopePlus,
|
|
configMessage: SignalService.ConfigurationMessage
|
|
) {
|
|
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
|
|
return;
|
|
}
|
|
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 handleClosedGroupsFromConfigLegacy(configMessage.closedGroups, envelope);
|
|
}
|
|
|
|
handleOpenGroupsFromConfig(configMessage.openGroups);
|
|
|
|
if (configMessage.contacts?.length) {
|
|
await Promise.all(
|
|
configMessage.contacts.map(async c => handleContactFromConfigLegacy(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<string>) => {
|
|
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
|
|
return;
|
|
}
|
|
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 handleClosedGroupsFromConfigLegacy = async (
|
|
closedGroups: Array<SignalService.ConfigurationMessage.IClosedGroup>,
|
|
envelope: EnvelopePlus
|
|
) => {
|
|
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
|
|
return;
|
|
}
|
|
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 handleContactFromConfigLegacy = async (
|
|
contactReceived: SignalService.ConfigurationMessage.IContact,
|
|
envelope: EnvelopePlus
|
|
) => {
|
|
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
|
|
return;
|
|
}
|
|
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.unblockAll([contactConvo.id]);
|
|
}
|
|
|
|
await ProfileManager.updateProfileOfContact(
|
|
contactConvo.id,
|
|
profileInDataMessage.displayName || undefined,
|
|
profileInDataMessage.profilePicture || null,
|
|
contactReceived.profileKey || null
|
|
);
|
|
} catch (e) {
|
|
window?.log?.warn('failed to handle a new closed group from configuration message');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This is the legacy way of handling incoming configuration message.
|
|
* Should not be used at all soon.
|
|
*/
|
|
async function handleConfigurationMessageLegacy(
|
|
envelope: EnvelopePlus,
|
|
configurationMessage: SignalService.ConfigurationMessage
|
|
): Promise<void> {
|
|
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
|
|
window?.log?.info(
|
|
'useSharedUtilForUserConfig is set, not handling config messages with "handleConfigurationMessageLegacy()"'
|
|
);
|
|
await removeFromCache(envelope);
|
|
return;
|
|
}
|
|
|
|
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 handleOurProfileUpdateLegacy(envelope.timestamp, configurationMessage);
|
|
|
|
await handleGroupsAndContactsFromConfigMessageLegacy(envelope, configurationMessage);
|
|
|
|
await removeFromCache(envelope);
|
|
}
|
|
|
|
export const ConfigMessageHandler = {
|
|
handleConfigurationMessageLegacy,
|
|
handleConfigMessageViaLibSession,
|
|
};
|