// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReadonlyDeep } from 'type-fest'; import type { ThunkAction } from 'redux-thunk'; import { omit } from 'lodash'; import { generateSafetyNumber } from '../../util/safetyNumber'; import type { SafetyNumberType } from '../../types/safetyNumber'; import type { ConversationType } from './conversations'; import { reloadProfiles, toggleVerification, } from '../../shims/contactVerification'; import * as log from '../../logging/log'; import * as Errors from '../../types/errors'; import type { StateType as RootStateType } from '../reducer'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions'; export type SafetyNumberContactType = ReadonlyDeep<{ safetyNumber: SafetyNumberType; safetyNumberChanged?: boolean; verificationDisabled: boolean; }>; export type SafetyNumberStateType = ReadonlyDeep<{ contacts: { [key: string]: SafetyNumberContactType; }; }>; const CLEAR_SAFETY_NUMBER = 'safetyNumber/CLEAR_SAFETY_NUMBER'; const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED'; const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED'; const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING'; type ClearSafetyNumberActionType = ReadonlyDeep<{ type: 'safetyNumber/CLEAR_SAFETY_NUMBER'; payload: { contactId: string; }; }>; type GenerateFulfilledActionType = ReadonlyDeep<{ type: 'safetyNumber/GENERATE_FULFILLED'; payload: { contact: ConversationType; safetyNumber: SafetyNumberType; }; }>; type ToggleVerifiedPendingActionType = ReadonlyDeep<{ type: 'safetyNumber/TOGGLE_VERIFIED_PENDING'; payload: { contact: ConversationType; }; }>; type ToggleVerifiedFulfilledActionType = ReadonlyDeep<{ type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED'; payload: { contact: ConversationType; safetyNumber?: SafetyNumberType; safetyNumberChanged?: boolean; }; }>; export type SafetyNumberActionType = ReadonlyDeep< | ClearSafetyNumberActionType | GenerateFulfilledActionType | ToggleVerifiedPendingActionType | ToggleVerifiedFulfilledActionType >; function clearSafetyNumber(contactId: string): ClearSafetyNumberActionType { return { type: CLEAR_SAFETY_NUMBER, payload: { contactId }, }; } function generate( contact: ConversationType ): ThunkAction { return async dispatch => { try { const safetyNumber = await generateSafetyNumber(contact); dispatch({ type: GENERATE_FULFILLED, payload: { contact, safetyNumber, }, }); } catch (error) { log.error( 'failed to generate security number:', Errors.toLogFormat(error) ); } }; } function toggleVerified( contact: ConversationType ): ThunkAction< void, RootStateType, unknown, ToggleVerifiedPendingActionType | ToggleVerifiedFulfilledActionType > { return async dispatch => { dispatch({ type: TOGGLE_VERIFIED_PENDING, payload: { contact, }, }); try { await alterVerification(contact); dispatch({ type: TOGGLE_VERIFIED_FULFILLED, payload: { contact, }, }); } catch (err) { if (err.name === 'OutgoingIdentityKeyError') { await reloadProfiles(contact.id); const safetyNumber = await generateSafetyNumber(contact); dispatch({ type: TOGGLE_VERIFIED_FULFILLED, payload: { contact, safetyNumber, safetyNumberChanged: true, }, }); } } }; } 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)); }); } } } } export const actions = { clearSafetyNumber, generateSafetyNumber: generate, toggleVerified, }; export const useSafetyNumberActions = (): BoundActionCreatorsMapObject< typeof actions > => useBoundActions(actions); export function getEmptyState(): SafetyNumberStateType { return { contacts: {}, }; } export function reducer( state: Readonly = getEmptyState(), action: Readonly ): SafetyNumberStateType { if (action.type === CLEAR_SAFETY_NUMBER) { const { contactId } = action.payload; return { contacts: omit(state.contacts, contactId), }; } 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; }