// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { webFrame } from 'electron'; import type { AudioDevice } from '@signalapp/ringrtc'; import { noop } from 'lodash'; import { getStoriesAvailable } from './stories'; import type { ZoomFactorType } from '../types/Storage.d'; import type { ConversationColorType, CustomColorType, DefaultConversationColorType, } from '../types/Colors'; import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors'; import * as Errors from '../types/errors'; import * as Stickers from '../types/Stickers'; import type { SystemTraySetting } from '../types/SystemTraySetting'; import { parseSystemTraySetting } from '../types/SystemTraySetting'; import type { ConversationType } from '../state/ducks/conversations'; import type { AuthorizeArtCreatorDataType } from '../state/ducks/globalModals'; import { calling } from '../services/calling'; import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations'; import { getCustomColors } from '../state/selectors/items'; import { themeChanged } from '../shims/themeChanged'; import { renderClearingDataView } from '../shims/renderClearingDataView'; import * as universalExpireTimer from './universalExpireTimer'; import { PhoneNumberDiscoverability } from './phoneNumberDiscoverability'; import { PhoneNumberSharingMode } from './phoneNumberSharingMode'; import { strictAssert, assertDev } from './assert'; import * as durations from './durations'; import type { DurationInSeconds } from './durations'; import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled'; import * as Registration from './registration'; import { parseE164FromSignalDotMeHash, parseUsernameFromSignalDotMeHash, } from './sgnlHref'; import { lookupConversationWithoutUuid } from './lookupConversationWithoutUuid'; import * as log from '../logging/log'; import { deleteAllMyStories } from './deleteAllMyStories'; type SentMediaQualityType = 'standard' | 'high'; type ThemeType = 'light' | 'dark' | 'system'; type NotificationSettingType = 'message' | 'name' | 'count' | 'off'; export type IPCEventsValuesType = { alwaysRelayCalls: boolean | undefined; audioNotification: boolean | undefined; autoDownloadUpdate: boolean; autoLaunch: boolean; callRingtoneNotification: boolean; callSystemNotification: boolean; countMutedConversations: boolean; hasStoriesDisabled: boolean; hideMenuBar: boolean | undefined; incomingCallNotification: boolean; lastSyncTime: number | undefined; notificationDrawAttention: boolean; notificationSetting: NotificationSettingType; preferredAudioInputDevice: AudioDevice | undefined; preferredAudioOutputDevice: AudioDevice | undefined; preferredVideoInputDevice: string | undefined; sentMediaQualitySetting: SentMediaQualityType; spellCheck: boolean; systemTraySetting: SystemTraySetting; themeSetting: ThemeType; universalExpireTimer: DurationInSeconds; zoomFactor: ZoomFactorType; storyViewReceiptsEnabled: boolean; // Optional mediaPermissions: boolean; mediaCameraPermissions: boolean; // Only getters blockedCount: number; linkPreviewSetting: boolean; phoneNumberDiscoverabilitySetting: PhoneNumberDiscoverability; phoneNumberSharingSetting: PhoneNumberSharingMode; readReceiptSetting: boolean; typingIndicatorSetting: boolean; deviceName: string | undefined; }; export type IPCEventsCallbacksType = { openArtCreator(): Promise; getAvailableIODevices(): Promise<{ availableCameras: Array< Pick >; availableMicrophones: Array; availableSpeakers: Array; }>; addCustomColor: (customColor: CustomColorType) => void; addDarkOverlay: () => void; authorizeArtCreator: (data: AuthorizeArtCreatorDataType) => void; deleteAllData: () => Promise; deleteAllMyStories: () => Promise; closeDB: () => Promise; editCustomColor: (colorId: string, customColor: CustomColorType) => void; getConversationsWithCustomColor: (x: string) => Array; installStickerPack: (packId: string, key: string) => Promise; isPhoneNumberSharingEnabled: () => boolean; isPrimary: () => boolean; removeCustomColor: (x: string) => void; removeCustomColorOnConversations: (x: string) => void; removeDarkOverlay: () => void; resetAllChatColors: () => void; resetDefaultChatColor: () => void; showConversationViaSignalDotMe: (hash: string) => Promise; showKeyboardShortcuts: () => void; showGroupViaLink: (x: string) => Promise; showReleaseNotes: () => void; showStickerPack: (packId: string, key: string) => void; shutdown: () => Promise; unknownSignalLink: () => void; getCustomColors: () => Record; syncRequest: () => Promise; setGlobalDefaultConversationColor: ( color: ConversationColorType, customColor?: { id: string; value: CustomColorType } ) => void; shouldShowStoriesSettings: () => boolean; getDefaultConversationColor: () => DefaultConversationColorType; persistZoomFactor: (factor: number) => Promise; }; type ValuesWithGetters = Omit< IPCEventsValuesType, // Optional 'mediaPermissions' | 'mediaCameraPermissions' | 'autoLaunch' >; type ValuesWithSetters = Omit< IPCEventsValuesType, | 'blockedCount' | 'defaultConversationColor' | 'linkPreviewSetting' | 'readReceiptSetting' | 'typingIndicatorSetting' | 'deviceName' // Optional | 'mediaPermissions' | 'mediaCameraPermissions' >; export type IPCEventGetterType = `get${Capitalize}`; export type IPCEventSetterType = `set${Capitalize}`; export type IPCEventsGettersType = { [Key in keyof ValuesWithGetters as IPCEventGetterType]: () => ValuesWithGetters[Key]; } & { getMediaPermissions?: () => Promise; getMediaCameraPermissions?: () => Promise; getAutoLaunch?: () => Promise; }; export type IPCEventsSettersType = { [Key in keyof ValuesWithSetters as IPCEventSetterType]: ( value: NonNullable ) => Promise; } & { setMediaPermissions?: (value: boolean) => Promise; setMediaCameraPermissions?: (value: boolean) => Promise; }; export type IPCEventsType = IPCEventsGettersType & IPCEventsSettersType & IPCEventsCallbacksType; export function createIPCEvents( overrideEvents: Partial = {} ): IPCEventsType { const setPhoneNumberDiscoverabilitySetting = async ( newValue: PhoneNumberDiscoverability ): Promise => { strictAssert(window.textsecure.server, 'WebAPI must be available'); await window.storage.put('phoneNumberDiscoverability', newValue); await window.textsecure.server.setPhoneNumberDiscoverability( newValue === PhoneNumberDiscoverability.Discoverable ); const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('phoneNumberDiscoverability'); }; return { openArtCreator: async () => { const auth = await window.textsecure.server?.getArtAuth(); if (!auth) { return; } window.openArtCreator(auth); }, getDeviceName: () => window.textsecure.storage.user.getDeviceName(), getZoomFactor: () => window.storage.get('zoomFactor', 1), setZoomFactor: async (zoomFactor: ZoomFactorType) => { webFrame.setZoomFactor(zoomFactor); }, setPhoneNumberDiscoverabilitySetting, setPhoneNumberSharingSetting: async (newValue: PhoneNumberSharingMode) => { const account = window.ConversationController.getOurConversationOrThrow(); const promises = new Array>(); promises.push(window.storage.put('phoneNumberSharingMode', newValue)); if (newValue === PhoneNumberSharingMode.Everybody) { promises.push( setPhoneNumberDiscoverabilitySetting( PhoneNumberDiscoverability.Discoverable ) ); } account.captureChange('phoneNumberSharingMode'); await Promise.all(promises); }, getHasStoriesDisabled: () => window.storage.get('hasStoriesDisabled', false), setHasStoriesDisabled: async (value: boolean) => { await window.storage.put('hasStoriesDisabled', value); const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('hasStoriesDisabled'); window.textsecure.server?.onHasStoriesDisabledChange(value); }, getStoryViewReceiptsEnabled: () => { return ( window.storage.get('storyViewReceiptsEnabled') ?? window.storage.get('read-receipt-setting') ?? false ); }, setStoryViewReceiptsEnabled: async (value: boolean) => { await window.storage.put('storyViewReceiptsEnabled', value); const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('storyViewReceiptsEnabled'); }, getPreferredAudioInputDevice: () => window.storage.get('preferred-audio-input-device'), setPreferredAudioInputDevice: device => window.storage.put('preferred-audio-input-device', device), getPreferredAudioOutputDevice: () => window.storage.get('preferred-audio-output-device'), setPreferredAudioOutputDevice: device => window.storage.put('preferred-audio-output-device', device), getPreferredVideoInputDevice: () => window.storage.get('preferred-video-input-device'), setPreferredVideoInputDevice: device => window.storage.put('preferred-video-input-device', device), deleteAllMyStories: async () => { await deleteAllMyStories(); }, // Chat Color redux hookups getCustomColors: () => { return getCustomColors(window.reduxStore.getState()) || {}; }, getConversationsWithCustomColor: colorId => { return getConversationsWithCustomColorSelector( window.reduxStore.getState() )(colorId); }, addCustomColor: (...args) => window.reduxActions.items.addCustomColor(...args), editCustomColor: (...args) => window.reduxActions.items.editCustomColor(...args), removeCustomColor: colorId => window.reduxActions.items.removeCustomColor(colorId), removeCustomColorOnConversations: colorId => window.reduxActions.conversations.removeCustomColorOnConversations( colorId ), resetAllChatColors: () => window.reduxActions.conversations.resetAllChatColors(), resetDefaultChatColor: () => window.reduxActions.items.resetDefaultChatColor(), setGlobalDefaultConversationColor: (...args) => window.reduxActions.items.setGlobalDefaultConversationColor(...args), // Getters only getAvailableIODevices: async () => { const { availableCameras, availableMicrophones, availableSpeakers } = await calling.getAvailableIODevices(); return { // mapping it to a pojo so that it is IPC friendly availableCameras: availableCameras.map( (inputDeviceInfo: MediaDeviceInfo) => ({ deviceId: inputDeviceInfo.deviceId, groupId: inputDeviceInfo.groupId, kind: inputDeviceInfo.kind, label: inputDeviceInfo.label, }) ), availableMicrophones, availableSpeakers, }; }, getBlockedCount: () => window.storage.blocked.getBlockedUuids().length + window.storage.blocked.getBlockedGroups().length, getDefaultConversationColor: () => window.storage.get( 'defaultConversationColor', DEFAULT_CONVERSATION_COLOR ), getLinkPreviewSetting: () => window.storage.get('linkPreviews', false), getPhoneNumberDiscoverabilitySetting: () => window.storage.get( 'phoneNumberDiscoverability', PhoneNumberDiscoverability.NotDiscoverable ), getPhoneNumberSharingSetting: () => window.storage.get( 'phoneNumberSharingMode', PhoneNumberSharingMode.Nobody ), getReadReceiptSetting: () => window.storage.get('read-receipt-setting', false), getTypingIndicatorSetting: () => window.storage.get('typingIndicators', false), // Configurable settings getAutoDownloadUpdate: () => window.storage.get('auto-download-update', true), setAutoDownloadUpdate: value => window.storage.put('auto-download-update', value), getSentMediaQualitySetting: () => window.storage.get('sent-media-quality', 'standard'), setSentMediaQualitySetting: value => window.storage.put('sent-media-quality', value), getThemeSetting: () => window.storage.get('theme-setting', 'system'), setThemeSetting: value => { const promise = window.storage.put('theme-setting', value); themeChanged(); return promise; }, getHideMenuBar: () => window.storage.get('hide-menu-bar'), setHideMenuBar: value => { const promise = window.storage.put('hide-menu-bar', value); window.IPC.setAutoHideMenuBar(value); window.IPC.setMenuBarVisibility(!value); return promise; }, getSystemTraySetting: () => parseSystemTraySetting(window.storage.get('system-tray-setting')), setSystemTraySetting: value => { const promise = window.storage.put('system-tray-setting', value); window.IPC.updateSystemTraySetting(value); return promise; }, getNotificationSetting: () => window.storage.get('notification-setting', 'message'), setNotificationSetting: (value: 'message' | 'name' | 'count' | 'off') => window.storage.put('notification-setting', value), getNotificationDrawAttention: () => window.storage.get('notification-draw-attention', false), setNotificationDrawAttention: value => window.storage.put('notification-draw-attention', value), getAudioNotification: () => window.storage.get('audio-notification'), setAudioNotification: value => window.storage.put('audio-notification', value), getCountMutedConversations: () => window.storage.get('badge-count-muted-conversations', false), setCountMutedConversations: value => { const promise = window.storage.put( 'badge-count-muted-conversations', value ); window.Whisper.events.trigger('updateUnreadCount'); return promise; }, getCallRingtoneNotification: () => window.storage.get('call-ringtone-notification', true), setCallRingtoneNotification: value => window.storage.put('call-ringtone-notification', value), getCallSystemNotification: () => window.storage.get('call-system-notification', true), setCallSystemNotification: value => window.storage.put('call-system-notification', value), getIncomingCallNotification: () => window.storage.get('incoming-call-notification', true), setIncomingCallNotification: value => window.storage.put('incoming-call-notification', value), getSpellCheck: () => window.storage.get('spell-check', true), setSpellCheck: value => window.storage.put('spell-check', value), getAlwaysRelayCalls: () => window.storage.get('always-relay-calls'), setAlwaysRelayCalls: value => window.storage.put('always-relay-calls', value), getAutoLaunch: () => window.IPC.getAutoLaunch(), setAutoLaunch: async (value: boolean) => { return window.IPC.setAutoLaunch(value); }, isPhoneNumberSharingEnabled: () => isPhoneNumberSharingEnabled(), isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1, shouldShowStoriesSettings: () => getStoriesAvailable(), syncRequest: () => new Promise((resolve, reject) => { const FIVE_MINUTES = 5 * durations.MINUTE; const syncRequest = window.getSyncRequest(FIVE_MINUTES); syncRequest.addEventListener('success', () => resolve()); syncRequest.addEventListener('timeout', () => reject(new Error('timeout')) ); }), getLastSyncTime: () => window.storage.get('synced_at'), setLastSyncTime: value => window.storage.put('synced_at', value), getUniversalExpireTimer: () => universalExpireTimer.get(), setUniversalExpireTimer: async newValue => { await universalExpireTimer.set(newValue); // Update account in Storage Service const account = window.ConversationController.getOurConversationOrThrow(); account.captureChange('universalExpireTimer'); // Add a notification to the currently open conversation const state = window.reduxStore.getState(); const selectedId = state.conversations.selectedConversationId; if (selectedId) { const conversation = window.ConversationController.get(selectedId); assertDev(conversation, "Conversation wasn't found"); await conversation.updateLastMessage(); } }, addDarkOverlay: () => { const elems = document.querySelectorAll('.dark-overlay'); if (elems.length) { return; } const newOverlay = document.createElement('div'); newOverlay.className = 'dark-overlay'; newOverlay.addEventListener('click', () => { newOverlay.remove(); }); document.body.prepend(newOverlay); }, authorizeArtCreator: (data: AuthorizeArtCreatorDataType) => { // We can get these events even if the user has never linked this instance. if (!Registration.everDone()) { log.warn('authorizeArtCreator: Not registered, returning early'); return; } window.reduxActions.globalModals.showAuthorizeArtCreator(data); }, removeDarkOverlay: () => { const elems = document.querySelectorAll('.dark-overlay'); for (const elem of elems) { elem.remove(); } }, showKeyboardShortcuts: () => window.reduxActions.globalModals.showShortcutGuideModal(), deleteAllData: async () => { await window.Signal.Data.goBackToMainProcess(); renderClearingDataView(); }, closeDB: async () => { await window.Signal.Data.goBackToMainProcess(); }, showStickerPack: (packId, key) => { // We can get these events even if the user has never linked this instance. if (!Registration.everDone()) { log.warn('showStickerPack: Not registered, returning early'); return; } window.reduxActions.globalModals.showStickerPackPreview(packId, key); }, showGroupViaLink: async hash => { // We can get these events even if the user has never linked this instance. if (!Registration.everDone()) { log.warn('showGroupViaLink: Not registered, returning early'); return; } try { await window.Signal.Groups.joinViaLink(hash); } catch (error) { log.error( 'showGroupViaLink: Ran into an error!', Errors.toLogFormat(error) ); window.reduxActions.globalModals.showErrorModal({ title: window.i18n('icu:GroupV2--join--general-join-failure--title'), description: window.i18n('icu:GroupV2--join--general-join-failure'), }); } }, async showConversationViaSignalDotMe(hash: string) { if (!Registration.everDone()) { log.info( 'showConversationViaSignalDotMe: Not registered, returning early' ); return; } const { showUserNotFoundModal } = window.reduxActions.globalModals; const maybeE164 = parseE164FromSignalDotMeHash(hash); if (maybeE164) { const convoId = await lookupConversationWithoutUuid({ type: 'e164', e164: maybeE164, phoneNumber: maybeE164, showUserNotFoundModal, setIsFetchingUUID: noop, }); if (convoId) { window.reduxActions.conversations.showConversation({ conversationId: convoId, }); return; } // We will show not found modal on error return; } const maybeUsername = parseUsernameFromSignalDotMeHash(hash); if (maybeUsername) { const convoId = await lookupConversationWithoutUuid({ type: 'username', username: maybeUsername, showUserNotFoundModal, setIsFetchingUUID: noop, }); if (convoId) { window.reduxActions.conversations.showConversation({ conversationId: convoId, }); return; } // We will show not found modal on error return; } log.info('showConversationViaSignalDotMe: invalid E164'); showUnknownSgnlLinkModal(); }, unknownSignalLink: () => { log.warn('unknownSignalLink: Showing error dialog'); showUnknownSgnlLinkModal(); }, installStickerPack: async (packId, key) => { void Stickers.downloadStickerPack(packId, key, { finalStatus: 'installed', }); }, shutdown: () => Promise.resolve(), showReleaseNotes: () => { const { showWhatsNewModal } = window.reduxActions.globalModals; showWhatsNewModal(); }, getMediaPermissions: window.IPC.getMediaPermissions, getMediaCameraPermissions: window.IPC.getMediaCameraPermissions, persistZoomFactor: zoomFactor => window.storage.put('zoomFactor', zoomFactor), ...overrideEvents, }; } function showUnknownSgnlLinkModal(): void { window.reduxActions.globalModals.showErrorModal({ description: window.i18n('icu:unknown-sgnl-link'), }); }