diff --git a/.eslintignore b/.eslintignore index 4f01beef6df6..cdc0de02b21f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -30,7 +30,6 @@ webpack.config.ts # Temporarily ignored during TSLint transition # JIRA: DESKTOP-304 -ts/*.ts ts/components/*.ts ts/components/*.tsx ts/components/conversation/** diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index cd27b717f196..c48c3cfb7cf0 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -1,5 +1,3 @@ -const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; - import { debounce, reduce, uniq, without } from 'lodash'; import dataInterface from './sql/Client'; import { @@ -9,6 +7,8 @@ import { } from './model-types.d'; import { SendOptionsType } from './textsecure/SendMessage'; +const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; + const { getAllConversations, getAllGroupsInvolvingId, @@ -22,7 +22,7 @@ const { // We have to run this in background.js, after all backbone models and collections on // Whisper.* have been created. Once those are in typescript we can use more reasonable // require statements for referencing these things, giving us more flexibility here. -export function start() { +export function start(): void { const conversations = new window.Whisper.ConversationCollection(); // This class is entirely designed to keep the app title, badge and tray icon updated. @@ -70,7 +70,9 @@ export function start() { export class ConversationController { _initialFetchComplete: boolean | undefined; + _initialPromise: Promise = Promise.resolve(); + _conversations: ConversationModelCollectionType; constructor(conversations?: ConversationModelCollectionType) { @@ -91,14 +93,18 @@ export class ConversationController { // This function takes null just fine. Backbone typings are too restrictive. return this._conversations.get(id as string); } - dangerouslyCreateAndAdd(attributes: Partial) { + + dangerouslyCreateAndAdd( + attributes: Partial + ): ConversationModelType { return this._conversations.add(attributes); } + getOrCreate( identifier: string, type: ConversationTypeType, additionalInitialProps = {} - ) { + ): ConversationModelType { if (typeof identifier !== 'string') { throw new TypeError("'id' must be a string"); } @@ -186,24 +192,24 @@ export class ConversationController { return conversation; } + async getOrCreateAndWait( id: string, type: ConversationTypeType, additionalInitialProps = {} - ) { - return this._initialPromise.then(async () => { - const conversation = this.getOrCreate(id, type, additionalInitialProps); + ): Promise { + await this._initialPromise; + const conversation = this.getOrCreate(id, type, additionalInitialProps); - if (conversation) { - return conversation.initialPromise.then(() => conversation); - } + if (conversation) { + await conversation.initialPromise; + return conversation; + } - return Promise.reject( - new Error('getOrCreateAndWait: did not get conversation') - ); - }); + throw new Error('getOrCreateAndWait: did not get conversation'); } - getConversationId(address: string) { + + getConversationId(address: string): string | null { if (!address) { return null; } @@ -217,11 +223,13 @@ export class ConversationController { return null; } + getOurConversationId(): string | undefined { const e164 = window.textsecure.storage.user.getNumber(); const uuid = window.textsecure.storage.user.getUuid(); return this.ensureContactIds({ e164, uuid, highTrust: true }); } + /** * Given a UUID and/or an E164, resolves to a string representing the local * database id of the given contact. In high trust mode, it may create new contacts, @@ -272,7 +280,8 @@ export class ConversationController { return newConvo.get('id'); // 2. Handle match on only E164 - } else if (convoE164 && !convoUuid) { + } + if (convoE164 && !convoUuid) { const haveUuid = Boolean(normalizedUuid); window.log.info( `ensureContactIds: e164-only match found (have UUID: ${haveUuid})` @@ -315,7 +324,8 @@ export class ConversationController { return newConvo.get('id'); // 3. Handle match on only UUID - } else if (!convoE164 && convoUuid) { + } + if (!convoE164 && convoUuid) { window.log.info( `ensureContactIds: UUID-only match found (have e164: ${Boolean(e164)})` ); @@ -363,6 +373,8 @@ export class ConversationController { // Conflict: If e164 match has no UUID, we merge. We prefer the UUID match. // Note: no await here, we want to keep this function synchronous convoUuid.updateE164(e164); + // `then` is used to trigger async updates, not affecting return value + // eslint-disable-next-line more/no-then this.combineContacts(convoUuid, convoE164) .then(() => { // If the old conversation was currently displayed, we load the new one @@ -381,7 +393,8 @@ export class ConversationController { return convoUuid.get('id'); } - async checkForConflicts() { + + async checkForConflicts(): Promise { window.log.info('checkForConflicts: starting...'); const byUuid = Object.create(null); const byE164 = Object.create(null); @@ -465,10 +478,11 @@ export class ConversationController { window.log.info('checkForConflicts: complete!'); } + async combineContacts( current: ConversationModelType, obsolete: ConversationModelType - ) { + ): Promise { const obsoleteId = obsolete.get('id'); const currentId = current.get('id'); window.log.warn('combineContacts: Combining two conversations', { @@ -541,6 +555,7 @@ export class ConversationController { current: currentId, }); } + /** * Given a groupId and optional additional initialization properties, * ensures the existence of a group conversation and returns a string @@ -549,16 +564,17 @@ export class ConversationController { ensureGroup(groupId: string, additionalInitProps = {}): string { return this.getOrCreate(groupId, 'group', additionalInitProps).get('id'); } + /** * Given certain metadata about a message (an identifier of who wrote the * message and the sent_at timestamp of the message) returns the * conversation the message belongs to OR null if a conversation isn't * found. */ - async getConversationForTargetMessage( + static async getConversationForTargetMessage( targetFromId: string, targetTimestamp: number - ) { + ): Promise { const messages = await getMessagesBySentAt(targetTimestamp, { MessageCollection: window.Whisper.MessageCollection, }); @@ -579,11 +595,12 @@ export class ConversationController { return null; } - prepareForSend( + + prepareForSend( id: string, - options?: any + options?: unknown ): { - wrap: (promise: Promise) => Promise; + wrap: (promise: Promise) => Promise; sendOptions: SendOptionsType | undefined; } { // id is any valid conversation identifier @@ -593,10 +610,11 @@ export class ConversationController { : undefined; const wrap = conversation ? conversation.wrapSend.bind(conversation) - : async (promise: Promise) => promise; + : async (promise: Promise) => promise; return { wrap, sendOptions }; } + async getAllGroupsInvolvingId( conversationId: string ): Promise> { @@ -605,18 +623,22 @@ export class ConversationController { }); return groups.map(group => this._conversations.add(group)); } - async loadPromise() { + + async loadPromise(): Promise { return this._initialPromise; } - reset() { + + reset(): void { this._initialPromise = Promise.resolve(); this._initialFetchComplete = false; this._conversations.reset([]); } - isFetchComplete() { + + isFetchComplete(): boolean | undefined { return this._initialFetchComplete; } - async load() { + + async load(): Promise { window.log.info('ConversationController: starting initial fetch'); if (this._conversations.length) { diff --git a/ts/Crypto.ts b/ts/Crypto.ts index ce4578ad8f50..968a19849b51 100644 --- a/ts/Crypto.ts +++ b/ts/Crypto.ts @@ -1,55 +1,47 @@ import pProps from 'p-props'; -// Yep, we're doing some bitwise stuff in an encryption-related file -// tslint:disable no-bitwise - -// We want some extra variables to make the decrption algorithm easier to understand -// tslint:disable no-unnecessary-local-variable - -// Seems that tslint doesn't understand that crypto.subtle.importKey does return a Promise -// tslint:disable await-promise - export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer { const { buffer, byteOffset, byteLength } = typedArray; - // tslint:disable-next-line no-unnecessary-type-assertion return buffer.slice(byteOffset, byteLength + byteOffset) as typeof typedArray; } -export function arrayBufferToBase64(arrayBuffer: ArrayBuffer) { +export function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string { return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64'); } -export function arrayBufferToHex(arrayBuffer: ArrayBuffer) { +export function arrayBufferToHex(arrayBuffer: ArrayBuffer): string { return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('hex'); } -export function base64ToArrayBuffer(base64string: string) { +export function base64ToArrayBuffer(base64string: string): ArrayBuffer { return window.dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer(); } -export function hexToArrayBuffer(hexString: string) { +export function hexToArrayBuffer(hexString: string): ArrayBuffer { return window.dcodeIO.ByteBuffer.wrap(hexString, 'hex').toArrayBuffer(); } -export function fromEncodedBinaryToArrayBuffer(key: string) { +export function fromEncodedBinaryToArrayBuffer(key: string): ArrayBuffer { return window.dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer(); } -export function bytesFromString(string: string) { +export function bytesFromString(string: string): ArrayBuffer { return window.dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer(); } -export function stringFromBytes(buffer: ArrayBuffer) { +export function stringFromBytes(buffer: ArrayBuffer): string { return window.dcodeIO.ByteBuffer.wrap(buffer).toString('utf8'); } -export function hexFromBytes(buffer: ArrayBuffer) { +export function hexFromBytes(buffer: ArrayBuffer): string { return window.dcodeIO.ByteBuffer.wrap(buffer).toString('hex'); } -export function bytesFromHexString(string: string) { +export function bytesFromHexString(string: string): ArrayBuffer { return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer(); } -export async function deriveStickerPackKey(packKey: ArrayBuffer) { +export async function deriveStickerPackKey( + packKey: ArrayBuffer +): Promise { const salt = getZeroes(32); const info = bytesFromString('Sticker Pack'); @@ -72,7 +64,7 @@ export async function computeHash(data: ArrayBuffer): Promise { export async function encryptDeviceName( deviceName: string, identityPublic: ArrayBuffer -) { +): Promise> { const plaintext = bytesFromString(deviceName); const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair(); const masterSecret = await window.libsignal.Curve.async.calculateAgreement( @@ -107,7 +99,7 @@ export async function decryptDeviceName( ciphertext: ArrayBuffer; }, identityPrivate: ArrayBuffer -) { +): Promise { const masterSecret = await window.libsignal.Curve.async.calculateAgreement( ephemeralPublic, identityPrivate @@ -130,7 +122,7 @@ export async function decryptDeviceName( } // Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa' -export function getAttachmentLabel(path: string) { +export function getAttachmentLabel(path: string): ArrayBuffer { const filename = path.slice(3); return base64ToArrayBuffer(filename); @@ -141,7 +133,7 @@ export async function encryptAttachment( staticPublicKey: ArrayBuffer, path: string, plaintext: ArrayBuffer -) { +): Promise { const uniqueId = getAttachmentLabel(path); return encryptFile(staticPublicKey, uniqueId, plaintext); @@ -151,7 +143,7 @@ export async function decryptAttachment( staticPrivateKey: ArrayBuffer, path: string, data: ArrayBuffer -) { +): Promise { const uniqueId = getAttachmentLabel(path); return decryptFile(staticPrivateKey, uniqueId, data); @@ -161,7 +153,7 @@ export async function encryptFile( staticPublicKey: ArrayBuffer, uniqueId: ArrayBuffer, plaintext: ArrayBuffer -) { +): Promise { const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair(); const agreement = await window.libsignal.Curve.async.calculateAgreement( staticPublicKey, @@ -178,7 +170,7 @@ export async function decryptFile( staticPrivateKey: ArrayBuffer, uniqueId: ArrayBuffer, data: ArrayBuffer -) { +): Promise { const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH); const ciphertext = getBytes(data, PUB_KEY_LENGTH, data.byteLength); const agreement = await window.libsignal.Curve.async.calculateAgreement( @@ -194,18 +186,20 @@ export async function decryptFile( export async function deriveStorageManifestKey( storageServiceKey: ArrayBuffer, version: number -) { +): Promise { return hmacSha256(storageServiceKey, bytesFromString(`Manifest_${version}`)); } export async function deriveStorageItemKey( storageServiceKey: ArrayBuffer, itemID: string -) { +): Promise { return hmacSha256(storageServiceKey, bytesFromString(`Item_${itemID}`)); } -export async function deriveAccessKey(profileKey: ArrayBuffer) { +export async function deriveAccessKey( + profileKey: ArrayBuffer +): Promise { const iv = getZeroes(12); const plaintext = getZeroes(16); const accessKey = await encryptAesGcm(profileKey, iv, plaintext); @@ -213,7 +207,9 @@ export async function deriveAccessKey(profileKey: ArrayBuffer) { return getFirstBytes(accessKey, 16); } -export async function getAccessKeyVerifier(accessKey: ArrayBuffer) { +export async function getAccessKeyVerifier( + accessKey: ArrayBuffer +): Promise { const plaintext = getZeroes(32); return hmacSha256(accessKey, plaintext); @@ -222,7 +218,7 @@ export async function getAccessKeyVerifier(accessKey: ArrayBuffer) { export async function verifyAccessKey( accessKey: ArrayBuffer, theirVerifier: ArrayBuffer -) { +): Promise { const ourVerifier = await getAccessKeyVerifier(accessKey); if (constantTimeEqual(ourVerifier, theirVerifier)) { @@ -239,14 +235,14 @@ const NONCE_LENGTH = 16; export async function encryptSymmetric( key: ArrayBuffer, plaintext: ArrayBuffer -) { +): Promise { const iv = getZeroes(IV_LENGTH); const nonce = getRandomBytes(NONCE_LENGTH); const cipherKey = await hmacSha256(key, nonce); const macKey = await hmacSha256(key, cipherKey); - const cipherText = await _encrypt_aes256_CBC_PKCSPadding( + const cipherText = await _encryptAes256CbcPkcsPadding( cipherKey, iv, plaintext @@ -256,7 +252,10 @@ export async function encryptSymmetric( return concatenateBytes(nonce, cipherText, mac); } -export async function decryptSymmetric(key: ArrayBuffer, data: ArrayBuffer) { +export async function decryptSymmetric( + key: ArrayBuffer, + data: ArrayBuffer +): Promise { const iv = getZeroes(IV_LENGTH); const nonce = getFirstBytes(data, NONCE_LENGTH); @@ -280,10 +279,13 @@ export async function decryptSymmetric(key: ArrayBuffer, data: ArrayBuffer) { ); } - return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText); + return _decryptAes256CbcPkcsPadding(cipherKey, iv, cipherText); } -export function constantTimeEqual(left: ArrayBuffer, right: ArrayBuffer) { +export function constantTimeEqual( + left: ArrayBuffer, + right: ArrayBuffer +): boolean { if (left.byteLength !== right.byteLength) { return false; } @@ -301,8 +303,11 @@ export function constantTimeEqual(left: ArrayBuffer, right: ArrayBuffer) { // Encryption -export async function hmacSha256(key: ArrayBuffer, plaintext: ArrayBuffer) { - const algorithm = { +export async function hmacSha256( + key: ArrayBuffer, + plaintext: ArrayBuffer +): Promise { + const algorithm: HmacImportParams = { name: 'HMAC', hash: 'SHA-256', }; @@ -311,7 +316,7 @@ export async function hmacSha256(key: ArrayBuffer, plaintext: ArrayBuffer) { const cryptoKey = await window.crypto.subtle.importKey( 'raw', key, - algorithm as any, + algorithm, extractable, ['sign'] ); @@ -319,11 +324,11 @@ export async function hmacSha256(key: ArrayBuffer, plaintext: ArrayBuffer) { return window.crypto.subtle.sign(algorithm, cryptoKey, plaintext); } -export async function _encrypt_aes256_CBC_PKCSPadding( +export async function _encryptAes256CbcPkcsPadding( key: ArrayBuffer, iv: ArrayBuffer, plaintext: ArrayBuffer -) { +): Promise { const algorithm = { name: 'AES-CBC', iv, @@ -333,6 +338,10 @@ export async function _encrypt_aes256_CBC_PKCSPadding( const cryptoKey = await window.crypto.subtle.importKey( 'raw', key, + // `algorithm` appears to be an instance of AesCbcParams, + // which is not in the param's types, so we need to pass as `any`. + // TODO: just pass the string "AES-CBC", per the docs? + // eslint-disable-next-line @typescript-eslint/no-explicit-any algorithm as any, extractable, ['encrypt'] @@ -341,11 +350,11 @@ export async function _encrypt_aes256_CBC_PKCSPadding( return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext); } -export async function _decrypt_aes256_CBC_PKCSPadding( +export async function _decryptAes256CbcPkcsPadding( key: ArrayBuffer, iv: ArrayBuffer, plaintext: ArrayBuffer -) { +): Promise { const algorithm = { name: 'AES-CBC', iv, @@ -355,6 +364,10 @@ export async function _decrypt_aes256_CBC_PKCSPadding( const cryptoKey = await window.crypto.subtle.importKey( 'raw', key, + // `algorithm` appears to be an instance of AesCbcParams, + // which is not in the param's types, so we need to pass as `any`. + // TODO: just pass the string "AES-CBC", per the docs? + // eslint-disable-next-line @typescript-eslint/no-explicit-any algorithm as any, extractable, ['decrypt'] @@ -367,7 +380,7 @@ export async function encryptAesCtr( key: ArrayBuffer, plaintext: ArrayBuffer, counter: ArrayBuffer -) { +): Promise { const extractable = false; const algorithm = { name: 'AES-CTR', @@ -378,6 +391,10 @@ export async function encryptAesCtr( const cryptoKey = await crypto.subtle.importKey( 'raw', key, + // `algorithm` appears to be an instance of AesCtrParams, + // which is not in the param's types, so we need to pass as `any`. + // TODO: just pass the string "AES-CTR", per the docs? + // eslint-disable-next-line @typescript-eslint/no-explicit-any algorithm as any, extractable, ['encrypt'] @@ -396,7 +413,7 @@ export async function decryptAesCtr( key: ArrayBuffer, ciphertext: ArrayBuffer, counter: ArrayBuffer -) { +): Promise { const extractable = false; const algorithm = { name: 'AES-CTR', @@ -407,6 +424,10 @@ export async function decryptAesCtr( const cryptoKey = await crypto.subtle.importKey( 'raw', key, + // `algorithm` appears to be an instance of AesCtrParams, + // which is not in the param's types, so we need to pass as `any`. + // TODO: just pass the string "AES-CTR", per the docs? + // eslint-disable-next-line @typescript-eslint/no-explicit-any algorithm as any, extractable, ['decrypt'] @@ -425,7 +446,7 @@ export async function encryptAesGcm( iv: ArrayBuffer, plaintext: ArrayBuffer, additionalData?: ArrayBuffer -) { +): Promise { const algorithm = { name: 'AES-GCM', iv, @@ -437,6 +458,10 @@ export async function encryptAesGcm( const cryptoKey = await crypto.subtle.importKey( 'raw', key, + // `algorithm` appears to be an instance of AesGcmParams, + // which is not in the param's types, so we need to pass as `any`. + // TODO: just pass the string "AES-GCM", per the docs? + // eslint-disable-next-line @typescript-eslint/no-explicit-any algorithm as any, extractable, ['encrypt'] @@ -450,7 +475,7 @@ export async function decryptAesGcm( iv: ArrayBuffer, ciphertext: ArrayBuffer, additionalData?: ArrayBuffer -) { +): Promise { const algorithm = { name: 'AES-GCM', iv, @@ -462,6 +487,10 @@ export async function decryptAesGcm( const cryptoKey = await crypto.subtle.importKey( 'raw', key, + // `algorithm` appears to be an instance of AesGcmParams, + // which is not in the param's types, so we need to pass as `any`. + // TODO: just pass the string "AES-GCM", per the docs? + // eslint-disable-next-line @typescript-eslint/no-explicit-any algorithm as any, extractable, ['decrypt'] @@ -472,13 +501,13 @@ export async function decryptAesGcm( // Hashing -export async function sha256(data: ArrayBuffer) { +export async function sha256(data: ArrayBuffer): Promise { return crypto.subtle.digest('SHA-256', data); } // Utility -export function getRandomBytes(n: number) { +export function getRandomBytes(n: number): ArrayBuffer { const bytes = new Uint8Array(n); window.crypto.getRandomValues(bytes); @@ -496,7 +525,7 @@ export function getRandomValue(low: number, high: number): number { return (bytes[0] % mod) + low; } -export function getZeroes(n: number) { +export function getZeroes(n: number): ArrayBuffer { const result = new Uint8Array(n); const value = 0; @@ -508,6 +537,7 @@ export function getZeroes(n: number) { } export function highBitsToInt(byte: number): number { + // eslint-disable-next-line no-bitwise return (byte & 0xff) >> 4; } @@ -515,10 +545,11 @@ export function intsToByteHighAndLow( highValue: number, lowValue: number ): number { + // eslint-disable-next-line no-bitwise return ((highValue << 4) | lowValue) & 0xff; } -export function trimBytes(buffer: ArrayBuffer, length: number) { +export function trimBytes(buffer: ArrayBuffer, length: number): ArrayBuffer { return getFirstBytes(buffer, length); } @@ -526,14 +557,16 @@ export function getViewOfArrayBuffer( buffer: ArrayBuffer, start: number, finish: number -) { +): ArrayBuffer | SharedArrayBuffer { const source = new Uint8Array(buffer); const result = source.slice(start, finish); return result.buffer; } -export function concatenateBytes(...elements: Array) { +export function concatenateBytes( + ...elements: Array +): ArrayBuffer { const length = elements.reduce( (total, element) => total + element.byteLength, 0 @@ -585,7 +618,7 @@ export function splitBytes( return results; } -export function getFirstBytes(data: ArrayBuffer, n: number) { +export function getFirstBytes(data: ArrayBuffer, n: number): ArrayBuffer { const source = new Uint8Array(data); return typedArrayToArrayBuffer(source.subarray(0, n)); @@ -595,7 +628,7 @@ export function getBytes( data: ArrayBuffer | Uint8Array, start: number, n: number -) { +): ArrayBuffer { const source = new Uint8Array(data); return typedArrayToArrayBuffer(source.subarray(start, start + n)); @@ -614,7 +647,7 @@ export async function encryptCdsDiscoveryRequest( [key: string]: { clientKey: ArrayBuffer; requestId: ArrayBuffer }; }, phoneNumbers: ReadonlyArray -) { +): Promise> { const nonce = getRandomBytes(32); const numbersArray = new window.dcodeIO.ByteBuffer( phoneNumbers.length * 8, @@ -669,7 +702,7 @@ export async function encryptCdsDiscoveryRequest( }; } -export function splitUuids(arrayBuffer: ArrayBuffer) { +export function splitUuids(arrayBuffer: ArrayBuffer): Array { const uuids = []; for (let i = 0; i < arrayBuffer.byteLength; i += 16) { const bytes = getBytes(arrayBuffer, i, 16); diff --git a/ts/OS.ts b/ts/OS.ts index e6a215f23349..d9c1d4cd868d 100644 --- a/ts/OS.ts +++ b/ts/OS.ts @@ -2,9 +2,9 @@ import is from '@sindresorhus/is'; import os from 'os'; import semver from 'semver'; -export const isMacOS = () => process.platform === 'darwin'; -export const isLinux = () => process.platform === 'linux'; -export const isWindows = (minVersion?: string) => { +export const isMacOS = (): boolean => process.platform === 'darwin'; +export const isLinux = (): boolean => process.platform === 'linux'; +export const isWindows = (minVersion?: string): boolean => { const osRelease = os.release(); if (process.platform !== 'win32') { diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 12fa4597f2ce..595a602bb7b5 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -34,12 +34,15 @@ function getServer(): WebAPIType { let config: ConfigMapType = {}; const listeners: ConfigListenersMapType = {}; -export async function initRemoteConfig() { +export async function initRemoteConfig(): Promise { config = window.storage.get('remoteConfig') || {}; await maybeRefreshRemoteConfig(); } -export function onChange(key: ConfigKeyType, fn: ConfigListenerType) { +export function onChange( + key: ConfigKeyType, + fn: ConfigListenerType +): () => void { const keyListeners: Array = get(listeners, key, []); keyListeners.push(fn); listeners[key] = keyListeners; @@ -49,7 +52,7 @@ export function onChange(key: ConfigKeyType, fn: ConfigListenerType) { }; } -export const refreshRemoteConfig = async () => { +export const refreshRemoteConfig = async (): Promise => { const now = Date.now(); const server = getServer(); const newConfig = await server.getConfig(); @@ -61,7 +64,8 @@ export const refreshRemoteConfig = async () => { config = newConfig.reduce((acc, { name, enabled, value }) => { const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false); const previousValue: unknown = get(oldConfig, [name, 'value'], undefined); - // If a flag was previously not enabled and is now enabled, record the time it was enabled + // If a flag was previously not enabled and is now enabled, + // record the time it was enabled const enabledAt: number | undefined = previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']); diff --git a/ts/groupChange.ts b/ts/groupChange.ts index 606169e87e9f..e009360c6821 100644 --- a/ts/groupChange.ts +++ b/ts/groupChange.ts @@ -26,7 +26,7 @@ export type RenderOptionsType = { export function renderChange( change: GroupV2ChangeType, options: RenderOptionsType -) { +): Array { const { details, from } = change; return details.map((detail: GroupV2ChangeDetailType) => @@ -37,7 +37,6 @@ export function renderChange( ); } -// tslint:disable-next-line cyclomatic-complexity max-func-body-length export function renderChangeDetail( detail: GroupV2ChangeDetailType, options: RenderOptionsType @@ -59,108 +58,103 @@ export function renderChangeDetail( if (newTitle) { if (fromYou) { return renderString('GroupV2--title--change--you', i18n, [newTitle]); - } else if (from) { + } + if (from) { return renderString('GroupV2--title--change--other', i18n, { memberName: renderContact(from), newTitle, }); - } else { - return renderString('GroupV2--title--change--unknown', i18n, [ - newTitle, - ]); - } - } else { - if (fromYou) { - return renderString('GroupV2--title--remove--you', i18n); - } else if (from) { - return renderString('GroupV2--title--remove--other', i18n, [ - renderContact(from), - ]); - } else { - return renderString('GroupV2--title--remove--unknown', i18n); } + return renderString('GroupV2--title--change--unknown', i18n, [newTitle]); } - } else if (detail.type === 'avatar') { + if (fromYou) { + return renderString('GroupV2--title--remove--you', i18n); + } + if (from) { + return renderString('GroupV2--title--remove--other', i18n, [ + renderContact(from), + ]); + } + return renderString('GroupV2--title--remove--unknown', i18n); + } + if (detail.type === 'avatar') { if (detail.removed) { if (fromYou) { return renderString('GroupV2--avatar--remove--you', i18n); - } else if (from) { + } + if (from) { return renderString('GroupV2--avatar--remove--other', i18n, [ renderContact(from), ]); - } else { - return renderString('GroupV2--avatar--remove--unknown', i18n); - } - } else { - if (fromYou) { - return renderString('GroupV2--avatar--change--you', i18n); - } else if (from) { - return renderString('GroupV2--avatar--change--other', i18n, [ - renderContact(from), - ]); - } else { - return renderString('GroupV2--avatar--change--unknown', i18n); } + return renderString('GroupV2--avatar--remove--unknown', i18n); } - } else if (detail.type === 'access-attributes') { + if (fromYou) { + return renderString('GroupV2--avatar--change--you', i18n); + } + if (from) { + return renderString('GroupV2--avatar--change--other', i18n, [ + renderContact(from), + ]); + } + return renderString('GroupV2--avatar--change--unknown', i18n); + } + if (detail.type === 'access-attributes') { const { newPrivilege } = detail; if (newPrivilege === AccessControlEnum.ADMINISTRATOR) { if (fromYou) { return renderString('GroupV2--access-attributes--admins--you', i18n); - } else if (from) { + } + if (from) { return renderString('GroupV2--access-attributes--admins--other', i18n, [ renderContact(from), ]); - } else { - return renderString( - 'GroupV2--access-attributes--admins--unknown', - i18n - ); } - } else if (newPrivilege === AccessControlEnum.MEMBER) { + return renderString('GroupV2--access-attributes--admins--unknown', i18n); + } + if (newPrivilege === AccessControlEnum.MEMBER) { if (fromYou) { return renderString('GroupV2--access-attributes--all--you', i18n); - } else if (from) { + } + if (from) { return renderString('GroupV2--access-attributes--all--other', i18n, [ renderContact(from), ]); - } else { - return renderString('GroupV2--access-attributes--all--unknown', i18n); } - } else { - throw new Error( - `access-attributes change type, privilege ${newPrivilege} is unknown` - ); + return renderString('GroupV2--access-attributes--all--unknown', i18n); } + throw new Error( + `access-attributes change type, privilege ${newPrivilege} is unknown` + ); } else if (detail.type === 'access-members') { const { newPrivilege } = detail; if (newPrivilege === AccessControlEnum.ADMINISTRATOR) { if (fromYou) { return renderString('GroupV2--access-members--admins--you', i18n); - } else if (from) { + } + if (from) { return renderString('GroupV2--access-members--admins--other', i18n, [ renderContact(from), ]); - } else { - return renderString('GroupV2--access-members--admins--unknown', i18n); } - } else if (newPrivilege === AccessControlEnum.MEMBER) { + return renderString('GroupV2--access-members--admins--unknown', i18n); + } + if (newPrivilege === AccessControlEnum.MEMBER) { if (fromYou) { return renderString('GroupV2--access-members--all--you', i18n); - } else if (from) { + } + if (from) { return renderString('GroupV2--access-members--all--other', i18n, [ renderContact(from), ]); - } else { - return renderString('GroupV2--access-members--all--unknown', i18n); } - } else { - throw new Error( - `access-members change type, privilege ${newPrivilege} is unknown` - ); + return renderString('GroupV2--access-members--all--unknown', i18n); } + throw new Error( + `access-members change type, privilege ${newPrivilege} is unknown` + ); } else if (detail.type === 'member-add') { const { conversationId } = detail; const weAreJoiner = conversationId === ourConversationId; @@ -168,29 +162,28 @@ export function renderChangeDetail( if (weAreJoiner) { if (fromYou) { return renderString('GroupV2--member-add--you--you', i18n); - } else if (from) { + } + if (from) { return renderString('GroupV2--member-add--you--other', i18n, [ renderContact(from), ]); - } else { - return renderString('GroupV2--member-add--you--unknown', i18n); - } - } else { - if (fromYou) { - return renderString('GroupV2--member-add--other--you', i18n, [ - renderContact(conversationId), - ]); - } else if (from) { - return renderString('GroupV2--member-add--other--other', i18n, { - adderName: renderContact(from), - addeeName: renderContact(conversationId), - }); - } else { - return renderString('GroupV2--member-add--other--unknown', i18n, [ - renderContact(conversationId), - ]); } + return renderString('GroupV2--member-add--you--unknown', i18n); } + if (fromYou) { + return renderString('GroupV2--member-add--other--you', i18n, [ + renderContact(conversationId), + ]); + } + if (from) { + return renderString('GroupV2--member-add--other--other', i18n, { + adderName: renderContact(from), + addeeName: renderContact(conversationId), + }); + } + return renderString('GroupV2--member-add--other--unknown', i18n, [ + renderContact(conversationId), + ]); } else if (detail.type === 'member-add-from-invite') { const { conversationId, inviter } = detail; const weAreJoiner = conversationId === ourConversationId; @@ -200,16 +193,16 @@ export function renderChangeDetail( return renderString('GroupV2--member-add--from-invite--you', i18n, [ renderContact(inviter), ]); - } else if (weAreInviter) { + } + if (weAreInviter) { return renderString('GroupV2--member-add--from-invite--from-you', i18n, [ renderContact(conversationId), ]); - } else { - return renderString('GroupV2--member-add--from-invite--other', i18n, { - inviteeName: renderContact(conversationId), - inviterName: renderContact(inviter), - }); } + return renderString('GroupV2--member-add--from-invite--other', i18n, { + inviteeName: renderContact(conversationId), + inviterName: renderContact(inviter), + }); } else if (detail.type === 'member-remove') { const { conversationId } = detail; const weAreLeaver = conversationId === ourConversationId; @@ -217,33 +210,34 @@ export function renderChangeDetail( if (weAreLeaver) { if (fromYou) { return renderString('GroupV2--member-remove--you--you', i18n); - } else if (from) { + } + if (from) { return renderString('GroupV2--member-remove--you--other', i18n, [ renderContact(from), ]); - } else { - return renderString('GroupV2--member-remove--you--unknown', i18n); - } - } else { - if (fromYou) { - return renderString('GroupV2--member-remove--other--you', i18n, [ - renderContact(conversationId), - ]); - } else if (from && from === conversationId) { - return renderString('GroupV2--member-remove--other--self', i18n, [ - renderContact(from), - ]); - } else if (from) { - return renderString('GroupV2--member-remove--other--other', i18n, { - adminName: renderContact(from), - memberName: renderContact(conversationId), - }); - } else { - return renderString('GroupV2--member-remove--other--unknown', i18n, [ - renderContact(conversationId), - ]); } + return renderString('GroupV2--member-remove--you--unknown', i18n); } + + if (fromYou) { + return renderString('GroupV2--member-remove--other--you', i18n, [ + renderContact(conversationId), + ]); + } + if (from && from === conversationId) { + return renderString('GroupV2--member-remove--other--self', i18n, [ + renderContact(from), + ]); + } + if (from) { + return renderString('GroupV2--member-remove--other--other', i18n, { + adminName: renderContact(from), + memberName: renderContact(conversationId), + }); + } + return renderString('GroupV2--member-remove--other--unknown', i18n, [ + renderContact(conversationId), + ]); } else if (detail.type === 'member-privilege') { const { conversationId, newPrivilege } = detail; const weAreMember = conversationId === ourConversationId; @@ -256,37 +250,38 @@ export function renderChangeDetail( i18n, [renderContact(from)] ); - } else { - return renderString( - 'GroupV2--member-privilege--promote--you--unknown', - i18n - ); - } - } else { - if (fromYou) { - return renderString( - 'GroupV2--member-privilege--promote--other--you', - i18n, - [renderContact(conversationId)] - ); - } else if (from) { - return renderString( - 'GroupV2--member-privilege--promote--other--other', - i18n, - { - adminName: renderContact(from), - memberName: renderContact(conversationId), - } - ); - } else { - return renderString( - 'GroupV2--member-privilege--promote--other--unknown', - i18n, - [renderContact(conversationId)] - ); } + + return renderString( + 'GroupV2--member-privilege--promote--you--unknown', + i18n + ); } - } else if (newPrivilege === RoleEnum.DEFAULT) { + + if (fromYou) { + return renderString( + 'GroupV2--member-privilege--promote--other--you', + i18n, + [renderContact(conversationId)] + ); + } + if (from) { + return renderString( + 'GroupV2--member-privilege--promote--other--other', + i18n, + { + adminName: renderContact(from), + memberName: renderContact(conversationId), + } + ); + } + return renderString( + 'GroupV2--member-privilege--promote--other--unknown', + i18n, + [renderContact(conversationId)] + ); + } + if (newPrivilege === RoleEnum.DEFAULT) { if (weAreMember) { if (from) { return renderString( @@ -294,41 +289,39 @@ export function renderChangeDetail( i18n, [renderContact(from)] ); - } else { - return renderString( - 'GroupV2--member-privilege--demote--you--unknown', - i18n - ); - } - } else { - if (fromYou) { - return renderString( - 'GroupV2--member-privilege--demote--other--you', - i18n, - [renderContact(conversationId)] - ); - } else if (from) { - return renderString( - 'GroupV2--member-privilege--demote--other--other', - i18n, - { - adminName: renderContact(from), - memberName: renderContact(conversationId), - } - ); - } else { - return renderString( - 'GroupV2--member-privilege--demote--other--unknown', - i18n, - [renderContact(conversationId)] - ); } + return renderString( + 'GroupV2--member-privilege--demote--you--unknown', + i18n + ); } - } else { - throw new Error( - `member-privilege change type, privilege ${newPrivilege} is unknown` + + if (fromYou) { + return renderString( + 'GroupV2--member-privilege--demote--other--you', + i18n, + [renderContact(conversationId)] + ); + } + if (from) { + return renderString( + 'GroupV2--member-privilege--demote--other--other', + i18n, + { + adminName: renderContact(from), + memberName: renderContact(conversationId), + } + ); + } + return renderString( + 'GroupV2--member-privilege--demote--other--unknown', + i18n, + [renderContact(conversationId)] ); } + throw new Error( + `member-privilege change type, privilege ${newPrivilege} is unknown` + ); } else if (detail.type === 'pending-add-one') { const { conversationId } = detail; const weAreInvited = conversationId === ourConversationId; @@ -337,22 +330,20 @@ export function renderChangeDetail( return renderString('GroupV2--pending-add--one--you--other', i18n, [ renderContact(from), ]); - } else { - return renderString('GroupV2--pending-add--one--you--unknown', i18n); - } - } else { - if (fromYou) { - return renderString('GroupV2--pending-add--one--other--you', i18n, [ - renderContact(conversationId), - ]); - } else if (from) { - return renderString('GroupV2--pending-add--one--other--other', i18n, [ - renderContact(from), - ]); - } else { - return renderString('GroupV2--pending-add--one--other--unknown', i18n); } + return renderString('GroupV2--pending-add--one--you--unknown', i18n); } + if (fromYou) { + return renderString('GroupV2--pending-add--one--other--you', i18n, [ + renderContact(conversationId), + ]); + } + if (from) { + return renderString('GroupV2--pending-add--one--other--other', i18n, [ + renderContact(from), + ]); + } + return renderString('GroupV2--pending-add--one--other--unknown', i18n); } else if (detail.type === 'pending-add-many') { const { count } = detail; @@ -360,16 +351,16 @@ export function renderChangeDetail( return renderString('GroupV2--pending-add--many--you', i18n, [ count.toString(), ]); - } else if (from) { + } + if (from) { return renderString('GroupV2--pending-add--many--other', i18n, { memberName: renderContact(from), count: count.toString(), }); - } else { - return renderString('GroupV2--pending-add--many--unknown', i18n, [ - count.toString(), - ]); } + return renderString('GroupV2--pending-add--many--unknown', i18n, [ + count.toString(), + ]); } else if (detail.type === 'pending-remove-one') { const { inviter, conversationId } = detail; const weAreInviter = Boolean(inviter && inviter === ourConversationId); @@ -380,13 +371,15 @@ export function renderChangeDetail( return renderString('GroupV2--pending-remove--decline--you', i18n, [ renderContact(conversationId), ]); - } else if (fromYou) { + } + if (fromYou) { return renderString( 'GroupV2--pending-remove--revoke-invite-from-you--one--you', i18n, [renderContact(conversationId)] ); - } else if (from) { + } + if (from) { return renderString( 'GroupV2--pending-remove--revoke-invite-from-you--one--other', i18n, @@ -395,29 +388,30 @@ export function renderChangeDetail( inviteeName: renderContact(conversationId), } ); - } else { - return renderString( - 'GroupV2--pending-remove--revoke-invite-from-you--one--unknown', - i18n, - [renderContact(conversationId)] - ); } - } else if (sentByInvited) { + return renderString( + 'GroupV2--pending-remove--revoke-invite-from-you--one--unknown', + i18n, + [renderContact(conversationId)] + ); + } + if (sentByInvited) { if (inviter) { return renderString('GroupV2--pending-remove--decline--other', i18n, [ renderContact(inviter), ]); - } else { - return renderString('GroupV2--pending-remove--decline--unknown', i18n); } - } else if (inviter) { + return renderString('GroupV2--pending-remove--decline--unknown', i18n); + } + if (inviter) { if (fromYou) { return renderString( 'GroupV2--pending-remove--revoke-invite-from--one--you', i18n, [renderContact(inviter)] ); - } else if (from) { + } + if (from) { return renderString( 'GroupV2--pending-remove--revoke-invite-from--one--other', i18n, @@ -426,29 +420,22 @@ export function renderChangeDetail( memberName: renderContact(inviter), } ); - } else { - return renderString( - 'GroupV2--pending-remove--revoke-invite-from--one--unknown', - i18n, - [renderContact(inviter)] - ); - } - } else { - if (fromYou) { - return renderString('GroupV2--pending-remove--revoke--one--you', i18n); - } else if (from) { - return renderString( - 'GroupV2--pending-remove--revoke--one--other', - i18n, - [renderContact(from)] - ); - } else { - return renderString( - 'GroupV2--pending-remove--revoke--one--unknown', - i18n - ); } + return renderString( + 'GroupV2--pending-remove--revoke-invite-from--one--unknown', + i18n, + [renderContact(inviter)] + ); } + if (fromYou) { + return renderString('GroupV2--pending-remove--revoke--one--you', i18n); + } + if (from) { + return renderString('GroupV2--pending-remove--revoke--one--other', i18n, [ + renderContact(from), + ]); + } + return renderString('GroupV2--pending-remove--revoke--one--unknown', i18n); } else if (detail.type === 'pending-remove-many') { const { count, inviter } = detail; const weAreInviter = Boolean(inviter && inviter === ourConversationId); @@ -460,7 +447,8 @@ export function renderChangeDetail( i18n, [count.toString()] ); - } else if (from) { + } + if (from) { return renderString( 'GroupV2--pending-remove--revoke-invite-from-you--many--other', i18n, @@ -469,14 +457,14 @@ export function renderChangeDetail( count: count.toString(), } ); - } else { - return renderString( - 'GroupV2--pending-remove--revoke-invite-from-you--many--unknown', - i18n, - [count.toString()] - ); } - } else if (inviter) { + return renderString( + 'GroupV2--pending-remove--revoke-invite-from-you--many--unknown', + i18n, + [count.toString()] + ); + } + if (inviter) { if (fromYou) { return renderString( 'GroupV2--pending-remove--revoke-invite-from--many--you', @@ -486,7 +474,8 @@ export function renderChangeDetail( memberName: renderContact(inviter), } ); - } else if (from) { + } + if (from) { return renderString( 'GroupV2--pending-remove--revoke-invite-from--many--other', i18n, @@ -496,40 +485,36 @@ export function renderChangeDetail( memberName: renderContact(inviter), } ); - } else { - return renderString( - 'GroupV2--pending-remove--revoke-invite-from--many--unknown', - i18n, - { - count: count.toString(), - memberName: renderContact(inviter), - } - ); - } - } else { - if (fromYou) { - return renderString( - 'GroupV2--pending-remove--revoke--many--you', - i18n, - [count.toString()] - ); - } else if (from) { - return renderString( - 'GroupV2--pending-remove--revoke--many--other', - i18n, - { - memberName: renderContact(from), - count: count.toString(), - } - ); - } else { - return renderString( - 'GroupV2--pending-remove--revoke--many--unknown', - i18n, - [count.toString()] - ); } + return renderString( + 'GroupV2--pending-remove--revoke-invite-from--many--unknown', + i18n, + { + count: count.toString(), + memberName: renderContact(inviter), + } + ); } + if (fromYou) { + return renderString('GroupV2--pending-remove--revoke--many--you', i18n, [ + count.toString(), + ]); + } + if (from) { + return renderString( + 'GroupV2--pending-remove--revoke--many--other', + i18n, + { + memberName: renderContact(from), + count: count.toString(), + } + ); + } + return renderString( + 'GroupV2--pending-remove--revoke--many--unknown', + i18n, + [count.toString()] + ); } else { throw missingCaseError(detail); } diff --git a/ts/groups.ts b/ts/groups.ts index 895801144713..4124403a1648 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -1,5 +1,3 @@ -/* tslint:disable no-dynamic-delete no-unnecessary-local-variable */ - import { compact, Dictionary, @@ -8,6 +6,8 @@ import { isNumber, values, } from 'lodash'; +import { ClientZkGroupCipher } from 'zkgroup'; +import { v4 as getGuid } from 'uuid'; import { getCredentialsForToday, GROUP_CREDENTIALS_KEY, @@ -33,7 +33,6 @@ import { getClientZkAuthOperations, getClientZkGroupCipher, } from './util/zkgroup'; -import { ClientZkGroupCipher } from 'zkgroup'; import { arrayBufferToBase64, arrayBufferToHex, @@ -50,7 +49,6 @@ import { ProtoBinaryType, } from './textsecure.d'; import { GroupCredentialsType } from './textsecure/WebAPI'; -import { v4 as getGuid } from 'uuid'; import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from '../js/modules/types/message'; export type GroupV2AccessAttributesChangeType = { @@ -250,7 +248,9 @@ export async function uploadGroupChange({ // Utility -export function deriveGroupFields(masterKey: ArrayBuffer) { +export function deriveGroupFields( + masterKey: ArrayBuffer +): Record { const secretParams = deriveGroupSecretParams(masterKey); const publicParams = deriveGroupPublicParams(secretParams); const id = deriveGroupID(secretParams); @@ -273,7 +273,9 @@ type MaybeUpdatePropsType = { dropInitialJoinMessage?: boolean; }; -export async function waitThenMaybeUpdateGroup(options: MaybeUpdatePropsType) { +export async function waitThenMaybeUpdateGroup( + options: MaybeUpdatePropsType +): Promise { // First wait to process all incoming messages on the websocket await window.waitForEmptyEventQueue(); @@ -321,8 +323,9 @@ export async function maybeUpdateGroup({ conversation.set(newAttributes); - // Ensure that all generated message are ordered properly. Before the provided timestamp - // so update messages appear before the initiating message, or after now(). + // Ensure that all generated messages are ordered properly. + // Before the provided timestamp so update messages appear before the + // initiating message, or after now(). let syntheticTimestamp = receivedAt ? receivedAt - (groupChangeMessages.length + 1) : Date.now(); @@ -355,7 +358,6 @@ export async function maybeUpdateGroup({ const contact = window.ConversationController.get(member.uuid); if (member.profileKey && contact && !contact.get('profileKey')) { - // tslint:disable-next-line no-floating-promises contact.setProfileKey(member.profileKey); } }); @@ -487,8 +489,8 @@ async function updateGroupViaState({ authCredentialBase64: groupCredentials.tomorrow.credential, }); return result; - } catch (error) { - if (error.code === GROUP_ACCESS_DENIED_CODE) { + } catch (subError) { + if (subError.code === GROUP_ACCESS_DENIED_CODE) { return generateLeftGroupChanges(group); } } @@ -537,9 +539,8 @@ async function updateGroupViaLogs({ ...deltaOptions, authCredentialBase64: groupCredentials.tomorrow.credential, }); - } else { - throw error; } + throw error; } } @@ -654,6 +655,7 @@ async function getGroupDelta({ let response; const changes: Array = []; do { + // eslint-disable-next-line no-await-in-loop response = await sender.getGroupLog(revisionToFetch, options); changes.push(response.changes); if (response.end) { @@ -689,6 +691,7 @@ async function integrateGroupChanges({ const { groupChanges } = changes[i]; if (!groupChanges) { + // eslint-disable-next-line no-continue continue; } @@ -699,6 +702,7 @@ async function integrateGroupChanges({ const { groupChange } = changeState; if (!groupChange) { + // eslint-disable-next-line no-continue continue; } @@ -707,6 +711,7 @@ async function integrateGroupChanges({ newAttributes, groupChangeMessages, members, + // eslint-disable-next-line no-await-in-loop } = await integrateGroupChange({ group: attributes, newRevision, @@ -868,7 +873,6 @@ export async function getCurrentGroupState({ }; } -// tslint:disable-next-line max-func-body-length cyclomatic-complexity function extractDiffs({ current, dropInitialJoinMessage, @@ -1126,7 +1130,6 @@ type GroupChangeResultType = { newProfileKeys: Array; }; -// tslint:disable-next-line cyclomatic-complexity max-func-body-length async function applyGroupChange({ group, actions, @@ -1158,7 +1161,7 @@ async function applyGroupChange({ result.revision = version; // addMembers?: Array; - (actions.addMembers || []).map(addMember => { + (actions.addMembers || []).forEach(addMember => { const { added } = addMember; if (!added) { throw new Error('applyGroupChange: addMember.added is missing'); @@ -1249,7 +1252,8 @@ async function applyGroupChange({ } }); - // modifyMemberProfileKeys?: Array; + // modifyMemberProfileKeys?: + // Array; (actions.modifyMemberProfileKeys || []).forEach(modifyMemberProfileKey => { const { profileKey, uuid } = modifyMemberProfileKey; if (!profileKey || !uuid) { @@ -1374,8 +1378,7 @@ async function applyGroupChange({ // modifyTitle?: GroupChangeClass.Actions.ModifyTitleAction; if (actions.modifyTitle) { - const title: GroupAttributeBlobClass | undefined = - actions.modifyTitle.title; + const { title } = actions.modifyTitle; if (title && title.content === 'title') { result.name = title.title; } else { @@ -1388,11 +1391,12 @@ async function applyGroupChange({ // modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction; if (actions.modifyAvatar) { - const avatar = actions.modifyAvatar.avatar; + const { avatar } = actions.modifyAvatar; await applyNewAvatar(avatar, result, logId); } - // modifyDisappearingMessagesTimer?: GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction; + // modifyDisappearingMessagesTimer?: + // GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction; if (actions.modifyDisappearingMessagesTimer) { const disappearingMessagesTimer: GroupAttributeBlobClass | undefined = actions.modifyDisappearingMessagesTimer.timer; @@ -1415,7 +1419,8 @@ async function applyGroupChange({ attributes: ACCESS_ENUM.MEMBER, }; - // modifyAttributesAccess?: GroupChangeClass.Actions.ModifyAttributesAccessControlAction; + // modifyAttributesAccess?: + // GroupChangeClass.Actions.ModifyAttributesAccessControlAction; if (actions.modifyAttributesAccess) { result.accessControl = { ...result.accessControl, @@ -1447,6 +1452,8 @@ async function applyGroupChange({ }; } +// Ovewriting result.avatar as part of functionality +/* eslint-disable no-param-reassign */ async function applyNewAvatar( newAvatar: string | undefined, result: ConversationAttributesType, @@ -1514,8 +1521,8 @@ async function applyNewAvatar( result.avatar = undefined; } } +/* eslint-enable no-param-reassign */ -// tslint:disable-next-line cyclomatic-complexity max-func-body-length async function applyGroupState( group: ConversationAttributesType, groupState: GroupClass @@ -1532,7 +1539,7 @@ async function applyGroupState( // title // Note: During decryption, title becomes a GroupAttributeBlob - const title: GroupAttributeBlobClass | undefined = groupState.title; + const { title } = groupState; if (title && title.content === 'title') { result.name = title.title; } else { @@ -1544,8 +1551,7 @@ async function applyGroupState( // disappearingMessagesTimer // Note: during decryption, disappearingMessageTimer becomes a GroupAttributeBlob - const disappearingMessagesTimer: GroupAttributeBlobClass | undefined = - groupState.disappearingMessagesTimer; + const { disappearingMessagesTimer } = groupState; if ( disappearingMessagesTimer && disappearingMessagesTimer.content === 'disappearingMessagesDuration' @@ -1665,13 +1671,13 @@ function hasData(data: ProtoBinaryType): boolean { return data && data.limit > 0; } -// tslint:disable-next-line max-func-body-length cyclomatic-complexity function decryptGroupChange( - actions: GroupChangeClass.Actions, + _actions: GroupChangeClass.Actions, groupSecretParams: string, logId: string ): GroupChangeClass.Actions { const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams); + const actions = _actions; if (hasData(actions.sourceUuid)) { try { @@ -1701,7 +1707,9 @@ function decryptGroupChange( // addMembers?: Array; actions.addMembers = compact( - (actions.addMembers || []).map(addMember => { + (actions.addMembers || []).map(_addMember => { + const addMember = _addMember; + if (addMember.added) { const decrypted = decryptMember( clientZkGroupCipher, @@ -1714,17 +1722,16 @@ function decryptGroupChange( addMember.added = decrypted; return addMember; - } else { - throw new Error( - 'decryptGroupChange: AddMember was missing added field!' - ); } + throw new Error('decryptGroupChange: AddMember was missing added field!'); }) ); // deleteMembers?: Array; actions.deleteMembers = compact( - (actions.deleteMembers || []).map(deleteMember => { + (actions.deleteMembers || []).map(_deleteMember => { + const deleteMember = _deleteMember; + if (hasData(deleteMember.deletedUserId)) { try { deleteMember.deletedUserId = decryptUuid( @@ -1764,7 +1771,9 @@ function decryptGroupChange( // modifyMemberRoles?: Array; actions.modifyMemberRoles = compact( - (actions.modifyMemberRoles || []).map(modifyMember => { + (actions.modifyMemberRoles || []).map(_modifyMember => { + const modifyMember = _modifyMember; + if (hasData(modifyMember.userId)) { try { modifyMember.userId = decryptUuid( @@ -1808,9 +1817,12 @@ function decryptGroupChange( }) ); - // modifyMemberProfileKeys?: Array; + // modifyMemberProfileKeys?: + // Array; actions.modifyMemberProfileKeys = compact( - (actions.modifyMemberProfileKeys || []).map(modifyMemberProfileKey => { + (actions.modifyMemberProfileKeys || []).map(_modifyMemberProfileKey => { + const modifyMemberProfileKey = _modifyMemberProfileKey; + if (hasData(modifyMemberProfileKey.presentation)) { const { profileKey, uuid } = decryptProfileKeyCredentialPresentation( clientZkGroupCipher, @@ -1854,7 +1866,9 @@ function decryptGroupChange( // addPendingMembers?: Array; actions.addPendingMembers = compact( - (actions.addPendingMembers || []).map(addPendingMember => { + (actions.addPendingMembers || []).map(_addPendingMember => { + const addPendingMember = _addPendingMember; + if (addPendingMember.added) { const decrypted = decryptPendingMember( clientZkGroupCipher, @@ -1867,17 +1881,18 @@ function decryptGroupChange( addPendingMember.added = decrypted; return addPendingMember; - } else { - throw new Error( - 'decryptGroupChange: addPendingMember was missing added field!' - ); } + throw new Error( + 'decryptGroupChange: addPendingMember was missing added field!' + ); }) ); // deletePendingMembers?: Array; actions.deletePendingMembers = compact( - (actions.deletePendingMembers || []).map(deletePendingMember => { + (actions.deletePendingMembers || []).map(_deletePendingMember => { + const deletePendingMember = _deletePendingMember; + if (hasData(deletePendingMember.deletedUserId)) { try { deletePendingMember.deletedUserId = decryptUuid( @@ -1917,7 +1932,9 @@ function decryptGroupChange( // promotePendingMembers?: Array; actions.promotePendingMembers = compact( - (actions.promotePendingMembers || []).map(promotePendingMember => { + (actions.promotePendingMembers || []).map(_promotePendingMember => { + const promotePendingMember = _promotePendingMember; + if (hasData(promotePendingMember.presentation)) { const { profileKey, uuid } = decryptProfileKeyCredentialPresentation( clientZkGroupCipher, @@ -1979,7 +1996,8 @@ function decryptGroupChange( // modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction; // Note: decryption happens during application of the change, on download of the avatar - // modifyDisappearingMessagesTimer?: GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction; + // modifyDisappearingMessagesTimer?: + // GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction; if ( actions.modifyDisappearingMessagesTimer && hasData(actions.modifyDisappearingMessagesTimer.timer) @@ -2002,7 +2020,8 @@ function decryptGroupChange( actions.modifyDisappearingMessagesTimer.timer = undefined; } - // modifyAttributesAccess?: GroupChangeClass.Actions.ModifyAttributesAccessControlAction; + // modifyAttributesAccess?: + // GroupChangeClass.Actions.ModifyAttributesAccessControlAction; if ( actions.modifyAttributesAccess && !isValidAccess(actions.modifyAttributesAccess.attributesAccess) @@ -2025,13 +2044,13 @@ function decryptGroupChange( return actions; } -// tslint:disable-next-line max-func-body-length function decryptGroupState( - groupState: GroupClass, + _groupState: GroupClass, groupSecretParams: string, logId: string ): GroupClass { const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams); + const groupState = _groupState; // title if (hasData(groupState.title)) { @@ -2121,9 +2140,11 @@ function decryptGroupState( function decryptMember( clientZkGroupCipher: ClientZkGroupCipher, - member: MemberClass, + _member: MemberClass, logId: string ) { + const member = _member; + // userId if (hasData(member.userId)) { try { @@ -2175,12 +2196,13 @@ function decryptMember( return member; } -// tslint:disable-next-line max-func-body-length cyclomatic-complexity function decryptPendingMember( clientZkGroupCipher: ClientZkGroupCipher, - member: PendingMemberClass, + _member: PendingMemberClass, logId: string ) { + const member = _member; + // addedByUserId if (hasData(member.addedByUserId)) { try { diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 4a606c3f33ca..6473edf9cc71 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -25,7 +25,7 @@ declare class DeletesModelType extends Backbone.Model { type TaskResultType = any; -type MessageAttributesType = { +export type MessageAttributesType = { id: string; type?: string; @@ -63,9 +63,9 @@ declare class MessageModelType extends Backbone.Model { ): Promise; } -type ConversationTypeType = 'private' | 'group'; +export type ConversationTypeType = 'private' | 'group'; -type ConversationAttributesType = { +export type ConversationAttributesType = { id: string; type: ConversationTypeType; timestamp: number; @@ -201,7 +201,7 @@ export declare class ConversationModelType extends Backbone.Model< wrapSend: (sendPromise: Promise) => Promise; } -declare class ConversationModelCollectionType extends Backbone.Collection< +export declare class ConversationModelCollectionType extends Backbone.Collection< ConversationModelType > { resetLookups(): void; diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts index ee6f9a8f7062..a494741b5725 100644 --- a/ts/textsecure.d.ts +++ b/ts/textsecure.d.ts @@ -209,7 +209,7 @@ type ProtobufCollectionType = DeviceMessagesProtobufTypes & // Note: there are a lot of places in the code that overwrite a field like this // with a type that the app can use. Being more rigorous with these // types would require code changes, out of scope for now. -type ProtoBinaryType = any; +export type ProtoBinaryType = any; type ProtoBigNumberType = any; // Groups.proto diff --git a/tslint.json b/tslint.json index eefe15e091d9..8cd695ffce98 100644 --- a/tslint.json +++ b/tslint.json @@ -177,6 +177,7 @@ "rulesDirectory": ["node_modules/tslint-microsoft-contrib"], "linterOptions": { "exclude": [ + "ts/*.ts", "ts/components/emoji/**", "ts/backbone/**", "ts/build/**",