diff --git a/protos/SignalService.proto b/protos/SignalService.proto index db1ef1fe7..5dd6dfded 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -52,11 +52,12 @@ message SharedConfigMessage { } message Content { + reserved 7; + reserved "configurationMessage"; optional DataMessage dataMessage = 1; optional CallMessage callMessage = 3; optional ReceiptMessage receiptMessage = 5; optional TypingMessage typingMessage = 6; - optional ConfigurationMessage configurationMessage = 7; optional DataExtractionNotification dataExtractionNotification = 8; optional Unsend unsendMessage = 9; optional MessageRequestResponse messageRequestResponse = 10; @@ -212,36 +213,6 @@ message CallMessage { } -message ConfigurationMessage { - - message ClosedGroup { - optional bytes publicKey = 1; - optional string name = 2; - optional KeyPair encryptionKeyPair = 3; - repeated bytes members = 4; - repeated bytes admins = 5; - } - - message Contact { - // @required - required bytes publicKey = 1; - // @required - required string name = 2; - optional string profilePicture = 3; - optional bytes profileKey = 4; - optional bool isApproved = 5; - optional bool isBlocked = 6; - optional bool didApproveMe = 7; - } - - repeated ClosedGroup closedGroups = 1; - repeated string openGroups = 2; - optional string displayName = 3; - optional string profilePicture = 4; - optional bytes profileKey = 5; - repeated Contact contacts = 6; -} - message ReceiptMessage { enum Type { diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index ad015a663..5e8263ccc 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -9,7 +9,6 @@ import useTimeoutFn from 'react-use/lib/useTimeoutFn'; import { Data } from '../../data/data'; import { ConvoHub } from '../../session/conversations'; import { getMessageQueue } from '../../session/sending'; -import { syncConfigurationIfNeeded } from '../../session/utils/sync/syncUtils'; import { clearSearch } from '../../state/ducks/search'; import { resetOverlayMode, SectionType, showLeftPaneSection } from '../../state/ducks/section'; @@ -42,10 +41,11 @@ import { forceRefreshRandomSnodePool, getFreshSwarmFor, } from '../../session/apis/snode_api/snodePool'; +import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob'; +import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/sync/syncUtils'; import { isDarkTheme } from '../../state/selectors/theme'; import { ThemeStateType } from '../../themes/constants/colors'; import { switchThemeTo } from '../../themes/switchTheme'; -import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob'; const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); @@ -167,7 +167,7 @@ const triggerSyncIfNeeded = async () => { const didWeHandleAConfigurationMessageAlready = (await Data.getItemById(SettingsKey.hasSyncedInitialConfigurationItem))?.value || false; if (didWeHandleAConfigurationMessageAlready) { - await syncConfigurationIfNeeded(); + await forceSyncConfigurationNowIfNeeded(); } }; @@ -267,7 +267,7 @@ export const ActionsPanel = () => { if (!ourPrimaryConversation) { return; } - void syncConfigurationIfNeeded(); + void forceSyncConfigurationNowIfNeeded(); }, DURATION.DAYS * 2); useInterval(() => { diff --git a/ts/components/registration/RegistrationStages.tsx b/ts/components/registration/RegistrationStages.tsx index db676adc0..7a63b26f8 100644 --- a/ts/components/registration/RegistrationStages.tsx +++ b/ts/components/registration/RegistrationStages.tsx @@ -1,12 +1,12 @@ import React, { createContext, useEffect, useState } from 'react'; -import { SignUpMode, SignUpTab } from './SignUpTab'; -import { SignInMode, SignInTab } from './SignInTab'; import { Data } from '../../data/data'; +import { SettingsKey } from '../../data/settings-key'; import { getSwarmPollingInstance } from '../../session/apis/snode_api'; import { ConvoHub } from '../../session/conversations'; import { mnDecode } from '../../session/crypto/mnemonic'; import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils'; import { TaskTimedOutError } from '../../session/utils/Promise'; +import { fromHex } from '../../session/utils/String'; import { trigger } from '../../shims/events'; import { generateMnemonic, @@ -14,9 +14,9 @@ import { sessionGenerateKeyPair, signInByLinkingDevice, } from '../../util/accountManager'; -import { fromHex } from '../../session/utils/String'; -import { setSignInByLinking, setSignWithRecoveryPhrase, Storage } from '../../util/storage'; -import { SettingsKey } from '../../data/settings-key'; +import { Storage, setSignInByLinking, setSignWithRecoveryPhrase } from '../../util/storage'; +import { SignInMode, SignInTab } from './SignInTab'; +import { SignUpMode, SignUpTab } from './SignUpTab'; export async function resetRegistration() { await Data.removeAll(); @@ -71,8 +71,7 @@ export async function signUp(signUpDetails: { /** * Sign in/restore from seed. - * Ask for a display name, as we will drop incoming ConfigurationMessages if any are saved on the swarm. - * We will handle a ConfigurationMessage + * Ask for a display name, as we will drop incoming libsession updates if any are saved on the swarm. */ export async function signInWithRecovery(signInDetails: { displayName: string; @@ -102,7 +101,7 @@ export async function signInWithRecovery(signInDetails: { /** * This is will try to sign in with the user recovery phrase. - * If no ConfigurationMessage is received in 60seconds, the loading will be canceled. + * If no libsession updates is received in 60seconds, the loading will be canceled. */ export async function signInWithLinking(signInDetails: { userRecoveryPhrase: string }) { const { userRecoveryPhrase } = signInDetails; diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index f13dd3cd5..2ec248783 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -22,10 +22,10 @@ import { perfEnd, perfStart } from '../session/utils/Performance'; import { ReleasedFeatures } from '../util/releaseFeature'; import { Storage } from '../util/storage'; // eslint-disable-next-line import/no-unresolved, import/extensions +import { ConfigWrapperUser } from '../webworker/workers/browser/libsession_worker_functions'; import { getSettingsKeyFromLibsessionWrapper } from './configMessage'; import { ECKeyPair, HexKeyPair } from './keypairs'; import { queueAllCachedFromSource } from './receiver'; -import { ConfigWrapperUser } from '../webworker/workers/browser/libsession_worker_functions'; export const distributingClosedGroupEncryptionKeyPairs = new Map(); @@ -101,7 +101,7 @@ export async function handleClosedGroupControlMessage( return; } - // We drop New closed group message from our other devices, as they will come as ConfigurationMessage instead + // We drop New closed group message from our other devices, as they will come through libsession instead if (type === Type.ENCRYPTION_KEY_PAIR) { const isComingFromGroupPubkey = envelope.type === SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 39b2239bb..1cfa8b27a 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -483,11 +483,7 @@ export async function innerHandleSwarmContentMessage( perfEnd(`handleTypingMessage-${envelope.id}`, 'handleTypingMessage'); return; } - if (content.configurationMessage) { - // we drop support for legacy configuration message - await removeFromCache(envelope); - return; - } + if (content.sharedConfigMessage) { window.log.warn('content.sharedConfigMessage are handled outside of the receiving pipeline'); // this should never happen, but remove it from cache just in case something is messed up @@ -509,16 +505,23 @@ export async function innerHandleSwarmContentMessage( } if (content.unsendMessage) { await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend); + return; } if (content.callMessage) { await handleCallMessage(envelope, content.callMessage as SignalService.CallMessage); + return; } if (content.messageRequestResponse) { await handleMessageRequestResponse( envelope, content.messageRequestResponse as SignalService.MessageRequestResponse ); + return; } + + // If we get here, we don't know how to handle that envelope. probably a very old type of message, or something we don't support. + // There is not much we can do expect drop it + await removeFromCache(envelope); } catch (e) { window?.log?.warn(e.message); } diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 956f9ddeb..789d8de8b 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -12,13 +12,13 @@ import { ConversationTypeEnum, READ_MESSAGE_STATE } from '../models/conversation import { MessageDirection } from '../models/messageType'; import { SignalService } from '../protobuf'; import { ProfileManager } from '../session/profile_manager/ProfileManager'; +import { PubKey } from '../session/types'; +import { PropsForMessageWithoutConvoProps, lookupQuote } from '../state/ducks/conversations'; import { showMessageRequestBannerOutsideRedux } from '../state/ducks/userConfig'; import { getHideMessageRequestBannerOutsideRedux } from '../state/selectors/userConfig'; import { GoogleChrome } from '../util'; import { LinkPreviews } from '../util/linkPreviews'; import { ReleasedFeatures } from '../util/releaseFeature'; -import { PropsForMessageWithoutConvoProps, lookupQuote } from '../state/ducks/conversations'; -import { PubKey } from '../session/types'; function contentTypeSupported(type: string): boolean { const Chrome = GoogleChrome; @@ -436,7 +436,7 @@ export async function handleMessageJob( void queueAttachmentDownloads(messageModel, conversation); // Check if we need to update any profile names // the only profile we don't update with what is coming here is ours, - // as our profile is shared across our devices with a ConfigurationMessage + // as our profile is shared across our devices with libsession if (messageModel.isIncoming() && regularDataMessage.profile) { await ProfileManager.updateProfileOfContact( sendingDeviceConversation.id, diff --git a/ts/session/messages/incoming/IncomingMessage.ts b/ts/session/messages/incoming/IncomingMessage.ts index 4e08f1d55..995e2b27b 100644 --- a/ts/session/messages/incoming/IncomingMessage.ts +++ b/ts/session/messages/incoming/IncomingMessage.ts @@ -6,7 +6,6 @@ type IncomingMessageAvailableTypes = | SignalService.CallMessage | SignalService.ReceiptMessage | SignalService.TypingMessage - | SignalService.ConfigurationMessage | SignalService.DataExtractionNotification | SignalService.Unsend | SignalService.MessageRequestResponse diff --git a/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts b/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts deleted file mode 100644 index 00ec330c3..000000000 --- a/ts/session/messages/outgoing/controlMessage/ConfigurationMessage.ts +++ /dev/null @@ -1,217 +0,0 @@ -// this is not a very good name, but a configuration message is a message sent to our other devices so sync our current public and closed groups - -import { SignalService } from '../../../../protobuf'; -import { MessageParams } from '../Message'; -import { ECKeyPair } from '../../../../receiver/keypairs'; -import { fromHexToArray } from '../../../utils/String'; -import { PubKey } from '../../../types'; -import { ContentMessage } from '..'; - -interface ConfigurationMessageParams extends MessageParams { - activeClosedGroups: Array; - activeOpenGroups: Array; - displayName: string; - profilePicture?: string; - profileKey?: Uint8Array; - contacts: Array; -} - -export class ConfigurationMessage extends ContentMessage { - public readonly activeClosedGroups: Array; - public readonly activeOpenGroups: Array; - public readonly displayName: string; - public readonly profilePicture?: string; - public readonly profileKey?: Uint8Array; - public readonly contacts: Array; - - constructor(params: ConfigurationMessageParams) { - super({ timestamp: params.timestamp, identifier: params.identifier }); - this.activeClosedGroups = params.activeClosedGroups; - this.activeOpenGroups = params.activeOpenGroups; - this.displayName = params.displayName; - this.profilePicture = params.profilePicture; - this.profileKey = params.profileKey; - this.contacts = params.contacts; - - if (!this.activeClosedGroups) { - throw new Error('closed group must be set'); - } - - if (!this.activeOpenGroups) { - throw new Error('open group must be set'); - } - - if (!this.displayName || !this.displayName?.length) { - throw new Error('displayName must be set'); - } - - if (this.profilePicture && typeof this.profilePicture !== 'string') { - throw new Error('profilePicture set but not an Uin8Array'); - } - - if (this.profileKey && !(this.profileKey instanceof Uint8Array)) { - throw new Error('profileKey set but not an Uin8Array'); - } - - if (!this.contacts) { - throw new Error('contacts must be set'); - } - } - - public contentProto(): SignalService.Content { - return new SignalService.Content({ - configurationMessage: this.configurationProto(), - }); - } - - protected configurationProto(): SignalService.ConfigurationMessage { - return new SignalService.ConfigurationMessage({ - closedGroups: this.mapClosedGroupsObjectToProto(this.activeClosedGroups), - openGroups: this.activeOpenGroups, - displayName: this.displayName, - profilePicture: this.profilePicture, - profileKey: this.profileKey, - contacts: this.mapContactsObjectToProto(this.contacts), - }); - } - - private mapClosedGroupsObjectToProto( - closedGroups: Array - ): Array { - return (closedGroups || []).map(m => m.toProto()); - } - - private mapContactsObjectToProto( - contacts: Array - ): Array { - return (contacts || []).map(m => m.toProto()); - } -} - -export class ConfigurationMessageContact { - public publicKey: string; - public displayName: string; - public profilePictureURL?: string; - public profileKey?: Uint8Array; - public isApproved?: boolean; - public isBlocked?: boolean; - public didApproveMe?: boolean; - - public constructor({ - publicKey, - displayName, - profilePictureURL, - profileKey, - isApproved, - isBlocked, - didApproveMe, - }: { - publicKey: string; - displayName: string; - profilePictureURL?: string; - profileKey?: Uint8Array; - isApproved?: boolean; - isBlocked?: boolean; - didApproveMe?: boolean; - }) { - this.publicKey = publicKey; - this.displayName = displayName; - this.profilePictureURL = profilePictureURL; - this.profileKey = profileKey; - this.isApproved = isApproved; - this.isBlocked = isBlocked; - this.didApproveMe = didApproveMe; - - // will throw if public key is invalid - PubKey.cast(publicKey); - - if (this.displayName?.length === 0) { - throw new Error('displayName must be set or undefined'); - } - - if (this.profilePictureURL !== undefined && this.profilePictureURL?.length === 0) { - throw new Error('profilePictureURL must either undefined or not empty'); - } - if (this.profileKey !== undefined && this.profileKey?.length === 0) { - throw new Error('profileKey must either undefined or not empty'); - } - } - - public toProto(): SignalService.ConfigurationMessage.Contact { - return new SignalService.ConfigurationMessage.Contact({ - publicKey: fromHexToArray(this.publicKey), - name: this.displayName, - profilePicture: this.profilePictureURL, - profileKey: this.profileKey, - isApproved: this.isApproved, - isBlocked: this.isBlocked, - didApproveMe: this.didApproveMe, - }); - } -} - -export class ConfigurationMessageClosedGroup { - public publicKey: string; - public name: string; - public encryptionKeyPair: ECKeyPair; - public members: Array; - public admins: Array; - - public constructor({ - publicKey, - name, - encryptionKeyPair, - members, - admins, - }: { - publicKey: string; - name: string; - encryptionKeyPair: ECKeyPair; - members: Array; - admins: Array; - }) { - this.publicKey = publicKey; - this.name = name; - this.encryptionKeyPair = encryptionKeyPair; - this.members = members; - this.admins = admins; - - // will throw if publik key is invalid - PubKey.cast(publicKey); - - if ( - !encryptionKeyPair?.privateKeyData?.byteLength || - !encryptionKeyPair?.publicKeyData?.byteLength - ) { - throw new Error('Encryption key pair looks invalid'); - } - - if (!this.name?.length) { - throw new Error('name must be set'); - } - - if (!this.members?.length) { - throw new Error('members must be set'); - } - if (!this.admins?.length) { - throw new Error('admins must be set'); - } - - if (this.admins.some(a => !this.members.includes(a))) { - throw new Error('some admins are not members'); - } - } - - public toProto(): SignalService.ConfigurationMessage.ClosedGroup { - return new SignalService.ConfigurationMessage.ClosedGroup({ - publicKey: fromHexToArray(this.publicKey), - name: this.name, - encryptionKeyPair: { - publicKey: this.encryptionKeyPair.publicKeyData, - privateKey: this.encryptionKeyPair.privateKeyData, - }, - members: this.members.map(fromHexToArray), - admins: this.admins.map(fromHexToArray), - }); - } -} diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index cce765925..78452b6a9 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -1,7 +1,6 @@ import { AbortController } from 'abort-controller'; import { MessageSender } from '.'; -import { ConfigurationMessage } from '../messages/outgoing/controlMessage/ConfigurationMessage'; import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage'; import { ClosedGroupNameChangeMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage'; import { PubKey, RawMessage } from '../types'; @@ -62,7 +61,7 @@ export class MessageQueue { sentCb?: (message: RawMessage) => Promise, isGroup = false ): Promise { - if (message instanceof ConfigurationMessage || !!(message as any).syncTarget) { + if ((message as any).syncTarget) { throw new Error('SyncMessage needs to be sent with sendSyncMessage'); } await this.process(destinationPubKey, message, namespace, sentCb, isGroup); @@ -230,11 +229,7 @@ export class MessageQueue { if (!message) { return; } - if ( - !(message instanceof ConfigurationMessage) && - !(message instanceof UnsendMessage) && - !(message as any)?.syncTarget - ) { + if (!(message instanceof UnsendMessage) && !(message as any)?.syncTarget) { throw new Error('Invalid message given to sendSyncMessage'); } @@ -347,7 +342,7 @@ export class MessageQueue { const us = UserUtils.getOurPubKeyFromCache(); let isSyncMessage = false; if (us && destinationPk.isEqual(us)) { - // We allow a message for ourselves only if it's a ConfigurationMessage, a ClosedGroupNewMessage, + // We allow a message for ourselves only if it's a ClosedGroupNewMessage, // or a message with a syncTarget set. if (MessageSender.isSyncMessage(message)) { diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index c6dc63e84..28e02eba9 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -29,7 +29,6 @@ import { ConvoHub } from '../conversations'; import { MessageEncrypter } from '../crypto'; import { addMessagePadding } from '../crypto/BufferPadding'; import { ContentMessage } from '../messages/outgoing'; -import { ConfigurationMessage } from '../messages/outgoing/controlMessage/ConfigurationMessage'; import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage'; import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage'; import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; @@ -79,7 +78,6 @@ function getMinRetryTimeout() { function isSyncMessage(message: ContentMessage) { if ( - message instanceof ConfigurationMessage || message instanceof ClosedGroupNewMessage || message instanceof UnsendMessage || (message as any).syncTarget?.length > 0 diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index df0d3239d..4a2def2dd 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -1,12 +1,12 @@ import _ from 'lodash'; import { UserUtils } from '.'; import { Data } from '../../data/data'; +import { SessionKeyPair } from '../../receiver/keypairs'; +import { LokiProfile } from '../../types/Message'; +import { getOurPubKeyStrFromStorage } from '../../util/storage'; +import { ConvoHub } from '../conversations'; import { PubKey } from '../types'; import { fromHexToArray, toHex } from './String'; -import { ConvoHub } from '../conversations'; -import { LokiProfile } from '../../types/Message'; -import { getOurPubKeyStrFromStorage, Storage } from '../../util/storage'; -import { SessionKeyPair } from '../../receiver/keypairs'; export type HexKeyPair = { pubKey: string; @@ -100,9 +100,7 @@ export const getUserED25519KeyPairBytes = async (): Promise => - (await Data.getItemById(ITEM_ID_LAST_SYNC_TIMESTAMP))?.value; - -const writeLastSyncTimestampToDb = async (timestamp: number) => - Storage.put(ITEM_ID_LAST_SYNC_TIMESTAMP, timestamp); - -/** - * Conditionally Syncs user configuration with other devices linked. - */ -export const syncConfigurationIfNeeded = async () => { - await UserSync.queueNewJobIfNeeded(); - - const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); - if (!userConfigLibsession) { - const lastSyncedTimestamp = (await getLastSyncTimestampFromDb()) || 0; - const now = Date.now(); - - // if the last sync was less than 2 days before, return early. - if (Math.abs(now - lastSyncedTimestamp) < DURATION.DAYS * 2) { - return; - } - - const allConvos = ConvoHub.use().getConversations(); - - const configMessage = await getCurrentConfigurationMessage(allConvos); - try { - // window?.log?.info('syncConfigurationIfNeeded with', configMessage); - - await getMessageQueue().sendSyncMessage({ - namespace: SnodeNamespaces.Default, - message: configMessage, - }); - } catch (e) { - window?.log?.warn('Caught an error while sending our ConfigurationMessage:', e); - // we do return early so that next time we use the old timestamp again - // and so try again to trigger a sync - return; - } - await writeLastSyncTimestampToDb(now); - } -}; - export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = false) => { await ReleasedFeatures.checkIsUserConfigFeatureReleased(); return new Promise(resolve => { @@ -91,197 +29,14 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal e.message ); }); - if (ReleasedFeatures.isUserConfigFeatureReleasedCached()) { - if (waitForMessageSent) { - window.Whisper.events.once(UserSyncJobDone, () => { - resolve(true); - }); - return; - } - resolve(true); - return; - } - const allConvos = ConvoHub.use().getConversations(); - // eslint-disable-next-line more/no-then - void getCurrentConfigurationMessage(allConvos) - .then(configMessage => { - // this just adds the message to the sending queue. - // if waitForMessageSent is set, we need to effectively wait until then - - const callback = waitForMessageSent - ? () => { - resolve(true); - } - : undefined; - void getMessageQueue().sendSyncMessage({ - namespace: SnodeNamespaces.Default, - message: configMessage, - sentCb: callback as any, - }); - // either we resolve from the callback if we need to wait for it, - // or we don't want to wait, we resolve it here. - if (!waitForMessageSent) { - resolve(true); - } - }) - .catch(e => { - window?.log?.warn('Caught an error while building our ConfigurationMessage:', e); - resolve(false); + if (waitForMessageSent) { + window.Whisper.events.once(UserSyncJobDone, () => { + resolve(true); }); - }); -}; - -const getActiveOpenGroupV2CompleteUrls = async ( - convos: Array -): Promise> => { - // Filter open groups v2 - const openGroupsV2ConvoIds = convos - .filter(c => !!c.getActiveAt() && c.isOpenGroupV2() && !c.isLeft()) - .map(c => c.id) as Array; - - const urls = await Promise.all( - openGroupsV2ConvoIds.map(async opengroupConvoId => { - const roomInfos = OpenGroupData.getV2OpenGroupRoom(opengroupConvoId); - - if (roomInfos) { - return getCompleteUrlFromRoom(roomInfos); - } - return null; - }) - ); - - return _.compact(urls) || []; -}; - -const getValidClosedGroups = async (convos: Array) => { - const ourPubKey = UserUtils.getOurPubKeyStrFromCache(); - - // Filter Closed/Medium groups - const closedGroupModels = convos.filter( - c => - !!c.getActiveAt() && - c.isClosedGroup() && - c.getGroupMembers()?.includes(ourPubKey) && - !c.isLeft() && - !c.isKickedFromGroup() && - !c.isBlocked() && - c.getRealSessionUsername() - ); - - const closedGroups = await Promise.all( - closedGroupModels.map(async c => { - const groupPubKey = c.get('id'); - const fetchEncryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(groupPubKey); - if (!fetchEncryptionKeyPair) { - return null; - } - - return new ConfigurationMessageClosedGroup({ - publicKey: groupPubKey, - name: c.getRealSessionUsername() || '', - members: c.getGroupMembers() || [], - admins: c.getGroupAdmins(), - encryptionKeyPair: ECKeyPair.fromHexKeyPair(fetchEncryptionKeyPair), - }); - }) - ); - - const onlyValidClosedGroup = closedGroups.filter( - m => m !== null - ) as Array; - return onlyValidClosedGroup; -}; - -const getValidContacts = (convos: Array) => { - // Filter contacts - // blindedId are synced with the outbox logic. - const contactsModels = convos.filter( - c => - !!c.getActiveAt() && - c.getRealSessionUsername() && - c.isPrivate() && - c.isApproved() && - !PubKey.isBlinded(c.get('id')) - ); - - const contacts = contactsModels.map(c => { - try { - const profileKey = c.getProfileKey(); - let profileKeyForContact = null; - if (typeof profileKey === 'string') { - // this will throw if the profileKey is not in hex. - try { - // for some reason, at some point, the saved profileKey is a string in base64 format - // this hack is here to update existing conversations with a non-hex profileKey to a hex format and save them - - if (!/^[0-9a-fA-F]+$/.test(profileKey)) { - throw new Error('Not Hex'); - } - profileKeyForContact = fromHexToArray(profileKey); - } catch (e) { - // if not hex, try to decode it as base64 - profileKeyForContact = fromBase64ToArray(profileKey); - // if the line above does not fail, update the stored profileKey for this convo - void c.setProfileKey(profileKeyForContact); - } - } else if (profileKey) { - window.log.warn( - 'Got a profileKey for a contact in another format than string. Contact: ', - c.id - ); - return null; - } - - return new ConfigurationMessageContact({ - publicKey: c.id as string, - displayName: c.getRealSessionUsername() || 'Anonymous', - profilePictureURL: c.getAvatarPointer(), - profileKey: !profileKeyForContact?.length ? undefined : profileKeyForContact, - isApproved: c.isApproved(), - isBlocked: c.isBlocked(), - didApproveMe: c.didApproveMe(), - }); - } catch (e) { - window?.log.warn('getValidContacts', e); - return null; + return; } - }); - return _.compact(contacts); -}; - -export const getCurrentConfigurationMessage = async ( - convos: Array -): Promise => { - const ourPubKey = UserUtils.getOurPubKeyStrFromCache(); - const ourConvo = convos.find(convo => convo.id === ourPubKey); - - const opengroupV2CompleteUrls = await getActiveOpenGroupV2CompleteUrls(convos); - const onlyValidClosedGroup = await getValidClosedGroups(convos); - const validContacts = getValidContacts(convos); - - if (!ourConvo) { - window?.log?.error('Could not find our convo while building a configuration message.'); - } - - const ourProfileKeyHex = - ConvoHub.use().get(UserUtils.getOurPubKeyStrFromCache())?.getProfileKey() || null; - const profileKey = ourProfileKeyHex ? fromHexToArray(ourProfileKeyHex) : undefined; - - const profilePicture = ourConvo?.getAvatarPointer() || undefined; - const displayName = ourConvo?.getRealSessionUsername() || 'Anonymous'; // this should never be undefined, but well... - - const activeOpenGroups = [...opengroupV2CompleteUrls]; - - return new ConfigurationMessage({ - identifier: uuidv4(), - timestamp: Date.now(), - activeOpenGroups, - activeClosedGroups: onlyValidClosedGroup, - displayName, - profilePicture, - profileKey, - contacts: validContacts, + resolve(true); }); }; @@ -348,7 +103,6 @@ const buildSyncExpireTimerMessage = ( export type SyncMessageType = | VisibleMessage | ExpirationTimerUpdateMessage - | ConfigurationMessage | MessageRequestResponse | UnsendMessage; @@ -368,7 +122,7 @@ export const buildSyncMessage = ( if (!sentTimestamp || !_.isNumber(sentTimestamp)) { throw new Error('Tried to build a sync message without a sentTimestamp'); } - // don't include our profileKey on syncing message. This is to be done by a ConfigurationMessage now + // don't include our profileKey on syncing message. This is to be done through libsession now const timestamp = _.toNumber(sentTimestamp); if (dataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) { return buildSyncExpireTimerMessage(identifier, dataMessage, timestamp, syncTarget); diff --git a/ts/test/session/unit/messages/ConfigurationMessage_test.ts b/ts/test/session/unit/messages/ConfigurationMessage_test.ts deleted file mode 100644 index ca541a776..000000000 --- a/ts/test/session/unit/messages/ConfigurationMessage_test.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { expect } from 'chai'; -import { ECKeyPair } from '../../../../receiver/keypairs'; -import { TTL_DEFAULT } from '../../../../session/constants'; - -import { - ConfigurationMessage, - ConfigurationMessageClosedGroup, - ConfigurationMessageContact, -} from '../../../../session/messages/outgoing/controlMessage/ConfigurationMessage'; -import { TestUtils } from '../../../test-utils'; - -describe('ConfigurationMessage', () => { - it('throw if closed group is not set', () => { - const activeClosedGroups = null as any; - const params = { - activeClosedGroups, - activeOpenGroups: [], - timestamp: Date.now(), - displayName: 'displayName', - contacts: [], - }; - expect(() => new ConfigurationMessage(params)).to.throw('closed group must be set'); - }); - - it('throw if open group is not set', () => { - const activeOpenGroups = null as any; - const params = { - activeClosedGroups: [], - activeOpenGroups, - timestamp: Date.now(), - displayName: 'displayName', - contacts: [], - }; - expect(() => new ConfigurationMessage(params)).to.throw('open group must be set'); - }); - - it('throw if display name is not set', () => { - const params = { - activeClosedGroups: [], - activeOpenGroups: [], - timestamp: Date.now(), - displayName: undefined as any, - contacts: [], - }; - expect(() => new ConfigurationMessage(params)).to.throw('displayName must be set'); - }); - - it('throw if display name is set but empty', () => { - const params = { - activeClosedGroups: [], - activeOpenGroups: [], - timestamp: Date.now(), - displayName: undefined as any, - contacts: [], - }; - expect(() => new ConfigurationMessage(params)).to.throw('displayName must be set'); - }); - - it('ttl is 4 days', () => { - const params = { - activeClosedGroups: [], - activeOpenGroups: [], - timestamp: Date.now(), - displayName: 'displayName', - contacts: [], - }; - const configMessage = new ConfigurationMessage(params); - expect(configMessage.ttl()).to.be.equal(TTL_DEFAULT.TTL_MAX); - }); - - describe('ConfigurationMessageClosedGroup', () => { - it('throw if closed group has no encryptionkeypair', () => { - const member = TestUtils.generateFakePubKey().key; - const params = { - publicKey: TestUtils.generateFakePubKey().key, - name: 'groupname', - members: [member], - admins: [member], - encryptionKeyPair: undefined as any, - }; - - expect(() => new ConfigurationMessageClosedGroup(params)).to.throw( - 'Encryption key pair looks invalid' - ); - }); - - it('throw if closed group has invalid encryptionkeypair', () => { - const member = TestUtils.generateFakePubKey().key; - const params = { - publicKey: TestUtils.generateFakePubKey().key, - name: 'groupname', - members: [member], - admins: [member], - encryptionKeyPair: new ECKeyPair(new Uint8Array(), new Uint8Array()), - }; - - expect(() => new ConfigurationMessageClosedGroup(params)).to.throw( - 'Encryption key pair looks invalid' - ); - }); - - it('throw if closed group has invalid pubkey', () => { - const member = TestUtils.generateFakePubKey().key; - const params = { - publicKey: 'invalidpubkey', - name: 'groupname', - members: [member], - admins: [member], - encryptionKeyPair: TestUtils.generateFakeECKeyPair(), - }; - - expect(() => new ConfigurationMessageClosedGroup(params)).to.throw(); - }); - - it('throw if closed group has invalid name', () => { - const member = TestUtils.generateFakePubKey().key; - const params = { - publicKey: TestUtils.generateFakePubKey().key, - name: '', - members: [member], - admins: [member], - encryptionKeyPair: TestUtils.generateFakeECKeyPair(), - }; - - expect(() => new ConfigurationMessageClosedGroup(params)).to.throw('name must be set'); - }); - - it('throw if members is empty', () => { - const member = TestUtils.generateFakePubKey().key; - const params = { - publicKey: TestUtils.generateFakePubKey().key, - name: 'groupname', - members: [], - admins: [member], - encryptionKeyPair: TestUtils.generateFakeECKeyPair(), - }; - - expect(() => new ConfigurationMessageClosedGroup(params)).to.throw('members must be set'); - }); - - it('throw if admins is empty', () => { - const member = TestUtils.generateFakePubKey().key; - const params = { - publicKey: TestUtils.generateFakePubKey().key, - name: 'groupname', - members: [member], - admins: [], - encryptionKeyPair: TestUtils.generateFakeECKeyPair(), - }; - - expect(() => new ConfigurationMessageClosedGroup(params)).to.throw('admins must be set'); - }); - - it('throw if some admins are not members', () => { - const member = TestUtils.generateFakePubKey().key; - const admin = TestUtils.generateFakePubKey().key; - const params = { - publicKey: TestUtils.generateFakePubKey().key, - name: 'groupname', - members: [member], - admins: [admin], - encryptionKeyPair: TestUtils.generateFakeECKeyPair(), - }; - - expect(() => new ConfigurationMessageClosedGroup(params)).to.throw( - 'some admins are not members' - ); - }); - }); - - describe('ConfigurationMessageContact', () => { - it('throws if contacts is not set', () => { - const params = { - activeClosedGroups: [], - activeOpenGroups: [], - timestamp: Date.now(), - displayName: 'displayName', - contacts: undefined as any, - }; - expect(() => new ConfigurationMessage(params)).to.throw('contacts must be set'); - }); - it('throw if some admins are not members', () => { - const member = TestUtils.generateFakePubKey().key; - const admin = TestUtils.generateFakePubKey().key; - const params = { - publicKey: TestUtils.generateFakePubKey().key, - name: 'groupname', - members: [member], - admins: [admin], - encryptionKeyPair: TestUtils.generateFakeECKeyPair(), - }; - - expect(() => new ConfigurationMessageClosedGroup(params)).to.throw( - 'some admins are not members' - ); - }); - - it('throw if the contact has not a valid pubkey', () => { - const params = { - publicKey: '05', - displayName: 'contactDisplayName', - }; - - expect(() => new ConfigurationMessageContact(params)).to.throw(); - - const params2 = { - publicKey: undefined as any, - displayName: 'contactDisplayName', - }; - - expect(() => new ConfigurationMessageContact(params2)).to.throw(); - }); - - it('throw if the contact has an empty display name', () => { - // a display name cannot be empty nor undefined - - expect(() => new ConfigurationMessageContact(params2)).to.throw(); - - const params2 = { - publicKey: TestUtils.generateFakePubKey().key, - displayName: '', - }; - - expect(() => new ConfigurationMessageContact(params2)).to.throw(); - }); - - it('throw if the contact has a profilePictureURL set but empty', () => { - const params = { - publicKey: TestUtils.generateFakePubKey().key, - displayName: 'contactDisplayName', - profilePictureURL: '', - }; - - expect(() => new ConfigurationMessageContact(params)).to.throw( - 'profilePictureURL must either undefined or not empty' - ); - }); - - it('throw if the contact has a profileKey set but empty', () => { - const params = { - publicKey: TestUtils.generateFakePubKey().key, - displayName: 'contactDisplayName', - profileKey: new Uint8Array(), - }; - - expect(() => new ConfigurationMessageContact(params)).to.throw( - 'profileKey must either undefined or not empty' - ); - }); - }); -}); diff --git a/ts/test/session/unit/utils/Messages_test.ts b/ts/test/session/unit/utils/Messages_test.ts index 82feda2be..babd9e70c 100644 --- a/ts/test/session/unit/utils/Messages_test.ts +++ b/ts/test/session/unit/utils/Messages_test.ts @@ -1,29 +1,21 @@ /* eslint-disable no-unused-expressions */ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { beforeEach } from 'mocha'; import Sinon from 'sinon'; -import { ConfigurationMessage } from '../../../../session/messages/outgoing/controlMessage/ConfigurationMessage'; import { ClosedGroupVisibleMessage } from '../../../../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; import { PubKey } from '../../../../session/types'; -import { MessageUtils, UserUtils } from '../../../../session/utils'; +import { MessageUtils } from '../../../../session/utils'; import { TestUtils } from '../../../test-utils'; -import { OpenGroupData, OpenGroupV2Room } from '../../../../data/opengroups'; -import { ConversationTypeEnum } from '../../../../models/conversationAttributes'; import { SignalService } from '../../../../protobuf'; -import { getOpenGroupV2ConversationId } from '../../../../session/apis/open_group_api/utils/OpenGroupUtils'; import { SnodeNamespaces } from '../../../../session/apis/snode_api/namespaces'; -import { ConvoHub } from '../../../../session/conversations'; import { ClosedGroupAddedMembersMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupAddedMembersMessage'; import { ClosedGroupEncryptionPairMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairMessage'; import { ClosedGroupEncryptionPairReplyMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage'; import { ClosedGroupNameChangeMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage'; import { ClosedGroupNewMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupNewMessage'; import { ClosedGroupRemovedMembersMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage'; -import { getCurrentConfigurationMessage } from '../../../../session/utils/sync/syncUtils'; -import { stubData, stubOpenGroupData } from '../../../test-utils/utils'; chai.use(chaiAsPromised as any); @@ -205,80 +197,5 @@ describe('Message Utils', () => { const rawMessage = await MessageUtils.toRawMessage(device, msg, SnodeNamespaces.Default); expect(rawMessage.encryption).to.equal(SignalService.Envelope.Type.SESSION_MESSAGE); }); - - it('passing a ConfigurationMessage returns Fallback', async () => { - const device = TestUtils.generateFakePubKey(); - - const msg = new ConfigurationMessage({ - timestamp: Date.now(), - activeOpenGroups: [], - activeClosedGroups: [], - displayName: 'displayName', - contacts: [], - }); - const rawMessage = await MessageUtils.toRawMessage(device, msg, SnodeNamespaces.Default); - expect(rawMessage.encryption).to.equal(SignalService.Envelope.Type.SESSION_MESSAGE); - }); - }); - - describe('getCurrentConfigurationMessage', () => { - const ourNumber = TestUtils.generateFakePubKey().key; - - beforeEach(async () => { - Sinon.stub(UserUtils, 'getOurPubKeyStrFromCache').resolves(ourNumber); - Sinon.stub(UserUtils, 'getOurPubKeyFromCache').resolves(PubKey.cast(ourNumber)); - stubData('getAllConversations').resolves([]); - stubData('saveConversation').resolves(); - stubOpenGroupData('getAllV2OpenGroupRooms').resolves(); - TestUtils.stubData('getItemById').callsFake(async () => { - return { value: '[]' }; - }); - ConvoHub.use().reset(); - - await ConvoHub.use().load(); - }); - - afterEach(() => { - Sinon.restore(); - }); - - // open groups are actually removed when we leave them so this doesn't make much sense, but just in case we break something later - it('filter out non active open groups', async () => { - await ConvoHub.use().getOrCreateAndWait('05123456789', ConversationTypeEnum.PRIVATE); - await ConvoHub.use().getOrCreateAndWait('0512345678', ConversationTypeEnum.PRIVATE); - - const convoId3 = getOpenGroupV2ConversationId('http://chat-dev2.lokinet.org', 'fish'); - const convoId4 = getOpenGroupV2ConversationId('http://chat-dev3.lokinet.org', 'fish2'); - const convoId5 = getOpenGroupV2ConversationId('http://chat-dev3.lokinet.org', 'fish3'); - - const convo3 = await ConvoHub.use().getOrCreateAndWait(convoId3, ConversationTypeEnum.GROUP); - convo3.set({ active_at: Date.now() }); - - stubOpenGroupData('getV2OpenGroupRoom') - .returns(null) - .withArgs(convoId3) - .returns({ - serverUrl: 'http://chat-dev2.lokinet.org', - roomId: 'fish', - serverPublicKey: 'serverPublicKey', - } as OpenGroupV2Room); - - const convo4 = await ConvoHub.use().getOrCreateAndWait(convoId4, ConversationTypeEnum.GROUP); - convo4.set({ active_at: undefined }); - - await OpenGroupData.opengroupRoomsLoad(); - const convo5 = await ConvoHub.use().getOrCreateAndWait(convoId5, ConversationTypeEnum.GROUP); - convo5.set({ active_at: 0 }); - - await ConvoHub.use().getOrCreateAndWait('051234567', ConversationTypeEnum.PRIVATE); - const convos = ConvoHub.use().getConversations(); - - // convoID3 is active but 4 and 5 are not - const configMessage = await getCurrentConfigurationMessage(convos); - expect(configMessage.activeOpenGroups.length).to.equal(1); - expect(configMessage.activeOpenGroups[0]).to.equal( - 'http://chat-dev2.lokinet.org/fish?public_key=serverPublicKey' - ); - }); }); }); diff --git a/ts/util/missingCaseError.ts b/ts/util/missingCaseError.ts index 4673f5c5f..8f5ff77d4 100644 --- a/ts/util/missingCaseError.ts +++ b/ts/util/missingCaseError.ts @@ -1,20 +1,5 @@ // `missingCaseError` is useful for compile-time checking that all `case`s in // a `switch` statement have been handled, e.g. // -// type AttachmentType = 'media' | 'documents'; -// -// const type: AttachmentType = selectedTab; -// switch (type) { -// case 'media': -// return ; -// case 'documents': -// return ; -// default: -// return missingCaseError(type); -// } -// -// If we extended `AttachmentType` to `'media' | 'documents' | 'links'` the code -// above would trigger a compiler error stating that `'links'` has not been -// handled by our `switch` / `case` statement which is useful for code -// maintenance and system evolution. + export const missingCaseError = (x: never): TypeError => new TypeError(`Unhandled case: ${x}`);