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.
454 lines
16 KiB
TypeScript
454 lines
16 KiB
TypeScript
import _, { groupBy, isArray, isEmpty } 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';
|
|
import { callLibSessionWorker } from '../webworker/workers/browser/libsession_worker_interface';
|
|
import { IncomingMessage } from '../session/messages/incoming/IncomingMessage';
|
|
import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions';
|
|
import { Dictionary } from '@reduxjs/toolkit';
|
|
import { ContactInfo, ProfilePicture } from 'session_util_wrapper';
|
|
|
|
type IncomingConfResult = {
|
|
needsPush: boolean;
|
|
needsDump: boolean;
|
|
messageHashes: Array<string>;
|
|
latestSentTimestamp: number;
|
|
};
|
|
|
|
function protobufSharedConfigTypeToWrapper(
|
|
kind: SignalService.SharedConfigMessage.Kind
|
|
): ConfigWrapperObjectTypes | null {
|
|
switch (kind) {
|
|
case SignalService.SharedConfigMessage.Kind.USER_PROFILE:
|
|
return 'UserConfig';
|
|
case SignalService.SharedConfigMessage.Kind.CONTACTS:
|
|
return 'ContactsConfig';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function mergeConfigsWithIncomingUpdates(
|
|
groupedByKind: Dictionary<Array<IncomingMessage<SignalService.SharedConfigMessage>>>
|
|
) {
|
|
const kindMessageMap: Map<SignalService.SharedConfigMessage.Kind, IncomingConfResult> = new Map();
|
|
// do the merging on all wrappers sequentially instead of with a promise.all()
|
|
const allKinds = (Object.keys(groupedByKind) as unknown) as Array<
|
|
SignalService.SharedConfigMessage.Kind
|
|
>;
|
|
for (let index = 0; index < allKinds.length; index++) {
|
|
const kind = allKinds[index];
|
|
// see comment above "groupedByKind = groupBy" about why this is needed
|
|
const castedKind = (kind as unknown) as SignalService.SharedConfigMessage.Kind;
|
|
const currentKindMessages = groupedByKind[castedKind];
|
|
if (!currentKindMessages) {
|
|
continue;
|
|
}
|
|
const toMerge = currentKindMessages.map(m => m.message.data);
|
|
|
|
const wrapperId = protobufSharedConfigTypeToWrapper(castedKind);
|
|
if (!wrapperId) {
|
|
throw new Error(`Invalid castedKind: ${castedKind}`);
|
|
}
|
|
|
|
await callLibSessionWorker([wrapperId, 'merge', toMerge]);
|
|
const needsPush = ((await callLibSessionWorker([wrapperId, 'needsPush'])) || false) as boolean;
|
|
const needsDump = ((await callLibSessionWorker([wrapperId, 'needsDump'])) || false) as boolean;
|
|
const messageHashes = currentKindMessages.map(m => m.messageHash);
|
|
const latestSentTimestamp = Math.max(...currentKindMessages.map(m => m.envelopeTimestamp));
|
|
|
|
const incomingConfResult: IncomingConfResult = {
|
|
latestSentTimestamp,
|
|
messageHashes,
|
|
needsDump,
|
|
needsPush,
|
|
};
|
|
kindMessageMap.set(kind, incomingConfResult);
|
|
}
|
|
|
|
return kindMessageMap;
|
|
}
|
|
|
|
async function handleUserProfileUpdate(result: IncomingConfResult) {
|
|
if (result.needsDump) {
|
|
return;
|
|
}
|
|
|
|
const updatedUserName = (await callLibSessionWorker(['UserConfig', 'getName'])) as
|
|
| string
|
|
| undefined;
|
|
const updatedProfilePicture = (await callLibSessionWorker([
|
|
'UserConfig',
|
|
'getProfilePicture',
|
|
])) as ProfilePicture;
|
|
|
|
// fetch our own conversation
|
|
const userPublicKey = UserUtils.getOurPubKeyStrFromCache();
|
|
if (!userPublicKey) {
|
|
return;
|
|
}
|
|
|
|
const picUpdate = !isEmpty(updatedProfilePicture.key) && !isEmpty(updatedProfilePicture.url);
|
|
|
|
// trigger an update of our profileName and picture if there is one.
|
|
// this call checks for differences between updating anything
|
|
void updateOurProfileSync(
|
|
{ displayName: updatedUserName, profilePicture: picUpdate ? updatedProfilePicture.url : null },
|
|
picUpdate ? updatedProfilePicture.key : null
|
|
);
|
|
}
|
|
|
|
async function handleContactsUpdate(result: IncomingConfResult) {
|
|
if (result.needsDump) {
|
|
return;
|
|
}
|
|
|
|
const allContacts = (await callLibSessionWorker(['ContactsConfig', 'getAll'])) as Array<
|
|
ContactInfo
|
|
>;
|
|
|
|
for (let index = 0; index < allContacts.length; index++) {
|
|
const wrapperConvo = allContacts[index];
|
|
|
|
if (wrapperConvo.id && getConversationController().get(wrapperConvo.id)) {
|
|
const existingConvo = getConversationController().get(wrapperConvo.id);
|
|
let changes = false;
|
|
|
|
// Note: the isApproved and didApproveMe flags are irreversible so they should only be updated when getting set to true
|
|
if (
|
|
existingConvo.get('isApproved') !== undefined &&
|
|
wrapperConvo.approved !== undefined &&
|
|
existingConvo.get('isApproved') !== wrapperConvo.approved
|
|
) {
|
|
await existingConvo.setIsApproved(wrapperConvo.approved, false);
|
|
changes = true;
|
|
}
|
|
|
|
if (
|
|
existingConvo.get('didApproveMe') !== undefined &&
|
|
wrapperConvo.approvedMe !== undefined &&
|
|
existingConvo.get('didApproveMe') !== wrapperConvo.approvedMe
|
|
) {
|
|
await existingConvo.setDidApproveMe(wrapperConvo.approvedMe, false);
|
|
changes = true;
|
|
}
|
|
|
|
const convoBlocked = wrapperConvo.blocked || false;
|
|
if (convoBlocked !== existingConvo.isBlocked()) {
|
|
if (existingConvo.isPrivate()) {
|
|
await BlockedNumberController.setBlocked(wrapperConvo.id, convoBlocked);
|
|
} else {
|
|
await BlockedNumberController.setGroupBlocked(wrapperConvo.id, convoBlocked);
|
|
}
|
|
}
|
|
|
|
if (wrapperConvo.nickname !== existingConvo.getNickname()) {
|
|
await existingConvo.setNickname(wrapperConvo.nickname || null, false);
|
|
changes = true;
|
|
}
|
|
// make sure to write the changes to the database now as the `appendFetchAvatarAndProfileJob` call below might take some time before getting run
|
|
if (changes) {
|
|
await existingConvo.commit();
|
|
}
|
|
|
|
// we still need to handle the the `name` and the `profilePicture` but those are currently made asynchronously
|
|
void appendFetchAvatarAndProfileJob(
|
|
existingConvo.id,
|
|
{
|
|
displayName: wrapperConvo.name,
|
|
profilePicture: wrapperConvo.profilePicture?.url || null,
|
|
},
|
|
wrapperConvo.profilePicture?.key || null
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function processMergingResults(
|
|
results: Map<SignalService.SharedConfigMessage.Kind, IncomingConfResult>
|
|
) {
|
|
const keys = [...results.keys()];
|
|
|
|
for (let index = 0; index < keys.length; index++) {
|
|
const kind = keys[index];
|
|
const result = results.get(kind);
|
|
|
|
if (!result) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
switch (kind) {
|
|
case SignalService.SharedConfigMessage.Kind.USER_PROFILE:
|
|
await handleUserProfileUpdate(result);
|
|
break;
|
|
case SignalService.SharedConfigMessage.Kind.CONTACTS:
|
|
await handleContactsUpdate(result);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleConfigMessagesViaLibSession(
|
|
configMessages: Array<IncomingMessage<SignalService.SharedConfigMessage>>
|
|
) {
|
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
|
|
|
if (
|
|
!window.sessionFeatureFlags.useSharedUtilForUserConfig ||
|
|
!configMessages ||
|
|
!isArray(configMessages) ||
|
|
configMessages.length === 0
|
|
) {
|
|
return;
|
|
}
|
|
|
|
window?.log?.info(
|
|
`Handling our profileUdpates via libsession_util. count: ${configMessages.length}`
|
|
);
|
|
|
|
// lodash does not have a way to give the type of the keys as generic parameter so this can only be a string: Array<>
|
|
const groupedByKind = groupBy(configMessages, m => m.message.kind);
|
|
|
|
const kindMessagesMap = await mergeConfigsWithIncomingUpdates(groupedByKind);
|
|
|
|
await processMergingResults(kindMessagesMap);
|
|
}
|
|
|
|
async function handleOurProfileUpdate(
|
|
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;
|
|
|
|
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<string>) => {
|
|
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<SignalService.ConfigurationMessage.IClosedGroup>,
|
|
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.id,
|
|
profileInDataMessage,
|
|
contactReceived.profileKey
|
|
);
|
|
} 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 handleConfigurationMessage(
|
|
envelope: EnvelopePlus,
|
|
configurationMessage: SignalService.ConfigurationMessage
|
|
): Promise<void> {
|
|
if (window.sessionFeatureFlags.useSharedUtilForUserConfig) {
|
|
window?.log?.info(
|
|
'useSharedUtilForUserConfig is set, not handling config messages with "handleConfigurationMessage()"'
|
|
);
|
|
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 handleOurProfileUpdate(envelope.timestamp, configurationMessage);
|
|
|
|
await handleGroupsAndContactsFromConfigMessage(envelope, configurationMessage);
|
|
|
|
await removeFromCache(envelope);
|
|
}
|
|
|
|
export const ConfigMessageHandler = {
|
|
handleConfigurationMessage,
|
|
handleConfigMessagesViaLibSession,
|
|
};
|