Skip to content

Commit

Permalink
Simplify redux ducks and avoid reexport
Browse files Browse the repository at this point in the history
  • Loading branch information
indutny-signal committed Apr 7, 2023
1 parent bd41d7b commit d34d187
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 92 deletions.
2 changes: 2 additions & 0 deletions ts/RemoteConfig.ts
Expand Up @@ -27,6 +27,8 @@ export type ConfigKeyType =
| 'desktop.messageCleanup'
| 'desktop.messageRequests'
| 'desktop.pnp'
| 'desktop.safetyNumberUUID'
| 'desktop.safetyNumberUUID.timestamp'
| 'desktop.retryReceiptLifespan'
| 'desktop.retryRespondMaxAge'
| 'desktop.senderKey.retry'
Expand Down
146 changes: 77 additions & 69 deletions ts/state/ducks/safetyNumber.ts
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only

import type { ReadonlyDeep } from 'type-fest';
import type { ThunkAction } from 'redux-thunk';

import { generateSecurityNumberBlock } from '../../util/safetyNumber';
import type { ConversationType } from './conversations';
import {
Expand All @@ -10,6 +12,8 @@ import {
} from '../../shims/contactVerification';
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
import type { StateType as RootStateType } from '../reducer';
import { getSecurityNumberIdentifierType } from '../selectors/items';

export type SafetyNumberContactType = ReadonlyDeep<{
safetyNumber: string;
Expand All @@ -23,83 +27,108 @@ export type SafetyNumberStateType = ReadonlyDeep<{
};
}>;

const GENERATE = 'safetyNumber/GENERATE';
const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED';
const TOGGLE_VERIFIED = 'safetyNumber/TOGGLE_VERIFIED';
const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING';

type GenerateAsyncActionType = ReadonlyDeep<{
contact: ConversationType;
safetyNumber: string;
}>;

type GenerateActionType = ReadonlyDeep<{
type: 'safetyNumber/GENERATE';
payload: Promise<GenerateAsyncActionType>;
}>;

type GenerateFulfilledActionType = ReadonlyDeep<{
type: 'safetyNumber/GENERATE_FULFILLED';
payload: GenerateAsyncActionType;
}>;

type ToggleVerifiedAsyncActionType = ReadonlyDeep<{
contact: ConversationType;
safetyNumber?: string;
safetyNumberChanged?: boolean;
}>;

type ToggleVerifiedActionType = ReadonlyDeep<{
type: 'safetyNumber/TOGGLE_VERIFIED';
payload: {
data: { contact: ConversationType };
promise: Promise<ToggleVerifiedAsyncActionType>;
contact: ConversationType;
safetyNumber: string;
};
}>;

type ToggleVerifiedPendingActionType = ReadonlyDeep<{
type: 'safetyNumber/TOGGLE_VERIFIED_PENDING';
payload: ToggleVerifiedAsyncActionType;
payload: {
contact: ConversationType;
};
}>;

type ToggleVerifiedFulfilledActionType = ReadonlyDeep<{
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
payload: ToggleVerifiedAsyncActionType;
payload: {
contact: ConversationType;
safetyNumber?: string;
safetyNumberChanged?: boolean;
};
}>;

export type SafetyNumberActionType = ReadonlyDeep<
| GenerateActionType
| GenerateFulfilledActionType
| ToggleVerifiedActionType
| ToggleVerifiedPendingActionType
| ToggleVerifiedFulfilledActionType
>;

function generate(contact: ConversationType): GenerateActionType {
return {
type: GENERATE,
payload: doGenerate(contact),
function generate(
contact: ConversationType
): ThunkAction<void, RootStateType, unknown, GenerateFulfilledActionType> {
return async (dispatch, getState) => {
try {
const securityNumberBlock = await generateSecurityNumberBlock(
contact,
getSecurityNumberIdentifierType(getState(), { now: Date.now() })
);
dispatch({
type: GENERATE_FULFILLED,
payload: {
contact,
safetyNumber: securityNumberBlock.join(' '),
},
});
} catch (error) {
log.error(
'failed to generate security number:',
Errors.toLogFormat(error)
);
}
};
}

async function doGenerate(
function toggleVerified(
contact: ConversationType
): Promise<GenerateAsyncActionType> {
const securityNumberBlock = await generateSecurityNumberBlock(contact);
return {
contact,
safetyNumber: securityNumberBlock.join(' '),
};
}
): ThunkAction<
void,
RootStateType,
unknown,
ToggleVerifiedPendingActionType | ToggleVerifiedFulfilledActionType
> {
return async (dispatch, getState) => {
dispatch({
type: TOGGLE_VERIFIED_PENDING,
payload: {
contact,
},
});

function toggleVerified(contact: ConversationType): ToggleVerifiedActionType {
return {
type: TOGGLE_VERIFIED,
payload: {
data: { contact },
promise: doToggleVerified(contact),
},
try {
await alterVerification(contact);

dispatch({
type: TOGGLE_VERIFIED_FULFILLED,
payload: {
contact,
},
});
} catch (err) {
if (err.name === 'OutgoingIdentityKeyError') {
await reloadProfiles(contact.id);
const securityNumberBlock = await generateSecurityNumberBlock(
contact,
getSecurityNumberIdentifierType(getState(), { now: Date.now() })
);

dispatch({
type: TOGGLE_VERIFIED_FULFILLED,
payload: {
contact,
safetyNumber: securityNumberBlock.join(' '),
safetyNumberChanged: true,
},
});
}
}
};
}

Expand Down Expand Up @@ -128,27 +157,6 @@ async function alterVerification(contact: ConversationType): Promise<void> {
}
}

async function doToggleVerified(
contact: ConversationType
): Promise<ToggleVerifiedAsyncActionType> {
try {
await alterVerification(contact);
} catch (err) {
if (err.name === 'OutgoingIdentityKeyError') {
await reloadProfiles(contact.id);
const securityNumberBlock = await generateSecurityNumberBlock(contact);

return {
contact,
safetyNumber: securityNumberBlock.join(' '),
safetyNumberChanged: true,
};
}
}

return { contact };
}

export const actions = {
generateSafetyNumber: generate,
toggleVerified,
Expand Down
20 changes: 20 additions & 0 deletions ts/state/selectors/items.ts
Expand Up @@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
import { isInteger } from 'lodash';

import { ITEM_NAME as UNIVERSAL_EXPIRE_TIMER_ITEM } from '../../util/universalExpireTimer';
import { SecurityNumberIdentifierType } from '../../util/safetyNumber';
import { innerIsBucketValueEnabled } from '../../RemoteConfig';
import type { ConfigKeyType, ConfigMapType } from '../../RemoteConfig';
import type { StateType } from '../reducer';
Expand Down Expand Up @@ -145,6 +146,25 @@ export const getContactManagementEnabled = createSelector(
}
);

export const getSecurityNumberIdentifierType = createSelector(
getRemoteConfig,
(_state: StateType, { now }: { now: number }) => now,
(remoteConfig: ConfigMapType, now: number): SecurityNumberIdentifierType => {
if (isRemoteConfigFlagEnabled(remoteConfig, 'desktop.safetyNumberUUID')) {
return SecurityNumberIdentifierType.UUIDIdentifier;
}

const timestamp = remoteConfig['desktop.safetyNumberUUID.timestamp']?.value;
if (typeof timestamp !== 'number') {
return SecurityNumberIdentifierType.E164Identifier;
}

return now >= timestamp
? SecurityNumberIdentifierType.UUIDIdentifier
: SecurityNumberIdentifierType.E164Identifier;
}
);

export const getDefaultConversationColor = createSelector(
getItems,
(
Expand Down
2 changes: 0 additions & 2 deletions ts/util/index.ts
Expand Up @@ -9,7 +9,6 @@ import { createBatcher } from './batcher';
import { createWaitBatcher } from './waitBatcher';
import { deleteForEveryone } from './deleteForEveryone';
import { downloadAttachment } from './downloadAttachment';
import { generateSecurityNumber } from './safetyNumber';
import { getStringForProfileChange } from './getStringForProfileChange';
import { getTextWithMentions } from './getTextWithMentions';
import { getUuidsForE164s } from './getUuidsForE164s';
Expand Down Expand Up @@ -53,7 +52,6 @@ export {
downloadAttachment,
flushMessageCounter,
fromWebSafeBase64,
generateSecurityNumber,
getStringForProfileChange,
getTextWithMentions,
getUserAgent,
Expand Down
64 changes: 43 additions & 21 deletions ts/util/safetyNumber.ts
Expand Up @@ -6,17 +6,18 @@ import type { ConversationType } from '../state/ducks/conversations';
import { UUID } from '../types/UUID';

import { assertDev } from './assert';
import { missingCaseError } from './missingCaseError';
import * as log from '../logging/log';

export async function generateSecurityNumber(
ourNumber: string,
function generateSecurityNumber(
ourId: string,
ourKey: Uint8Array,
theirNumber: string,
theirId: string,
theirKey: Uint8Array
): Promise<string> {
const ourNumberBuf = Buffer.from(ourNumber);
): string {
const ourNumberBuf = Buffer.from(ourId);
const ourKeyObj = PublicKey.deserialize(Buffer.from(ourKey));
const theirNumberBuf = Buffer.from(theirNumber);
const theirNumberBuf = Buffer.from(theirId);
const theirKeyObj = PublicKey.deserialize(Buffer.from(theirKey));

const fingerprint = Fingerprint.new(
Expand All @@ -28,13 +29,21 @@ export async function generateSecurityNumber(
theirKeyObj
);

const fingerprintString = fingerprint.displayableFingerprint().toString();
return Promise.resolve(fingerprintString);
return fingerprint.displayableFingerprint().toString();
}

export enum SecurityNumberIdentifierType {
UUIDIdentifier = 'UUIDIdentifier',
E164Identifier = 'E164Identifier',
}

export async function generateSecurityNumberBlock(
contact: ConversationType
contact: ConversationType,
identifierType: SecurityNumberIdentifierType
): Promise<Array<string>> {
const logId = `generateSecurityNumberBlock(${contact.id}, ${identifierType})`;
log.info(`${logId}: starting`);

const { storage } = window.textsecure;
const ourNumber = storage.user.getNumber();
const ourUuid = storage.user.getCheckedUuid();
Expand All @@ -56,21 +65,34 @@ export async function generateSecurityNumberBlock(
throw new Error('Could not load their key');
}

if (!contact.e164) {
log.error(
'generateSecurityNumberBlock: Attempted to generate security number for contact with no e164'
let securityNumber: string;
if (identifierType === SecurityNumberIdentifierType.E164Identifier) {
if (!contact.e164) {
log.error(
`${logId}: Attempted to generate security number for contact with no e164`
);
return [];
}

assertDev(ourNumber, 'Should have our number');
securityNumber = generateSecurityNumber(
ourNumber,
ourKey,
contact.e164,
theirKey
);
return [];
} else if (identifierType === SecurityNumberIdentifierType.UUIDIdentifier) {
assertDev(theirUuid, 'Should have their uuid');
securityNumber = generateSecurityNumber(
ourUuid.toString(),
ourKey,
theirUuid.toString(),
theirKey
);
} else {
throw missingCaseError(identifierType);
}

assertDev(ourNumber, 'Should have our number');
const securityNumber = await generateSecurityNumber(
ourNumber,
ourKey,
contact.e164,
theirKey
);

const chunks = [];
for (let i = 0; i < securityNumber.length; i += 5) {
chunks.push(securityNumber.substring(i, i + 5));
Expand Down

0 comments on commit d34d187

Please sign in to comment.