// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReadonlyDeep } from 'type-fest'; import { generateSecurityNumberBlock } from '../../util/safetyNumber'; import type { ConversationType } from './conversations'; import { reloadProfiles, toggleVerification, } from '../../shims/contactVerification'; import * as log from '../../logging/log'; import * as Errors from '../../types/errors'; export type SafetyNumberContactType = ReadonlyDeep<{ safetyNumber: string; safetyNumberChanged?: boolean; verificationDisabled: boolean; }>; export type SafetyNumberStateType = ReadonlyDeep<{ contacts: { [key: string]: SafetyNumberContactType; }; }>; 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; }>; 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; }; }>; type ToggleVerifiedPendingActionType = ReadonlyDeep<{ type: 'safetyNumber/TOGGLE_VERIFIED_PENDING'; payload: ToggleVerifiedAsyncActionType; }>; type ToggleVerifiedFulfilledActionType = ReadonlyDeep<{ type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED'; payload: ToggleVerifiedAsyncActionType; }>; export type SafetyNumberActionType = ReadonlyDeep< | GenerateActionType | GenerateFulfilledActionType | ToggleVerifiedActionType | ToggleVerifiedPendingActionType | ToggleVerifiedFulfilledActionType >; function generate(contact: ConversationType): GenerateActionType { return { type: GENERATE, payload: doGenerate(contact), }; } async function doGenerate( contact: ConversationType ): Promise { const securityNumberBlock = await generateSecurityNumberBlock(contact); return { contact, safetyNumber: securityNumberBlock.join(' '), }; } function toggleVerified(contact: ConversationType): ToggleVerifiedActionType { return { type: TOGGLE_VERIFIED, payload: { data: { contact }, promise: doToggleVerified(contact), }, }; } async function alterVerification(contact: ConversationType): Promise { try { await toggleVerification(contact.id); } catch (result) { if (result instanceof Error) { if (result.name === 'OutgoingIdentityKeyError') { throw result; } else { log.error('failed to toggle verified:', Errors.toLogFormat(result)); } } else { const keyError = result.errors.find( (error: Error) => error.name === 'OutgoingIdentityKeyError' ); if (keyError) { throw keyError; } else { result.errors.forEach((error: Error) => { log.error('failed to toggle verified:', Errors.toLogFormat(error)); }); } } } } async function doToggleVerified( contact: ConversationType ): Promise { 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, }; export function getEmptyState(): SafetyNumberStateType { return { contacts: {}, }; } export function reducer( state: Readonly = getEmptyState(), action: Readonly ): SafetyNumberStateType { if (action.type === TOGGLE_VERIFIED_PENDING) { const { contact } = action.payload; const { id } = contact; const record = state.contacts[id]; return { contacts: { ...state.contacts, [id]: { ...record, safetyNumberChanged: false, verificationDisabled: true, }, }, }; } if (action.type === TOGGLE_VERIFIED_FULFILLED) { const { contact, ...restProps } = action.payload; const { id } = contact; const record = state.contacts[id]; return { contacts: { ...state.contacts, [id]: { ...record, ...restProps, verificationDisabled: false, }, }, }; } if (action.type === GENERATE_FULFILLED) { const { contact, safetyNumber } = action.payload; const { id } = contact; const record = state.contacts[id]; return { contacts: { ...state.contacts, [id]: { ...record, safetyNumber, }, }, }; } return state; }