From 44483b7d23d4372fff7dcaf76ac2799432929b6b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 2 May 2023 12:06:08 +1000 Subject: [PATCH] fix: use releaseFeature from disappearing message PR as featureFlag --- preload.js | 1 - ts/components/dialog/EditProfileDialog.tsx | 11 +- ts/interactions/conversationInteractions.ts | 4 +- ts/receiver/configMessage.ts | 31 ++++-- ts/receiver/queuedJob.ts | 5 +- ts/session/apis/snode_api/swarmPolling.ts | 16 ++- .../job_runners/jobs/ConfigurationSyncJob.ts | 8 +- ts/session/utils/sync/syncUtils.ts | 15 ++- .../unit/utils/job_runner/JobRunner_test.ts | 4 +- ts/util/releaseFeature.ts | 104 ++++++++++++++++++ ts/util/storage.ts | 3 +- ts/window.d.ts | 1 - 12 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 ts/util/releaseFeature.ts diff --git a/preload.js b/preload.js index e25a9d1f1..fe8c08818 100644 --- a/preload.js +++ b/preload.js @@ -33,7 +33,6 @@ window.sessionFeatureFlags = { ), useDebugLogging: !_.isEmpty(process.env.SESSION_DEBUG), useClosedGroupV3: false || process.env.USE_CLOSED_GROUP_V3, - useSharedUtilForUserConfig: true, debug: { debugFileServerRequests: false, debugNonSnodeRequests: false, diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx index 91534716a..34d4947a1 100644 --- a/ts/components/dialog/EditProfileDialog.tsx +++ b/ts/components/dialog/EditProfileDialog.tsx @@ -15,7 +15,6 @@ import { ConversationTypeEnum } from '../../models/conversationAttributes'; import { MAX_USERNAME_BYTES } from '../../session/constants'; import { getConversationController } from '../../session/conversations'; import { sanitizeSessionUsername } from '../../session/utils/String'; -import { ConfigurationSync } from '../../session/utils/job_runners/jobs/ConfigurationSyncJob'; import { editProfileModal } from '../../state/ducks/modalDialog'; import { pickFileForAvatar } from '../../types/attachments/VisualAttachment'; import { saveQRCode } from '../../util/saveQRCode'; @@ -357,12 +356,6 @@ async function commitProfileEdits(newName: string, scaledAvatarUrl: string | nul // might be good to not trigger a sync if the name did not change await conversation.commit(); - await ConfigurationSync.queueNewJobIfNeeded(); - - if (window.sessionFeatureFlags.useSharedUtilForUserConfig) { - await setLastProfileUpdateTimestamp(Date.now()); - } else { - await setLastProfileUpdateTimestamp(Date.now()); - await SyncUtils.forceSyncConfigurationNowIfNeeded(true); - } + await setLastProfileUpdateTimestamp(Date.now()); + await SyncUtils.forceSyncConfigurationNowIfNeeded(true); } diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index db97cd6ed..f6f3a2586 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -43,6 +43,7 @@ import { SessionUtilUserGroups } from '../session/utils/libsession/libsession_ut import { leaveClosedGroup } from '../session/group/closed-group'; import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts'; import { SettingsKey } from '../data/settings-key'; +import { ReleasedFeatures } from '../util/releaseFeature'; export function copyPublicKeyByConvoId(convoId: string) { if (OpenGroupUtils.isOpenGroupV2(convoId)) { @@ -461,8 +462,9 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) { if (newAvatarDecrypted) { await setLastProfileUpdateTimestamp(Date.now()); await ConfigurationSync.queueNewJobIfNeeded(); + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); - if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) { + if (!userConfigLibsession) { await SyncUtils.forceSyncConfigurationNowIfNeeded(true); } } else { diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index e5dd33b94..9ac3451d8 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -46,6 +46,7 @@ import { HexKeyPair } from './keypairs'; import { queueAllCachedFromSource } from './receiver'; import { EnvelopePlus } from './types'; import { SettingsKey } from '../data/settings-key'; +import { ReleasedFeatures } from '../util/releaseFeature'; function groupByVariant( incomingConfigs: Array> @@ -639,7 +640,9 @@ async function processMergingResults(results: Map> ) { - if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) { + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + + if (!userConfigLibsession) { return; } @@ -678,8 +681,9 @@ async function handleOurProfileUpdateLegacy( sentAt: number | Long, configMessage: SignalService.ConfigurationMessage ) { + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); // we want to allow if we are not registered, as we might need to fetch an old config message (can be removed once we released for a weeks the libsession util) - if (window.sessionFeatureFlags.useSharedUtilForUserConfig && Registration.isDone()) { + if (userConfigLibsession && Registration.isDone()) { return; } const latestProfileUpdateTimestamp = getLastProfileUpdateTimestamp(); @@ -703,7 +707,9 @@ async function handleGroupsAndContactsFromConfigMessageLegacy( envelope: EnvelopePlus, configMessage: SignalService.ConfigurationMessage ) { - if (window.sessionFeatureFlags.useSharedUtilForUserConfig && Registration.isDone()) { + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + + if (userConfigLibsession && Registration.isDone()) { return; } const envelopeTimestamp = toNumber(envelope.timestamp); @@ -736,7 +742,7 @@ async function handleGroupsAndContactsFromConfigMessageLegacy( await handleClosedGroupsFromConfigLegacy(configMessage.closedGroups, envelope); } - handleOpenGroupsFromConfigLegacy(configMessage.openGroups); + void handleOpenGroupsFromConfigLegacy(configMessage.openGroups); if (configMessage.contacts?.length) { await Promise.all( @@ -749,8 +755,10 @@ async function handleGroupsAndContactsFromConfigMessageLegacy( * Trigger a join for all open groups we are not already in. * @param openGroups string array of open group urls */ -const handleOpenGroupsFromConfigLegacy = (openGroups: Array) => { - if (window.sessionFeatureFlags.useSharedUtilForUserConfig && Registration.isDone()) { +const handleOpenGroupsFromConfigLegacy = async (openGroups: Array) => { + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + + if (userConfigLibsession && Registration.isDone()) { return; } const numberOpenGroup = openGroups?.length || 0; @@ -778,7 +786,9 @@ const handleClosedGroupsFromConfigLegacy = async ( closedGroups: Array, envelope: EnvelopePlus ) => { - if (window.sessionFeatureFlags.useSharedUtilForUserConfig && Registration.isDone()) { + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + + if (userConfigLibsession && Registration.isDone()) { return; } const numberClosedGroup = closedGroups?.length || 0; @@ -813,7 +823,9 @@ const handleContactFromConfigLegacy = async ( contactReceived: SignalService.ConfigurationMessage.IContact, envelope: EnvelopePlus ) => { - if (window.sessionFeatureFlags.useSharedUtilForUserConfig && Registration.isDone()) { + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + + if (userConfigLibsession && Registration.isDone()) { return; } try { @@ -880,8 +892,9 @@ async function handleConfigurationMessageLegacy( // when the useSharedUtilForUserConfig flag is ON, we want only allow a legacy config message if we are registering a new user. // this is to allow users linking a device to find their config message if they do not have a shared config message yet. // the process of those messages is always done after the process of the shared config messages, so that's only a fallback. + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); - if (window.sessionFeatureFlags.useSharedUtilForUserConfig && Registration.isDone()) { + if (userConfigLibsession && Registration.isDone()) { window?.log?.info( 'useSharedUtilForUserConfig is set, not handling config messages with "handleConfigurationMessageLegacy()"' ); diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index d6063056d..40c9e5bf3 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -15,6 +15,7 @@ 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'; function contentTypeSupported(type: string): boolean { const Chrome = GoogleChrome; @@ -254,7 +255,9 @@ async function handleRegularMessage( await conversation.setDidApproveMe(true); } } else if (type === 'outgoing') { - if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) { + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + + if (!userConfigLibsession) { // we want to do this for all types of conversations, not just private chats handleSyncedReceiptsNoCommit(message, conversation); diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index e65fe87d6..afc9865e4 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -21,6 +21,7 @@ import { perfEnd, perfStart } from '../../utils/Performance'; import { SnodeNamespace, SnodeNamespaces } from './namespaces'; import { SnodeAPIRetrieve } from './retrieveRequest'; import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types'; +import { ReleasedFeatures } from '../../../util/releaseFeature'; export function extractWebSocketContent( message: string, @@ -151,9 +152,10 @@ export class SwarmPolling { } // we always poll as often as possible for our pubkey const ourPubkey = UserUtils.getOurPubKeyFromCache(); - const directPromise = Promise.all([ - this.pollOnceForKey(ourPubkey, false, this.getUserNamespacesPolled()), - ]).then(() => undefined); + const userNamespaces = await this.getUserNamespacesPolled(); + const directPromise = Promise.all([this.pollOnceForKey(ourPubkey, false, userNamespaces)]).then( + () => undefined + ); const now = Date.now(); const groupPromises = this.groupPolling.map(async group => { @@ -222,10 +224,11 @@ export class SwarmPolling { } let allNamespacesWithoutUserConfigIfNeeded: Array = []; + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); // check if we just fetched the details from the config namespaces. // If yes, merge them together and exclude them from the rest of the messages. - if (window.sessionFeatureFlags.useSharedUtilForUserConfig && resultsFromAllNamespaces) { + if (userConfigLibsession && resultsFromAllNamespaces) { const userConfigMessages = resultsFromAllNamespaces .filter(m => SnodeNamespace.isUserConfigNamespace(m.namespace)) .map(r => r.messages.messages); @@ -484,8 +487,9 @@ export class SwarmPolling { return newMessages; } - private getUserNamespacesPolled() { - return window.sessionFeatureFlags.useSharedUtilForUserConfig + private async getUserNamespacesPolled() { + const isUserConfigRelease = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + return isUserConfigRelease ? [ SnodeNamespaces.UserMessages, SnodeNamespaces.UserProfile, diff --git a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts index 04379a610..752508483 100644 --- a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts @@ -16,6 +16,8 @@ import { PersistedJob, RunJobResult, } from '../PersistedJob'; +import { ReleasedFeatures } from '../../../../util/releaseFeature'; +import { allowOnlyOneAtATime } from '../../Promise'; const defaultMsBetweenRetries = 3000; const defaultMaxAttempts = 3; @@ -182,9 +184,10 @@ class ConfigurationSyncJob extends PersistedJob // save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc await saveDumpsNeededToDB(thisJobDestination); + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); // if the feature flag is not enabled, we want to keep updating the dumps, but just not sync them. - if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) { + if (!userConfigLibsession) { this.triggerConfSyncJobDone(); return RunJobResult.Success; } @@ -305,5 +308,6 @@ async function queueNewJobIfNeeded() { export const ConfigurationSync = { ConfigurationSyncJob, - queueNewJobIfNeeded, + queueNewJobIfNeeded: () => + allowOnlyOneAtATime(`ConfigurationSyncJob-oneAtAtTime`, queueNewJobIfNeeded), }; diff --git a/ts/session/utils/sync/syncUtils.ts b/ts/session/utils/sync/syncUtils.ts index fe4f91b26..cf0d7f3d9 100644 --- a/ts/session/utils/sync/syncUtils.ts +++ b/ts/session/utils/sync/syncUtils.ts @@ -31,6 +31,7 @@ import { ConfigurationSync } from '../job_runners/jobs/ConfigurationSyncJob'; import { fromBase64ToArray, fromHexToArray } from '../String'; import { getCompleteUrlFromRoom } from '../../apis/open_group_api/utils/OpenGroupUtils'; import { Storage } from '../../../util/storage'; +import { ReleasedFeatures } from '../../../util/releaseFeature'; const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp'; @@ -46,7 +47,8 @@ const writeLastSyncTimestampToDb = async (timestamp: number) => export const syncConfigurationIfNeeded = async () => { await ConfigurationSync.queueNewJobIfNeeded(); - if (!window.sessionFeatureFlags.useSharedUtilForUserConfig) { + const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + if (!userConfigLibsession) { const lastSyncedTimestamp = (await getLastSyncTimestampFromDb()) || 0; const now = Date.now(); @@ -75,9 +77,9 @@ export const syncConfigurationIfNeeded = async () => { } }; -export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = false) => - new Promise(resolve => { - const allConvos = getConversationController().getConversations(); +export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = false) => { + await ReleasedFeatures.checkIsUserConfigFeatureReleased(); + return new Promise(resolve => { // if we hang for more than 20sec, force resolve this promise. setTimeout(() => { resolve(false); @@ -90,17 +92,19 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal e.message ); }); - if (window.sessionFeatureFlags.useSharedUtilForUserConfig) { + if (ReleasedFeatures.isUserConfigFeatureReleasedCached()) { if (waitForMessageSent) { window.Whisper.events.once(ConfigurationSyncJobDone, () => { resolve(true); return; }); + return; } else { resolve(true); return; } } + const allConvos = getConversationController().getConversations(); void getCurrentConfigurationMessage(allConvos) .then(configMessage => { @@ -128,6 +132,7 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal resolve(false); }); }); +}; const getActiveOpenGroupV2CompleteUrls = async ( convos: Array diff --git a/ts/test/session/unit/utils/job_runner/JobRunner_test.ts b/ts/test/session/unit/utils/job_runner/JobRunner_test.ts index 75d5dda6a..f8a024950 100644 --- a/ts/test/session/unit/utils/job_runner/JobRunner_test.ts +++ b/ts/test/session/unit/utils/job_runner/JobRunner_test.ts @@ -77,9 +77,9 @@ describe('JobRunner', () => { const job = getFakeSleepForJob(123); await runner.addJob(job); - throw new Error('PLOP'); // the line above should throw something else + throw new Error('fake error'); // the line above should throw something else } catch (e) { - expect(e.message).to.not.eq('PLOP'); + expect(e.message).to.not.eq('fake error'); } }); it('unsorted list is sorted after loading', async () => { diff --git a/ts/util/releaseFeature.ts b/ts/util/releaseFeature.ts new file mode 100644 index 000000000..2465bb636 --- /dev/null +++ b/ts/util/releaseFeature.ts @@ -0,0 +1,104 @@ +import { GetNetworkTime } from '../session/apis/snode_api/getNetworkTime'; +import { assertUnreachable } from '../types/sqlSharedTypes'; +import { Storage } from './storage'; + +let isDisappearingMessageFeatureReleased: boolean | undefined; +let isUserConfigLibsessionFeatureReleased: boolean | undefined; +type FeatureNameTracked = 'disappearing_messages' | 'user_config_libsession'; + +/** + * This is only intended for testing. Do not call this in production. + */ +export function resetFeatureReleasedCachedValue() { + isDisappearingMessageFeatureReleased = undefined; + isUserConfigLibsessionFeatureReleased = undefined; +} + +function getIsFeatureReleasedCached(featureName: FeatureNameTracked) { + switch (featureName) { + case 'disappearing_messages': + return isDisappearingMessageFeatureReleased; + case 'user_config_libsession': + return isUserConfigLibsessionFeatureReleased; + default: + assertUnreachable(featureName, `case not handled for getIsFeatureReleasedCached`); + } +} + +function setIsFeatureReleasedCached(featureName: FeatureNameTracked, value: boolean) { + switch (featureName) { + case 'disappearing_messages': + isDisappearingMessageFeatureReleased = value; + break; + case 'user_config_libsession': + isUserConfigLibsessionFeatureReleased = value; + break; + default: + assertUnreachable(featureName, `case not handled for setIsFeatureReleasedCached `); + } +} + +function getFeatureReleaseTimestamp(featureName: FeatureNameTracked) { + switch (featureName) { + case 'disappearing_messages': + // TODO update to agreed value between platforms for `disappearing_messages` + return 1706778000000; // unix 01/02/2024 09:00; + // return 1677488400000; // testing: unix 27/02/2023 09:00 + case 'user_config_libsession': + // TODO update to agreed value between platforms for `user_config_libsession` + + return 1706778000000; // unix 01/02/2024 09:00; + // return 1677488400000; // testing: unix 27/02/2023 09:00 + + default: + assertUnreachable(featureName, `case not handled for getFeatureReleaseTimestamp `); + } +} + +export async function getIsFeatureReleased(featureName: FeatureNameTracked): Promise { + if (getIsFeatureReleasedCached(featureName) === undefined) { + // read values from db and cache them as it looks like we did not + const oldIsFeatureReleased = Boolean(Storage.get(`featureReleased-${featureName}`)); + // values do not exist in the db yet. Let's store false for now in the db and update our cached value. + if (oldIsFeatureReleased === undefined) { + await Storage.put(`featureReleased-${featureName}`, false); + setIsFeatureReleasedCached(featureName, false); + } else { + setIsFeatureReleasedCached(featureName, oldIsFeatureReleased); + } + } + return Boolean(getIsFeatureReleasedCached(featureName)); +} + +async function checkIsFeatureReleased(featureName: FeatureNameTracked): Promise { + const featureAlreadyReleased = await getIsFeatureReleased(featureName); + + // Is it time to release the feature based on the network timestamp? + if ( + !featureAlreadyReleased && + GetNetworkTime.getNowWithNetworkOffset() >= getFeatureReleaseTimestamp(featureName) + ) { + window.log.info(`[releaseFeature]: It is time to release ${featureName}. Releasing it now`); + await Storage.put(`featureReleased-${featureName}`, true); + setIsFeatureReleasedCached(featureName, true); + } + + const isReleased = Boolean(getIsFeatureReleasedCached(featureName)); + window.log.debug( + `[releaseFeature]: "${featureName}" ${isReleased ? 'is released' : 'has not been released yet'}` + ); + return isReleased; +} + +function checkIsUserConfigFeatureReleased() { + return checkIsFeatureReleased('user_config_libsession'); +} + +function isUserConfigFeatureReleasedCached(): boolean { + return !!isUserConfigLibsessionFeatureReleased; +} + +export const ReleasedFeatures = { + checkIsUserConfigFeatureReleased, + isUserConfigFeatureReleasedCached, +}; diff --git a/ts/util/storage.ts b/ts/util/storage.ts index c864b1d47..856f77f4d 100644 --- a/ts/util/storage.ts +++ b/ts/util/storage.ts @@ -3,6 +3,7 @@ import { SessionKeyPair } from '../receiver/keypairs'; import { DEFAULT_RECENT_REACTS } from '../session/constants'; import { deleteSettingsBoolValue, updateSettingsBoolValue } from '../state/ducks/settings'; import { isBoolean } from 'lodash'; +import { ReleasedFeatures } from './releaseFeature'; let ready = false; @@ -135,7 +136,7 @@ export function getLastProfileUpdateTimestamp() { } export async function setLastProfileUpdateTimestamp(lastUpdateTimestamp: number) { - if (window.sessionFeatureFlags.useSharedUtilForUserConfig) { + if (await ReleasedFeatures.checkIsUserConfigFeatureReleased()) { return; } await put('last_profile_update_timestamp', lastUpdateTimestamp); diff --git a/ts/window.d.ts b/ts/window.d.ts index a0bad52c3..586a787ed 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -37,7 +37,6 @@ declare global { useOnionRequests: boolean; useTestNet: boolean; useClosedGroupV3: boolean; - useSharedUtilForUserConfig: boolean; debug: { debugFileServerRequests: boolean; debugNonSnodeRequests: boolean;