Switch remote config fetching to use new endpoint
This commit is contained in:
parent
8305ad160f
commit
1d37db78d2
13 changed files with 303 additions and 155 deletions
|
@ -236,7 +236,7 @@
|
||||||
"@indutny/parallel-prettier": "3.0.0",
|
"@indutny/parallel-prettier": "3.0.0",
|
||||||
"@indutny/rezip-electron": "2.0.1",
|
"@indutny/rezip-electron": "2.0.1",
|
||||||
"@napi-rs/canvas": "0.1.61",
|
"@napi-rs/canvas": "0.1.61",
|
||||||
"@signalapp/mock-server": "13.2.2",
|
"@signalapp/mock-server": "13.3.0",
|
||||||
"@storybook/addon-a11y": "8.4.4",
|
"@storybook/addon-a11y": "8.4.4",
|
||||||
"@storybook/addon-actions": "8.4.4",
|
"@storybook/addon-actions": "8.4.4",
|
||||||
"@storybook/addon-controls": "8.4.4",
|
"@storybook/addon-controls": "8.4.4",
|
||||||
|
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
@ -439,8 +439,8 @@ importers:
|
||||||
specifier: 0.1.61
|
specifier: 0.1.61
|
||||||
version: 0.1.61
|
version: 0.1.61
|
||||||
'@signalapp/mock-server':
|
'@signalapp/mock-server':
|
||||||
specifier: 13.2.2
|
specifier: 13.3.0
|
||||||
version: 13.2.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
version: 13.3.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||||
'@storybook/addon-a11y':
|
'@storybook/addon-a11y':
|
||||||
specifier: 8.4.4
|
specifier: 8.4.4
|
||||||
version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10))
|
version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10))
|
||||||
|
@ -3302,8 +3302,8 @@ packages:
|
||||||
'@signalapp/minimask@1.0.1':
|
'@signalapp/minimask@1.0.1':
|
||||||
resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==}
|
resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==}
|
||||||
|
|
||||||
'@signalapp/mock-server@13.2.2':
|
'@signalapp/mock-server@13.3.0':
|
||||||
resolution: {integrity: sha512-iwJ5fAXIPetc2mW4Q37Pkd+FwCxVvSYjn753KJlchTcyJq3xLnHHSvz1SDvgXwk6i6y6p7Mwe2V86WAXCyrxBA==}
|
resolution: {integrity: sha512-qWLI+J0hptzKC3Xm9FWWqFMvJ+jpLLPRq+Y6gdbprfA/DMHcNK53T8A54onbEyqJHnxdPoyqxtH4wcsiS1HglQ==}
|
||||||
|
|
||||||
'@signalapp/parchment-cjs@3.0.1':
|
'@signalapp/parchment-cjs@3.0.1':
|
||||||
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
||||||
|
@ -13948,7 +13948,7 @@ snapshots:
|
||||||
|
|
||||||
'@signalapp/minimask@1.0.1': {}
|
'@signalapp/minimask@1.0.1': {}
|
||||||
|
|
||||||
'@signalapp/mock-server@13.2.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
'@signalapp/mock-server@13.3.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
||||||
'@signalapp/libsignal-client': 0.76.7
|
'@signalapp/libsignal-client': 0.76.7
|
||||||
|
|
|
@ -10,60 +10,60 @@ import { parseIntOrThrow } from './util/parseIntOrThrow';
|
||||||
import { HOUR } from './util/durations';
|
import { HOUR } from './util/durations';
|
||||||
import * as Bytes from './Bytes';
|
import * as Bytes from './Bytes';
|
||||||
import { uuidToBytes } from './util/uuidToBytes';
|
import { uuidToBytes } from './util/uuidToBytes';
|
||||||
import { dropNull } from './util/dropNull';
|
|
||||||
import { HashType } from './types/Crypto';
|
import { HashType } from './types/Crypto';
|
||||||
import { getCountryCode } from './types/PhoneNumber';
|
import { getCountryCode } from './types/PhoneNumber';
|
||||||
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
|
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
|
||||||
|
|
||||||
const log = createLogger('RemoteConfig');
|
const log = createLogger('RemoteConfig');
|
||||||
|
|
||||||
export type ConfigKeyType =
|
const KnownConfigKeys = [
|
||||||
| 'desktop.chatFolders.alpha'
|
'desktop.chatFolders.alpha',
|
||||||
| 'desktop.chatFolders.beta'
|
'desktop.chatFolders.beta',
|
||||||
| 'desktop.chatFolders.prod'
|
'desktop.chatFolders.prod',
|
||||||
| 'desktop.clientExpiration'
|
'desktop.clientExpiration',
|
||||||
| 'desktop.backup.credentialFetch'
|
'desktop.backup.credentialFetch',
|
||||||
| 'desktop.donations'
|
'desktop.donations',
|
||||||
| 'desktop.internalUser'
|
'desktop.internalUser',
|
||||||
| 'desktop.mediaQuality.levels'
|
'desktop.mediaQuality.levels',
|
||||||
| 'desktop.messageCleanup'
|
'desktop.messageCleanup',
|
||||||
| 'desktop.retryRespondMaxAge'
|
'desktop.retryRespondMaxAge',
|
||||||
| 'desktop.senderKey.retry'
|
'desktop.senderKey.retry',
|
||||||
| 'desktop.senderKeyMaxAge'
|
'desktop.senderKeyMaxAge',
|
||||||
| 'desktop.experimentalTransport.enableAuth'
|
'desktop.experimentalTransport.enableAuth',
|
||||||
| 'desktop.experimentalTransportEnabled.alpha'
|
'desktop.experimentalTransportEnabled.alpha',
|
||||||
| 'desktop.experimentalTransportEnabled.beta'
|
'desktop.experimentalTransportEnabled.beta',
|
||||||
| 'desktop.experimentalTransportEnabled.prod.2'
|
'desktop.experimentalTransportEnabled.prod.2',
|
||||||
| 'desktop.libsignalNet.enforceMinimumTls'
|
'desktop.libsignalNet.enforceMinimumTls',
|
||||||
| 'desktop.libsignalNet.shadowUnauthChatWithNoise'
|
'desktop.libsignalNet.shadowUnauthChatWithNoise',
|
||||||
| 'desktop.libsignalNet.shadowAuthChatWithNoise'
|
'desktop.libsignalNet.shadowAuthChatWithNoise',
|
||||||
| 'desktop.cdsiViaLibsignal'
|
'desktop.cdsiViaLibsignal',
|
||||||
| 'desktop.cdsiViaLibsignal.disableNewConnectionLogic'
|
'desktop.cdsiViaLibsignal.disableNewConnectionLogic',
|
||||||
| 'desktop.funPicker' // alpha
|
'desktop.funPicker', // alpha
|
||||||
| 'desktop.funPicker.beta'
|
'desktop.funPicker.beta',
|
||||||
| 'desktop.funPicker.prod'
|
'desktop.funPicker.prod',
|
||||||
| 'desktop.usePqRatchet'
|
'desktop.usePqRatchet',
|
||||||
| 'global.attachments.maxBytes'
|
'global.attachments.maxBytes',
|
||||||
| 'global.attachments.maxReceiveBytes'
|
'global.attachments.maxReceiveBytes',
|
||||||
| 'global.backups.mediaTierFallbackCdnNumber'
|
'global.backups.mediaTierFallbackCdnNumber',
|
||||||
| 'global.calling.maxGroupCallRingSize'
|
'global.calling.maxGroupCallRingSize',
|
||||||
| 'global.groupsv2.groupSizeHardLimit'
|
'global.groupsv2.groupSizeHardLimit',
|
||||||
| 'global.groupsv2.maxGroupSize'
|
'global.groupsv2.maxGroupSize',
|
||||||
| 'global.messageQueueTimeInSeconds'
|
'global.messageQueueTimeInSeconds',
|
||||||
| 'global.nicknames.max'
|
'global.nicknames.max',
|
||||||
| 'global.nicknames.min'
|
'global.nicknames.min',
|
||||||
| 'global.textAttachmentLimitBytes';
|
'global.textAttachmentLimitBytes',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ConfigKeyType = (typeof KnownConfigKeys)[number];
|
||||||
|
|
||||||
type ConfigValueType = {
|
type ConfigValueType = {
|
||||||
name: ConfigKeyType;
|
name: ConfigKeyType;
|
||||||
enabled: boolean;
|
|
||||||
enabledAt?: number;
|
enabledAt?: number;
|
||||||
value?: string;
|
} & ({ enabled: true; value: string } | { enabled: false; value?: never });
|
||||||
};
|
|
||||||
export type ConfigMapType = {
|
export type ConfigMapType = {
|
||||||
[key in ConfigKeyType]?: ConfigValueType;
|
[key in ConfigKeyType]?: ConfigValueType;
|
||||||
};
|
};
|
||||||
type ConfigListenerType = (value: ConfigValueType) => unknown;
|
export type ConfigListenerType = (value: ConfigValueType) => unknown;
|
||||||
type ConfigListenersMapType = {
|
type ConfigListenersMapType = {
|
||||||
[key: string]: Array<ConfigListenerType>;
|
[key: string]: Array<ConfigListenerType>;
|
||||||
};
|
};
|
||||||
|
@ -92,7 +92,13 @@ export const _refreshRemoteConfig = async (
|
||||||
server: WebAPIType
|
server: WebAPIType
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const { config: newConfig, serverTimestamp } = await server.getConfig();
|
const oldConfigHash = window.storage.get('remoteConfigHash');
|
||||||
|
|
||||||
|
const {
|
||||||
|
config: newConfig,
|
||||||
|
serverTimestamp,
|
||||||
|
configHash,
|
||||||
|
} = await server.getConfig(oldConfigHash);
|
||||||
|
|
||||||
const serverTimeSkew = serverTimestamp - now;
|
const serverTimeSkew = serverTimestamp - now;
|
||||||
|
|
||||||
|
@ -103,27 +109,51 @@ export const _refreshRemoteConfig = async (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process new configuration in light of the old configuration
|
if (newConfig === 'unmodified') {
|
||||||
// The old configuration is not set as the initial value in reduce because
|
log.info(
|
||||||
// flags may have been deleted
|
'remote config was unmodified; server-generated hash is %s',
|
||||||
|
configHash
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process new configuration in light of the old configuration. Since the
|
||||||
|
// new configuration only includes enabled flags we can't distinguish betewen
|
||||||
|
// a remote flag being deleted or being disabled. We synthesize that for our
|
||||||
|
// known keys.
|
||||||
|
const newConfigValues: Map<string, string | undefined> = new Map(
|
||||||
|
KnownConfigKeys.map(name => [name, undefined])
|
||||||
|
);
|
||||||
|
for (const [name, value] of newConfig) {
|
||||||
|
newConfigValues.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
const oldConfig = config;
|
const oldConfig = config;
|
||||||
config = newConfig.reduce((acc, { name, enabled, value }) => {
|
config = Array.from(newConfigValues.entries()).reduce(
|
||||||
const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false);
|
(acc, [name, value]) => {
|
||||||
|
const enabled = value !== undefined;
|
||||||
|
const previouslyEnabled: boolean = get(
|
||||||
|
oldConfig,
|
||||||
|
[name, 'enabled'],
|
||||||
|
false
|
||||||
|
);
|
||||||
const previousValue: string | undefined = get(
|
const previousValue: string | undefined = get(
|
||||||
oldConfig,
|
oldConfig,
|
||||||
[name, 'value'],
|
[name, 'value'],
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
// If a flag was previously not enabled and is now enabled,
|
// If a flag was previously not enabled and is now enabled,
|
||||||
// record the time it was enabled
|
// record the time it was enabled
|
||||||
const enabledAt: number | undefined =
|
const enabledAt: number | undefined =
|
||||||
previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']);
|
previouslyEnabled && enabled
|
||||||
|
? now
|
||||||
|
: get(oldConfig, [name, 'enabledAt']);
|
||||||
|
|
||||||
const configValue = {
|
const configValue: ConfigValueType = {
|
||||||
name: name as ConfigKeyType,
|
name: name as ConfigKeyType,
|
||||||
enabled,
|
|
||||||
enabledAt,
|
enabledAt,
|
||||||
value: dropNull(value),
|
...(enabled ? { enabled: true, value } : { enabled: false }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasChanged =
|
const hasChanged =
|
||||||
|
@ -143,7 +173,9 @@ export const _refreshRemoteConfig = async (
|
||||||
...acc,
|
...acc,
|
||||||
[name]: configValue,
|
[name]: configValue,
|
||||||
};
|
};
|
||||||
}, {});
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
const remoteExpirationValue = getValue('desktop.clientExpiration');
|
const remoteExpirationValue = getValue('desktop.clientExpiration');
|
||||||
if (!remoteExpirationValue) {
|
if (!remoteExpirationValue) {
|
||||||
|
@ -165,6 +197,7 @@ export const _refreshRemoteConfig = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.storage.put('remoteConfig', config);
|
await window.storage.put('remoteConfig', config);
|
||||||
|
await window.storage.put('remoteConfigHash', configHash);
|
||||||
await window.storage.put('serverTimeSkew', serverTimeSkew);
|
await window.storage.put('serverTimeSkew', serverTimeSkew);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -193,7 +226,7 @@ export function isEnabled(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getValue(name: ConfigKeyType): string | undefined {
|
export function getValue(name: ConfigKeyType): string | undefined {
|
||||||
return get(config, [name, 'value'], undefined);
|
return get(config, [name, 'value']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// See isRemoteConfigBucketEnabled in selectors/items.ts
|
// See isRemoteConfigBucketEnabled in selectors/items.ts
|
||||||
|
|
|
@ -1509,10 +1509,12 @@ export async function startApp(): Promise<void> {
|
||||||
// Listen for changes to the `desktop.clientExpiration` remote flag
|
// Listen for changes to the `desktop.clientExpiration` remote flag
|
||||||
window.Signal.RemoteConfig.onChange(
|
window.Signal.RemoteConfig.onChange(
|
||||||
'desktop.clientExpiration',
|
'desktop.clientExpiration',
|
||||||
({ value }) => {
|
({ enabled, value }) => {
|
||||||
const remoteBuildExpirationTimestamp = parseRemoteClientExpiration(
|
if (!enabled) {
|
||||||
value as string
|
return;
|
||||||
);
|
}
|
||||||
|
const remoteBuildExpirationTimestamp =
|
||||||
|
parseRemoteClientExpiration(value);
|
||||||
if (remoteBuildExpirationTimestamp) {
|
if (remoteBuildExpirationTimestamp) {
|
||||||
drop(
|
drop(
|
||||||
window.storage.put(
|
window.storage.put(
|
||||||
|
|
|
@ -1971,11 +1971,10 @@ describe('both/state/ducks/conversations', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig([
|
||||||
{ name: 'global.groupsv2.maxGroupSize', value: '22', enabled: true },
|
{ name: 'global.groupsv2.maxGroupSize', value: '22' },
|
||||||
{
|
{
|
||||||
name: 'global.groupsv2.groupSizeHardLimit',
|
name: 'global.groupsv2.groupSizeHardLimit',
|
||||||
value: '33',
|
value: '33',
|
||||||
enabled: true,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -2057,18 +2056,23 @@ describe('both/state/ducks/conversations', () => {
|
||||||
it('defaults the maximum recommended size to 151', async () => {
|
it('defaults the maximum recommended size to 151', async () => {
|
||||||
for (const value of [null, 'xyz']) {
|
for (const value of [null, 'xyz']) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig(
|
||||||
{
|
[
|
||||||
name: 'global.groupsv2.maxGroupSize',
|
|
||||||
value,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'global.groupsv2.groupSizeHardLimit',
|
name: 'global.groupsv2.groupSizeHardLimit',
|
||||||
value: '33',
|
value: '33',
|
||||||
enabled: true,
|
|
||||||
},
|
},
|
||||||
]);
|
].concat(
|
||||||
|
value
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'global.groupsv2.maxGroupSize',
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
...getEmptyState(),
|
...getEmptyState(),
|
||||||
|
@ -2145,14 +2149,18 @@ describe('both/state/ducks/conversations', () => {
|
||||||
it('defaults the maximum group size to 1001 if the recommended maximum is smaller', async () => {
|
it('defaults the maximum group size to 1001 if the recommended maximum is smaller', async () => {
|
||||||
for (const value of [null, 'xyz']) {
|
for (const value of [null, 'xyz']) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig(
|
||||||
{ name: 'global.groupsv2.maxGroupSize', value: '2', enabled: true },
|
[{ name: 'global.groupsv2.maxGroupSize', value: '2' }].concat(
|
||||||
|
value
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
name: 'global.groupsv2.groupSizeHardLimit',
|
name: 'global.groupsv2.groupSizeHardLimit',
|
||||||
value,
|
value,
|
||||||
enabled: true,
|
|
||||||
},
|
},
|
||||||
]);
|
]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
...getEmptyState(),
|
...getEmptyState(),
|
||||||
|
@ -2169,12 +2177,10 @@ describe('both/state/ducks/conversations', () => {
|
||||||
{
|
{
|
||||||
name: 'global.groupsv2.maxGroupSize',
|
name: 'global.groupsv2.maxGroupSize',
|
||||||
value: '1234',
|
value: '1234',
|
||||||
enabled: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'global.groupsv2.groupSizeHardLimit',
|
name: 'global.groupsv2.groupSizeHardLimit',
|
||||||
value: '2',
|
value: '2',
|
||||||
enabled: true,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -362,7 +362,6 @@ describe('getCdnNumberForBackupTier', () => {
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig([
|
||||||
{
|
{
|
||||||
name: 'global.backups.mediaTierFallbackCdnNumber',
|
name: 'global.backups.mediaTierFallbackCdnNumber',
|
||||||
enabled: true,
|
|
||||||
value: '42',
|
value: '42',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -8,11 +8,16 @@ import type {
|
||||||
} from '../textsecure/WebAPI';
|
} from '../textsecure/WebAPI';
|
||||||
|
|
||||||
export async function updateRemoteConfig(
|
export async function updateRemoteConfig(
|
||||||
newConfig: RemoteConfigResponseType['config']
|
newConfig: Array<{ name: string; value: string }>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const fakeServer = {
|
const fakeServer = {
|
||||||
async getConfig() {
|
async getConfig(): Promise<RemoteConfigResponseType> {
|
||||||
return { config: newConfig, serverTimestamp: Date.now() };
|
const serverTimestamp = Date.now();
|
||||||
|
return {
|
||||||
|
config: new Map(newConfig.map(({ name, value }) => [name, value])),
|
||||||
|
serverTimestamp,
|
||||||
|
configHash: serverTimestamp.toString(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
} as Partial<WebAPIType> as unknown as WebAPIType;
|
} as Partial<WebAPIType> as unknown as WebAPIType;
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,20 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
|
import { omit } from 'lodash';
|
||||||
import { normalizeAci } from '../util/normalizeAci';
|
import { normalizeAci } from '../util/normalizeAci';
|
||||||
|
import type { ConfigKeyType, ConfigListenerType } from '../RemoteConfig';
|
||||||
import {
|
import {
|
||||||
getCountryCodeValue,
|
getCountryCodeValue,
|
||||||
getBucketValue,
|
getBucketValue,
|
||||||
innerIsBucketValueEnabled,
|
innerIsBucketValueEnabled,
|
||||||
|
onChange,
|
||||||
|
getValue,
|
||||||
|
isEnabled,
|
||||||
} from '../RemoteConfig';
|
} from '../RemoteConfig';
|
||||||
|
import { updateRemoteConfig } from '../test-helpers/RemoteConfigStub';
|
||||||
|
|
||||||
describe('RemoteConfig', () => {
|
describe('RemoteConfig', () => {
|
||||||
const aci = normalizeAci('95b9729c-51ea-4ddb-b516-652befe78062', 'test');
|
const aci = normalizeAci('95b9729c-51ea-4ddb-b516-652befe78062', 'test');
|
||||||
|
@ -95,4 +102,79 @@ describe('RemoteConfig', () => {
|
||||||
assert.strictEqual(getBucketValue(aci, flagName), 222732);
|
assert.strictEqual(getBucketValue(aci, flagName), 222732);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#getValue', () => {
|
||||||
|
it('returns value if enabled', async () => {
|
||||||
|
await updateRemoteConfig([]);
|
||||||
|
|
||||||
|
assert.equal(getValue('desktop.internalUser'), undefined);
|
||||||
|
|
||||||
|
await updateRemoteConfig([
|
||||||
|
{ name: 'desktop.internalUser', value: 'yes' },
|
||||||
|
]);
|
||||||
|
assert.equal(getValue('desktop.internalUser'), 'yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return disabled value', async () => {
|
||||||
|
await updateRemoteConfig([]);
|
||||||
|
assert.equal(getValue('desktop.internalUser'), undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#isEnabled', () => {
|
||||||
|
it('is false for missing flag', async () => {
|
||||||
|
await updateRemoteConfig([]);
|
||||||
|
assert.equal(isEnabled('desktop.internalUser'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is false for disabled flag', async () => {
|
||||||
|
await updateRemoteConfig([]);
|
||||||
|
assert.equal(isEnabled('desktop.internalUser'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is true for enabled flag', async () => {
|
||||||
|
await updateRemoteConfig([
|
||||||
|
{ name: 'desktop.internalUser', value: 'yes' },
|
||||||
|
]);
|
||||||
|
assert.equal(isEnabled('desktop.internalUser'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects the value of an unknown flag in the config', async () => {
|
||||||
|
assert.equal(
|
||||||
|
isEnabled('desktop.unknownFlagName' as ConfigKeyType),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await updateRemoteConfig([
|
||||||
|
{ name: 'desktop.unknownFlagName', value: 'unknownFlagValue' },
|
||||||
|
]);
|
||||||
|
assert.equal(isEnabled('desktop.unknownFlagName' as ConfigKeyType), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#onChange', () => {
|
||||||
|
it('triggers listener on known flag change', async () => {
|
||||||
|
await updateRemoteConfig([]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
const listener = sinon.spy<ConfigListenerType>(() => {});
|
||||||
|
onChange('desktop.internalUser', listener);
|
||||||
|
|
||||||
|
await updateRemoteConfig([
|
||||||
|
{ name: 'desktop.internalUser', value: 'yes' },
|
||||||
|
]);
|
||||||
|
await updateRemoteConfig([]);
|
||||||
|
await updateRemoteConfig([
|
||||||
|
{ name: 'desktop.internalUser', value: 'yes' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const calls = listener
|
||||||
|
.getCalls()
|
||||||
|
.map(call => omit(call.firstArg, 'enabledAt'));
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
{ name: 'desktop.internalUser', value: 'yes', enabled: true },
|
||||||
|
{ name: 'desktop.internalUser', enabled: false },
|
||||||
|
{ name: 'desktop.internalUser', value: 'yes', enabled: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,14 +36,12 @@ describe('isConversationTooBigToRing', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns whether there are 16 or more people in the group, if the remote config value is bogus', async () => {
|
it('returns whether there are 16 or more people in the group, if the remote config value is bogus', async () => {
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig([{ name: CONFIG_KEY, value: 'uh oh' }]);
|
||||||
{ name: CONFIG_KEY, value: 'uh oh', enabled: true },
|
|
||||||
]);
|
|
||||||
textMaximum(16);
|
textMaximum(16);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns whether there are 9 or more people in the group, if the remote config value is 9', async () => {
|
it('returns whether there are 9 or more people in the group, if the remote config value is 9', async () => {
|
||||||
await updateRemoteConfig([{ name: CONFIG_KEY, value: '9', enabled: true }]);
|
await updateRemoteConfig([{ name: CONFIG_KEY, value: '9' }]);
|
||||||
textMaximum(9);
|
textMaximum(9);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,9 +28,7 @@ describe('group add banned member', () => {
|
||||||
const clientZkGroupCipher = getClientZkGroupCipher(secretParams);
|
const clientZkGroupCipher = getClientZkGroupCipher(secretParams);
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig([{ name: HARD_LIMIT_KEY, value: '5' }]);
|
||||||
{ name: HARD_LIMIT_KEY, value: '5', enabled: true },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add banned member without deleting', () => {
|
it('should add banned member without deleting', () => {
|
||||||
|
|
|
@ -21,15 +21,13 @@ describe('group limit utilities', () => {
|
||||||
|
|
||||||
it('throws if the value in remote config is not a parseable integer', async () => {
|
it('throws if the value in remote config is not a parseable integer', async () => {
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig([
|
||||||
{ name: RECOMMENDED_SIZE_KEY, value: 'uh oh', enabled: true },
|
{ name: RECOMMENDED_SIZE_KEY, value: 'uh oh' },
|
||||||
]);
|
]);
|
||||||
assert.throws(getGroupSizeRecommendedLimit);
|
assert.throws(getGroupSizeRecommendedLimit);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the value in remote config, parsed as an integer', async () => {
|
it('returns the value in remote config, parsed as an integer', async () => {
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig([{ name: RECOMMENDED_SIZE_KEY, value: '123' }]);
|
||||||
{ name: RECOMMENDED_SIZE_KEY, value: '123', enabled: true },
|
|
||||||
]);
|
|
||||||
assert.strictEqual(getGroupSizeRecommendedLimit(), 123);
|
assert.strictEqual(getGroupSizeRecommendedLimit(), 123);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -41,16 +39,12 @@ describe('group limit utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if the value in remote config is not a parseable integer', async () => {
|
it('throws if the value in remote config is not a parseable integer', async () => {
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig([{ name: HARD_LIMIT_KEY, value: 'uh oh' }]);
|
||||||
{ name: HARD_LIMIT_KEY, value: 'uh oh', enabled: true },
|
|
||||||
]);
|
|
||||||
assert.throws(getGroupSizeHardLimit);
|
assert.throws(getGroupSizeHardLimit);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the value in remote config, parsed as an integer', async () => {
|
it('returns the value in remote config, parsed as an integer', async () => {
|
||||||
await updateRemoteConfig([
|
await updateRemoteConfig([{ name: HARD_LIMIT_KEY, value: '123' }]);
|
||||||
{ name: HARD_LIMIT_KEY, value: '123', enabled: true },
|
|
||||||
]);
|
|
||||||
assert.strictEqual(getGroupSizeHardLimit(), 123);
|
assert.strictEqual(getGroupSizeHardLimit(), 123);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -701,6 +701,7 @@ const CHAT_CALLS = {
|
||||||
boostReceiptCredentials: 'v1/subscription/boost/receipt_credentials',
|
boostReceiptCredentials: 'v1/subscription/boost/receipt_credentials',
|
||||||
challenge: 'v1/challenge',
|
challenge: 'v1/challenge',
|
||||||
config: 'v1/config',
|
config: 'v1/config',
|
||||||
|
configV2: 'v2/config',
|
||||||
createBoost: 'v1/subscription/boost/create',
|
createBoost: 'v1/subscription/boost/create',
|
||||||
deliveryCert: 'v1/certificate/delivery',
|
deliveryCert: 'v1/certificate/delivery',
|
||||||
devices: 'v1/devices',
|
devices: 'v1/devices',
|
||||||
|
@ -925,17 +926,13 @@ export type UploadAvatarHeadersOrOtherType = z.infer<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const remoteConfigResponseZod = z.object({
|
const remoteConfigResponseZod = z.object({
|
||||||
config: z
|
config: z.object({}).catchall(z.string()),
|
||||||
.object({
|
|
||||||
name: z.string(),
|
|
||||||
enabled: z.boolean(),
|
|
||||||
value: z.string().nullish(),
|
|
||||||
})
|
|
||||||
.array(),
|
|
||||||
});
|
});
|
||||||
export type RemoteConfigResponseType = z.infer<typeof remoteConfigResponseZod> &
|
export type RemoteConfigResponseType = {
|
||||||
Readonly<{
|
config: Map<string, string> | 'unmodified';
|
||||||
|
} & Readonly<{
|
||||||
serverTimestamp: number;
|
serverTimestamp: number;
|
||||||
|
configHash: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ProfileType = Readonly<{
|
export type ProfileType = Readonly<{
|
||||||
|
@ -1823,7 +1820,7 @@ export type WebAPIType = {
|
||||||
) => Promise<string>;
|
) => Promise<string>;
|
||||||
whoami: () => Promise<WhoamiResultType>;
|
whoami: () => Promise<WhoamiResultType>;
|
||||||
sendChallengeResponse: (challengeResponse: ChallengeType) => Promise<void>;
|
sendChallengeResponse: (challengeResponse: ChallengeType) => Promise<void>;
|
||||||
getConfig: () => Promise<RemoteConfigResponseType>;
|
getConfig: (configHash?: string) => Promise<RemoteConfigResponseType>;
|
||||||
authenticate: (credentials: WebAPICredentials) => Promise<void>;
|
authenticate: (credentials: WebAPICredentials) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
getServerAlerts: () => Array<ServerAlert>;
|
getServerAlerts: () => Array<ServerAlert>;
|
||||||
|
@ -2468,31 +2465,64 @@ export function initialize({
|
||||||
void socketManager.onHasStoriesDisabledChange(newValue);
|
void socketManager.onHasStoriesDisabledChange(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getConfig() {
|
async function getConfig(
|
||||||
|
configHash?: string
|
||||||
|
): Promise<RemoteConfigResponseType> {
|
||||||
const { data, response } = await _ajax({
|
const { data, response } = await _ajax({
|
||||||
host: 'chatService',
|
host: 'chatService',
|
||||||
call: 'config',
|
call: 'configV2',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
responseType: 'jsonwithdetails',
|
responseType: 'jsonwithdetails',
|
||||||
zodSchema: remoteConfigResponseZod,
|
zodSchema: z.union([
|
||||||
|
remoteConfigResponseZod,
|
||||||
|
// When a 304 is returned, the body of the response is empty.
|
||||||
|
z.literal(''),
|
||||||
|
]),
|
||||||
|
headers: {
|
||||||
|
...(configHash && { 'if-none-match': configHash }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverTimestamp = safeParseNumber(
|
const serverTimestamp = safeParseNumber(
|
||||||
response.headers.get('x-signal-timestamp') || ''
|
response.headers.get('x-signal-timestamp') || ''
|
||||||
);
|
);
|
||||||
|
|
||||||
if (serverTimestamp == null) {
|
if (serverTimestamp == null) {
|
||||||
throw new Error('Missing required x-signal-timestamp header');
|
throw new Error('Missing required x-signal-timestamp header');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newConfigHash = response.headers.get('etag');
|
||||||
|
if (newConfigHash == null) {
|
||||||
|
throw new Error('Missing required ETag header');
|
||||||
|
}
|
||||||
|
|
||||||
|
const partialResponse = { serverTimestamp, configHash: newConfigHash };
|
||||||
|
|
||||||
|
if (response.status === 304) {
|
||||||
return {
|
return {
|
||||||
...data,
|
config: 'unmodified',
|
||||||
serverTimestamp,
|
...partialResponse,
|
||||||
config: data.config.filter(
|
};
|
||||||
({ name }: { name: string }) =>
|
}
|
||||||
|
|
||||||
|
if (data === '') {
|
||||||
|
throw new Error('Empty data returned for non-304');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { config: newConfig } = data;
|
||||||
|
|
||||||
|
const config = new Map(
|
||||||
|
Object.entries(newConfig).filter(
|
||||||
|
([name, _value]) =>
|
||||||
name.startsWith('desktop.') ||
|
name.startsWith('desktop.') ||
|
||||||
name.startsWith('global.') ||
|
name.startsWith('global.') ||
|
||||||
name.startsWith('cds.')
|
name.startsWith('cds.')
|
||||||
),
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
...partialResponse,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -147,6 +147,7 @@ export type StorageAccessType = {
|
||||||
'preferred-audio-input-device': AudioDevice | undefined;
|
'preferred-audio-input-device': AudioDevice | undefined;
|
||||||
'preferred-audio-output-device': AudioDevice | undefined;
|
'preferred-audio-output-device': AudioDevice | undefined;
|
||||||
remoteConfig: RemoteConfigType;
|
remoteConfig: RemoteConfigType;
|
||||||
|
remoteConfigHash: string;
|
||||||
serverTimeSkew: number;
|
serverTimeSkew: number;
|
||||||
unidentifiedDeliveryIndicators: boolean;
|
unidentifiedDeliveryIndicators: boolean;
|
||||||
groupCredentials: ReadonlyArray<GroupCredentialType>;
|
groupCredentials: ReadonlyArray<GroupCredentialType>;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue