From 6b094e15142db34f516c36a67a9373f686ce53e6 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 1 Apr 2020 11:59:11 -0700 Subject: [PATCH] Refactor: Move data-access code to Typescript w/ shared interface --- .gitignore | 2 +- app/sql_channel.js | 2 +- app/user_config.d.ts | 1 + js/background.js | 14 +- js/conversation_controller.js | 5 +- js/expiring_messages.js | 9 +- js/models/conversations.js | 40 +- js/models/messages.js | 5 +- js/modules/attachment_downloads.js | 4 +- js/modules/debug.js | 2 +- js/modules/emojis.js | 2 +- js/modules/migrate_to_sql.js | 2 +- js/modules/privacy.d.ts | 1 + js/modules/signal.js | 2 +- js/modules/stickers.js | 2 +- js/modules/types/message.d.ts | 1 + js/signal_protocol_store.js | 2 +- js/views/conversation_view.js | 8 +- main.js | 2 +- test/backup_test.js | 2 +- js/modules/data.js => ts/sql/Client.ts | 724 ++++++++++++++-------- ts/sql/Interface.ts | 382 ++++++++++++ app/sql.js => ts/sql/Server.ts | 804 +++++++++++++++++-------- ts/sqlcipher.d.ts | 181 ++++++ ts/state/ducks/emojis.ts | 4 +- ts/state/ducks/search.ts | 12 +- ts/state/ducks/stickers.ts | 12 +- ts/types/I18N.ts | 11 + ts/types/Logging.ts | 10 + ts/updater/common.ts | 22 +- ts/updater/index.ts | 3 +- ts/updater/macos.ts | 4 +- ts/updater/windows.ts | 4 +- ts/util/lint/exceptions.json | 2 +- ts/window.d.ts | 10 + 35 files changed, 1695 insertions(+), 598 deletions(-) create mode 100644 app/user_config.d.ts create mode 100644 js/modules/privacy.d.ts create mode 100644 js/modules/types/message.d.ts rename js/modules/data.js => ts/sql/Client.ts (65%) create mode 100644 ts/sql/Interface.ts rename app/sql.js => ts/sql/Server.ts (81%) create mode 100644 ts/sqlcipher.d.ts create mode 100644 ts/types/I18N.ts create mode 100644 ts/types/Logging.ts diff --git a/.gitignore b/.gitignore index 4a06bc560..b051b4c29 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ release/ /dev-app-update.yml .nyc_output/ *.sublime* -sql/ +/sql/ # generated files js/components.js diff --git a/app/sql_channel.js b/app/sql_channel.js index cbe24656d..b74401313 100644 --- a/app/sql_channel.js +++ b/app/sql_channel.js @@ -1,6 +1,6 @@ const electron = require('electron'); const Queue = require('p-queue').default; -const sql = require('./sql'); +const sql = require('../ts/sql/Server').default; const { remove: removeUserConfig } = require('./user_config'); const { remove: removeEphemeralConfig } = require('./ephemeral_config'); diff --git a/app/user_config.d.ts b/app/user_config.d.ts new file mode 100644 index 000000000..6dc591a70 --- /dev/null +++ b/app/user_config.d.ts @@ -0,0 +1 @@ +export function remove(): void; diff --git a/js/background.js b/js/background.js index 7e961e136..c616eaafe 100644 --- a/js/background.js +++ b/js/background.js @@ -1996,10 +1996,7 @@ conversation.set({ avatar: null }); } - window.Signal.Data.updateConversation( - details.number || details.uuid, - conversation.attributes - ); + window.Signal.Data.updateConversation(conversation.attributes); const { expireTimer } = details; const isValidExpireTimer = typeof expireTimer === 'number'; @@ -2108,7 +2105,7 @@ conversation.set(newAttributes); } - window.Signal.Data.updateConversation(id, conversation.attributes); + window.Signal.Data.updateConversation(conversation.attributes); const { appView } = window.owsDesktopApp; if (appView && appView.installView && appView.installView.didLink) { @@ -2240,7 +2237,7 @@ ); conversation.set({ profileSharing: true }); - window.Signal.Data.updateConversation(id, conversation.attributes); + window.Signal.Data.updateConversation(conversation.attributes); // Then we update our own profileKey if it's different from what we have const ourNumber = textsecure.storage.user.getNumber(); @@ -2518,10 +2515,7 @@ ev.confirm(); } - window.Signal.Data.updateConversation( - conversationId, - conversation.attributes - ); + window.Signal.Data.updateConversation(conversation.attributes); }); } diff --git a/js/conversation_controller.js b/js/conversation_controller.js index 68f0bda27..625ec40c4 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -240,10 +240,7 @@ this.model.set({ draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH), }); - window.Signal.Data.updateConversation( - conversation.id, - conversation.attributes - ); + window.Signal.Data.updateConversation(conversation.attributes); } }) ); diff --git a/js/expiring_messages.js b/js/expiring_messages.js index be1db7ff0..4b80d4326 100644 --- a/js/expiring_messages.js +++ b/js/expiring_messages.js @@ -60,16 +60,15 @@ let timeout; async function checkExpiringMessages() { // Look up the next expiring message and set a timer to destroy it - const messages = await window.Signal.Data.getNextExpiringMessage({ - MessageCollection: Whisper.MessageCollection, + const message = await window.Signal.Data.getNextExpiringMessage({ + Message: Whisper.Message, }); - const next = messages.at(0); - if (!next) { + if (!message) { return; } - const expiresAt = next.get('expires_at'); + const expiresAt = message.get('expires_at'); Whisper.ExpiringMessagesListener.nextExpiration = expiresAt; window.log.info('next message expires', new Date(expiresAt).toISOString()); diff --git a/js/models/conversations.js b/js/models/conversations.js index e1473ea46..aeba9604c 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -424,7 +424,7 @@ const oldValue = this.get('e164'); if (e164 !== oldValue) { this.set('e164', e164); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); this.trigger('idUpdated', this, 'e164', oldValue); } }, @@ -432,7 +432,7 @@ const oldValue = this.get('uuid'); if (uuid !== oldValue) { this.set('uuid', uuid); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); this.trigger('idUpdated', this, 'uuid', oldValue); } }, @@ -440,7 +440,7 @@ const oldValue = this.get('groupId'); if (groupId !== oldValue) { this.set('groupId', groupId); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); this.trigger('idUpdated', this, 'groupId', oldValue); } }, @@ -461,7 +461,7 @@ if (this.get('verified') !== verified) { this.set({ verified }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); } return; @@ -525,7 +525,7 @@ } this.set({ verified }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); // Three situations result in a verification notice in the conversation: // 1) The message came from an explicit verification in another client (not @@ -1222,7 +1222,7 @@ draft: null, draftTimestamp: null, }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); // We're offline! if (!textsecure.messaging) { @@ -1354,10 +1354,7 @@ conversation.set({ sealedSender: SEALED_SENDER.DISABLED, }); - window.Signal.Data.updateConversation( - conversation.id, - conversation.attributes - ); + window.Signal.Data.updateConversation(conversation.attributes); } }) ); @@ -1385,10 +1382,7 @@ sealedSender: SEALED_SENDER.UNRESTRICTED, }); } - window.Signal.Data.updateConversation( - conversation.id, - conversation.attributes - ); + window.Signal.Data.updateConversation(conversation.attributes); } }) ); @@ -1530,7 +1524,7 @@ this.set(lastMessageUpdate); if (this.hasChanged()) { - window.Signal.Data.updateConversation(this.id, this.attributes, { + window.Signal.Data.updateConversation(this.attributes, { Conversation: Whisper.Conversation, }); } @@ -1538,7 +1532,7 @@ async setArchived(isArchived) { this.set({ isArchived }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); }, async updateExpirationTimer( @@ -1581,7 +1575,7 @@ const timestamp = (receivedAt || Date.now()) - 1; this.set({ expireTimer }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); const model = new Whisper.Message({ // Even though this isn't reflected to the user, we want to place the last seen @@ -1787,7 +1781,7 @@ if (this.get('type') === 'group') { const groupNumbers = this.getRecipients(); this.set({ left: true }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); const model = new Whisper.Message({ group_update: { left: 'You' }, @@ -1852,7 +1846,7 @@ const unreadCount = unreadMessages.length - read.length; this.set({ unreadCount }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); // If a message has errors, we don't want to send anything out about it. // read syncs - let's wait for a client that really understands the message @@ -2055,7 +2049,7 @@ } if (c.hasChanged()) { - window.Signal.Data.updateConversation(id, c.attributes); + window.Signal.Data.updateConversation(c.attributes); } }, async setProfileName(encryptedName) { @@ -2135,7 +2129,7 @@ await this.deriveAccessKeyIfNeeded(); - window.Signal.Data.updateConversation(this.id, this.attributes, { + window.Signal.Data.updateConversation(this.attributes, { Conversation: Whisper.Conversation, }); } @@ -2159,7 +2153,7 @@ sealedSender: SEALED_SENDER.UNKNOWN, }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); } }, @@ -2219,7 +2213,7 @@ timestamp: null, active_at: null, }); - window.Signal.Data.updateConversation(this.id, this.attributes); + window.Signal.Data.updateConversation(this.attributes); await window.Signal.Data.removeAllMessagesInConversation(this.id, { MessageCollection: Whisper.MessageCollection, diff --git a/js/models/messages.js b/js/models/messages.js index 544e93d67..8cb5c4bc5 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -2437,10 +2437,7 @@ } MessageController.register(message.id, message); - window.Signal.Data.updateConversation( - conversationId, - conversation.attributes - ); + window.Signal.Data.updateConversation(conversation.attributes); await message.queueAttachmentDownloads(); await window.Signal.Data.saveMessage(message.attributes, { diff --git a/js/modules/attachment_downloads.js b/js/modules/attachment_downloads.js index d676b20d2..49e10f927 100644 --- a/js/modules/attachment_downloads.js +++ b/js/modules/attachment_downloads.js @@ -18,7 +18,7 @@ const { saveAttachmentDownloadJob, saveMessage, setAttachmentDownloadJobPending, -} = require('./data'); +} = require('../../ts/sql/Client').default; const { stringFromBytes } = require('../../ts/Crypto'); module.exports = { @@ -445,7 +445,7 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) { hash, }, }); - Signal.Data.updateConversation(conversationId, conversation.attributes); + Signal.Data.updateConversation(conversation.attributes); message.set({ group_update: { diff --git a/js/modules/debug.js b/js/modules/debug.js index 070986cad..6f16ada8b 100644 --- a/js/modules/debug.js +++ b/js/modules/debug.js @@ -50,7 +50,7 @@ exports.createConversation = async ({ unread: numMessages, }); const conversationId = conversation.get('id'); - Signal.Data.updateConversation(conversationId, conversation.attributes); + Signal.Data.updateConversation(conversation.attributes); await Promise.all( range(0, numMessages).map(async index => { diff --git a/js/modules/emojis.js b/js/modules/emojis.js index f7c0dcfba..7bd8f2bb3 100644 --- a/js/modules/emojis.js +++ b/js/modules/emojis.js @@ -1,5 +1,5 @@ const { take } = require('lodash'); -const { getRecentEmojis } = require('./data'); +const { getRecentEmojis } = require('../../ts/sql/Client').default; module.exports = { getInitialState, diff --git a/js/modules/migrate_to_sql.js b/js/modules/migrate_to_sql.js index fb6995472..1e882270a 100644 --- a/js/modules/migrate_to_sql.js +++ b/js/modules/migrate_to_sql.js @@ -22,7 +22,7 @@ const { saveConversations, _removeConversations, -} = require('./data'); +} = require('../../ts/sql/Client').default; const { getMessageExportLastIndex, setMessageExportLastIndex, diff --git a/js/modules/privacy.d.ts b/js/modules/privacy.d.ts new file mode 100644 index 000000000..77fd0a202 --- /dev/null +++ b/js/modules/privacy.d.ts @@ -0,0 +1 @@ +export function redactAll(log: string): string; diff --git a/js/modules/signal.js b/js/modules/signal.js index d01228ce2..7d42c4a09 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -3,7 +3,7 @@ const { bindActionCreators } = require('redux'); const Backbone = require('../../ts/backbone'); const Crypto = require('../../ts/Crypto'); -const Data = require('./data'); +const Data = require('../../ts/sql/Client').default; const Database = require('./database'); const Emojis = require('./emojis'); const EmojiLib = require('../../ts/components/emoji/lib'); diff --git a/js/modules/stickers.js b/js/modules/stickers.js index 9b5fa9eb6..91ca64f8d 100644 --- a/js/modules/stickers.js +++ b/js/modules/stickers.js @@ -47,7 +47,7 @@ const { getAllStickers, getRecentStickers, updateStickerPackStatus, -} = require('./data'); +} = require('../../ts/sql/Client').default; module.exports = { BLESSED_PACKS, diff --git a/js/modules/types/message.d.ts b/js/modules/types/message.d.ts new file mode 100644 index 000000000..123d5bd23 --- /dev/null +++ b/js/modules/types/message.d.ts @@ -0,0 +1 @@ +export const CURRENT_SCHEMA_VERSION: number; diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 3188aebed..b43266192 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -431,7 +431,7 @@ } } - await window.Signal.Data.removeSessionsById(identifier); + await window.Signal.Data.removeSessionsByConversation(identifier); }, async archiveSiblingSessions(identifier) { const address = libsignal.SignalProtocolAddress.fromString(identifier); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index b3ee6aca3..4db60a6d2 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1123,13 +1123,7 @@ }, async saveModel() { - window.Signal.Data.updateConversation( - this.model.id, - this.model.attributes, - { - Conversation: Whisper.Conversation, - } - ); + window.Signal.Data.updateConversation(this.model.attributes); }, async addAttachment(attachment) { diff --git a/main.js b/main.js index a6aba174e..95dbafc2f 100644 --- a/main.js +++ b/main.js @@ -87,7 +87,7 @@ const createTrayIcon = require('./app/tray_icon'); const dockIcon = require('./app/dock_icon'); const ephemeralConfig = require('./app/ephemeral_config'); const logging = require('./app/logging'); -const sql = require('./app/sql'); +const sql = require('./ts/sql/Server').default; const sqlChannels = require('./app/sql_channel'); const windowState = require('./app/window_state'); const { createTemplate } = require('./app/menu'); diff --git a/test/backup_test.js b/test/backup_test.js index 7b9ac7b9c..218cbe6d3 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -570,7 +570,7 @@ describe('Backup', () => { ); console.log('Backup test: Check messages'); - const messageCollection = await window.Signal.Data.getAllMessages({ + const messageCollection = await window.Signal.Data._getAllMessages({ MessageCollection: Whisper.MessageCollection, }); assert.strictEqual(messageCollection.length, MESSAGE_COUNT); diff --git a/js/modules/data.js b/ts/sql/Client.ts similarity index 65% rename from js/modules/data.js rename to ts/sql/Client.ts index d8faa624d..822da7286 100644 --- a/js/modules/data.js +++ b/ts/sql/Client.ts @@ -1,10 +1,11 @@ -/* global window, setTimeout, IDBKeyRange, ConversationController */ +// tslint:disable no-default-export no-unnecessary-local-variable -const electron = require('electron'); +import { ipcRenderer } from 'electron'; -const { +import { cloneDeep, - forEach, + compact, + fromPairs, get, groupBy, isFunction, @@ -12,13 +13,36 @@ const { last, map, set, -} = require('lodash'); +} from 'lodash'; -const { base64ToArrayBuffer, arrayBufferToBase64 } = require('../../ts/Crypto'); -const MessageType = require('./types/message'); -const { createBatcher } = require('../../ts/util/batcher'); +import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto'; +import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; +import { createBatcher } from '../util/batcher'; +import { v4 as getGuid } from 'uuid'; -const { ipcRenderer } = electron; +import { + AttachmentDownloadJobType, + BackboneConversationCollectionType, + BackboneConversationModelType, + BackboneMessageCollectionType, + BackboneMessageModelType, + ClientInterface, + ClientJobType, + ConversationType, + IdentityKeyType, + ItemType, + MessageType, + MessageTypeUnhydrated, + PreKeyType, + SearchResultMessageType, + ServerInterface, + SessionType, + SignedPreKeyType, + StickerPackStatusType, + StickerPackType, + StickerType, + UnprocessedType, +} from './Interface'; // We listen to a lot of events on ipcRenderer, often on the same channel. This prevents // any warnings that might be sent to the console in that case. @@ -35,20 +59,24 @@ const ERASE_DRAFTS_KEY = 'erase-drafts'; const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments'; const ENSURE_FILE_PERMISSIONS = 'ensure-file-permissions'; -const _jobs = Object.create(null); +type ClientJobUpdateType = { + resolve: Function; + reject: Function; + args?: Array; +}; + +const _jobs: { [id: string]: ClientJobType } = Object.create(null); const _DEBUG = false; let _jobCounter = 0; let _shuttingDown = false; -let _shutdownCallback = null; -let _shutdownPromise = null; +let _shutdownCallback: Function | null = null; +let _shutdownPromise: Promise | null = null; -const channels = {}; - -module.exports = { - _jobs, - _cleanData, - - shutdown, +// Because we can't force this module to conform to an interface, we narrow our exports +// to this one default export, which does conform to the interface. +// Note: In Javascript, you need to access the .default property when requiring it +// https://github.com/microsoft/TypeScript/issues/420 +const dataInterface: ClientInterface = { close, removeDB, removeIndexedDBFiles, @@ -87,7 +115,7 @@ module.exports = { getSessionsById, bulkAddSessions, removeSessionById, - removeSessionsById, + removeSessionsByConversation, removeAllSessions, getAllSessions, @@ -98,7 +126,6 @@ module.exports = { updateConversation, updateConversations, removeConversation, - _removeConversations, getAllConversations, getAllConversationIds, @@ -111,17 +138,12 @@ module.exports = { getMessageCount, saveMessage, - saveLegacyMessage, saveMessages, removeMessage, - _removeMessages, getUnreadByConversation, - removeAllMessagesInConversation, - getMessageBySender, getMessageById, - getAllMessages, getAllMessageIds, getMessagesBySentAt, getExpiredMessages, @@ -151,6 +173,7 @@ module.exports = { removeAttachmentDownloadJob, removeAllAttachmentDownloadJobs, + getStickerCount, createOrUpdateStickerPack, updateStickerPackStatus, createOrUpdateSticker, @@ -168,23 +191,57 @@ module.exports = { removeAll, removeAllConfiguration, + getMessagesNeedingUpgrade, + getMessagesWithVisualMediaAttachments, + getMessagesWithFileAttachments, + + // Test-only + + _getAllMessages, + + // Client-side only + + shutdown, + removeAllMessagesInConversation, + removeOtherData, cleanupOrphanedAttachments, ensureFilePermissions, - // Returning plain JSON - getMessagesNeedingUpgrade, getLegacyMessagesNeedingUpgrade, - getMessagesWithVisualMediaAttachments, - getMessagesWithFileAttachments, + saveLegacyMessage, + + // Client-side only, and test-only + + _removeConversations, + _removeMessages, + _cleanData, + _jobs, }; +export default dataInterface; + +const channelsAsUnknown = fromPairs( + compact( + map(dataInterface, (value: any) => { + if (isFunction(value)) { + return [value.name, makeChannel(value.name)]; + } + + return null; + }) + ) +) as any; + +const channels: ServerInterface = channelsAsUnknown; + // When IPC arguments are prepared for the cross-process send, they are JSON.stringified. // We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates), // We also cannot send objects with function-value keys, like what protobufjs gives us. -function _cleanData(data, path = 'root') { +function _cleanData(data: any, path: string = 'root') { if (data === null || data === undefined) { window.log.warn(`_cleanData: null or undefined value at path ${path}`); + return data; } @@ -197,29 +254,27 @@ function _cleanData(data, path = 'root') { } const keys = Object.keys(data); - for (let index = 0, max = keys.length; index < max; index += 1) { + const max = keys.length; + for (let index = 0; index < max; index += 1) { const key = keys[index]; const value = data[key]; if (value === null || value === undefined) { - // eslint-disable-next-line no-continue continue; } if (isFunction(value)) { // To prepare for Electron v9 IPC, we need to take functions off of any object - // eslint-disable-next-line no-param-reassign + // tslint:disable-next-line no-dynamic-delete delete data[key]; } else if (isFunction(value.toNumber)) { - // eslint-disable-next-line no-param-reassign + // tslint:disable-next-line no-dynamic-delete data[key] = value.toNumber(); } else if (Array.isArray(value)) { - // eslint-disable-next-line no-param-reassign data[key] = value.map((item, mapIndex) => _cleanData(item, `${path}.${key}.${mapIndex}`) ); } else if (isObject(value)) { - // eslint-disable-next-line no-param-reassign data[key] = _cleanData(value, `${path}.${key}`); } else if ( typeof value !== 'string' && @@ -241,6 +296,7 @@ async function _shutdown() { if (_shutdownPromise) { await _shutdownPromise; + return; } @@ -253,20 +309,24 @@ async function _shutdown() { // Outstanding jobs; we need to wait until the last one is done _shutdownPromise = new Promise((resolve, reject) => { - _shutdownCallback = error => { + _shutdownCallback = (error: Error) => { window.log.info('data.shutdown: process complete'); if (error) { - return reject(error); + reject(error); + + return; } - return resolve(); + resolve(); + + return; }; }); await _shutdownPromise; } -function _makeJob(fnName) { +function _makeJob(fnName: string) { if (_shuttingDown && fnName !== 'close') { throw new Error( `Rejecting SQL channel job (${fnName}); application is shutting down` @@ -287,14 +347,14 @@ function _makeJob(fnName) { return id; } -function _updateJob(id, data) { +function _updateJob(id: number, data: ClientJobUpdateType) { const { resolve, reject } = data; const { fnName, start } = _jobs[id]; _jobs[id] = { ..._jobs[id], ...data, - resolve: value => { + resolve: (value: any) => { _removeJob(id); const end = Date.now(); const delta = end - start; @@ -303,9 +363,10 @@ function _updateJob(id, data) { `SQL channel job ${id} (${fnName}) succeeded in ${end - start}ms` ); } + return resolve(value); }, - reject: error => { + reject: (error: Error) => { _removeJob(id); const end = Date.now(); window.log.info( @@ -324,12 +385,14 @@ function _updateJob(id, data) { }; } -function _removeJob(id) { +function _removeJob(id: number) { if (_DEBUG) { _jobs[id].complete = true; + return; } + // tslint:disable-next-line no-dynamic-delete delete _jobs[id]; if (_shutdownCallback) { @@ -340,13 +403,13 @@ function _removeJob(id) { } } -function _getJob(id) { +function _getJob(id: number) { return _jobs[id]; } ipcRenderer.on( `${SQL_CHANNEL_KEY}-done`, - (event, jobId, errorForDisplay, result) => { + (_, jobId, errorForDisplay, result) => { const job = _getJob(jobId); if (!job) { throw new Error( @@ -356,6 +419,12 @@ ipcRenderer.on( const { resolve, reject, fnName } = job; + if (!resolve || !reject) { + throw new Error( + `SQL channel job ${jobId} (${fnName}): didn't have a resolve or reject` + ); + } + if (errorForDisplay) { return reject( new Error( @@ -368,8 +437,8 @@ ipcRenderer.on( } ); -function makeChannel(fnName) { - channels[fnName] = (...args) => { +function makeChannel(fnName: string) { + return async (...args: Array) => { const jobId = _makeJob(fnName); return new Promise((resolve, reject) => { @@ -379,14 +448,12 @@ function makeChannel(fnName) { _updateJob(jobId, { resolve, reject, - args: _DEBUG ? args : null, + args: _DEBUG ? args : undefined, }); - setTimeout( - () => - reject(new Error(`SQL channel job ${jobId} (${fnName}) timed out`)), - DATABASE_UPDATE_TIMEOUT - ); + setTimeout(() => { + reject(new Error(`SQL channel job ${jobId} (${fnName}) timed out`)); + }, DATABASE_UPDATE_TIMEOUT); } catch (error) { _removeJob(jobId); @@ -396,15 +463,11 @@ function makeChannel(fnName) { }; } -forEach(module.exports, fn => { - if (isFunction(fn)) { - makeChannel(fn.name); - } -}); - -function keysToArrayBuffer(keys, data) { +function keysToArrayBuffer(keys: Array, data: any) { const updated = cloneDeep(data); - for (let i = 0, max = keys.length; i < max; i += 1) { + + const max = keys.length; + for (let i = 0; i < max; i += 1) { const key = keys[i]; const value = get(data, key); @@ -416,9 +479,11 @@ function keysToArrayBuffer(keys, data) { return updated; } -function keysFromArrayBuffer(keys, data) { +function keysFromArrayBuffer(keys: Array, data: any) { const updated = cloneDeep(data); - for (let i = 0, max = keys.length; i < max; i += 1) { + + const max = keys.length; + for (let i = 0; i < max; i += 1) { const key = keys[i]; const value = get(data, key); @@ -457,26 +522,33 @@ async function removeIndexedDBFiles() { // Identity Keys const IDENTITY_KEY_KEYS = ['publicKey']; -async function createOrUpdateIdentityKey(data) { +async function createOrUpdateIdentityKey(data: IdentityKeyType) { const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, { ...data, - id: ConversationController.getConversationId(data.id), + id: window.ConversationController.getConversationId(data.id), }); await channels.createOrUpdateIdentityKey(updated); } -async function getIdentityKeyById(identifier) { - const id = ConversationController.getConversationId(identifier); +async function getIdentityKeyById(identifier: string) { + const id = window.ConversationController.getConversationId(identifier); + if (!id) { + throw new Error('getIdentityKeyById: unable to find conversationId'); + } const data = await channels.getIdentityKeyById(id); + return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); } -async function bulkAddIdentityKeys(array) { +async function bulkAddIdentityKeys(array: Array) { const updated = map(array, data => keysFromArrayBuffer(IDENTITY_KEY_KEYS, data) ); await channels.bulkAddIdentityKeys(updated); } -async function removeIdentityKeyById(identifier) { - const id = ConversationController.getConversationId(identifier); +async function removeIdentityKeyById(identifier: string) { + const id = window.ConversationController.getConversationId(identifier); + if (!id) { + throw new Error('removeIdentityKeyById: unable to find conversationId'); + } await channels.removeIdentityKeyById(id); } async function removeAllIdentityKeys() { @@ -484,24 +556,26 @@ async function removeAllIdentityKeys() { } async function getAllIdentityKeys() { const keys = await channels.getAllIdentityKeys(); + return keys.map(key => keysToArrayBuffer(IDENTITY_KEY_KEYS, key)); } // Pre Keys -async function createOrUpdatePreKey(data) { +async function createOrUpdatePreKey(data: PreKeyType) { const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); await channels.createOrUpdatePreKey(updated); } -async function getPreKeyById(id) { +async function getPreKeyById(id: number) { const data = await channels.getPreKeyById(id); + return keysToArrayBuffer(PRE_KEY_KEYS, data); } -async function bulkAddPreKeys(array) { +async function bulkAddPreKeys(array: Array) { const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); await channels.bulkAddPreKeys(updated); } -async function removePreKeyById(id) { +async function removePreKeyById(id: number) { await channels.removePreKeyById(id); } async function removeAllPreKeys() { @@ -509,29 +583,34 @@ async function removeAllPreKeys() { } async function getAllPreKeys() { const keys = await channels.getAllPreKeys(); + return keys.map(key => keysToArrayBuffer(PRE_KEY_KEYS, key)); } // Signed Pre Keys const PRE_KEY_KEYS = ['privateKey', 'publicKey']; -async function createOrUpdateSignedPreKey(data) { +async function createOrUpdateSignedPreKey(data: SignedPreKeyType) { const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); await channels.createOrUpdateSignedPreKey(updated); } -async function getSignedPreKeyById(id) { +async function getSignedPreKeyById(id: number) { const data = await channels.getSignedPreKeyById(id); + return keysToArrayBuffer(PRE_KEY_KEYS, data); } async function getAllSignedPreKeys() { const keys = await channels.getAllSignedPreKeys(); - return keys.map(key => keysToArrayBuffer(PRE_KEY_KEYS, key)); + + return keys.map((key: SignedPreKeyType) => + keysToArrayBuffer(PRE_KEY_KEYS, key) + ); } -async function bulkAddSignedPreKeys(array) { +async function bulkAddSignedPreKeys(array: Array) { const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); await channels.bulkAddSignedPreKeys(updated); } -async function removeSignedPreKeyById(id) { +async function removeSignedPreKeyById(id: number) { await channels.removeSignedPreKeyById(id); } async function removeAllSignedPreKeys() { @@ -540,14 +619,14 @@ async function removeAllSignedPreKeys() { // Items -const ITEM_KEYS = { +const ITEM_KEYS: { [key: string]: Array | undefined } = { identityKey: ['value.pubKey', 'value.privKey'], senderCertificate: ['value.serialized'], senderCertificateWithUuid: ['value.serialized'], signaling_key: ['value'], profileKey: ['value'], }; -async function createOrUpdateItem(data) { +async function createOrUpdateItem(data: ItemType) { const { id } = data; if (!id) { throw new Error( @@ -560,7 +639,7 @@ async function createOrUpdateItem(data) { await channels.createOrUpdateItem(updated); } -async function getItemById(id) { +async function getItemById(id: string) { const keys = ITEM_KEYS[id]; const data = await channels.getItemById(id); @@ -568,21 +647,24 @@ async function getItemById(id) { } async function getAllItems() { const items = await channels.getAllItems(); + return map(items, item => { const { id } = item; const keys = ITEM_KEYS[id]; + return Array.isArray(keys) ? keysToArrayBuffer(keys, item) : item; }); } -async function bulkAddItems(array) { +async function bulkAddItems(array: Array) { const updated = map(array, data => { const { id } = data; const keys = ITEM_KEYS[id]; - return Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; + + return keys && Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; }); await channels.bulkAddItems(updated); } -async function removeItemById(id) { +async function removeItemById(id: string) { await channels.removeItemById(id); } async function removeAllItems() { @@ -591,34 +673,38 @@ async function removeAllItems() { // Sessions -async function createOrUpdateSession(data) { +async function createOrUpdateSession(data: SessionType) { await channels.createOrUpdateSession(data); } -async function createOrUpdateSessions(items) { - await channels.createOrUpdateSessions(items); +async function createOrUpdateSessions(array: Array) { + await channels.createOrUpdateSessions(array); } -async function getSessionById(id) { +async function getSessionById(id: string) { const session = await channels.getSessionById(id); + return session; } -async function getSessionsById(id) { +async function getSessionsById(id: string) { const sessions = await channels.getSessionsById(id); + return sessions; } -async function bulkAddSessions(array) { +async function bulkAddSessions(array: Array) { await channels.bulkAddSessions(array); } -async function removeSessionById(id) { +async function removeSessionById(id: string) { await channels.removeSessionById(id); } -async function removeSessionsById(id) { - await channels.removeSessionsById(id); + +async function removeSessionsByConversation(conversationId: string) { + await channels.removeSessionsByConversation(conversationId); } -async function removeAllSessions(id) { - await channels.removeAllSessions(id); +async function removeAllSessions() { + await channels.removeAllSessions(); } -async function getAllSessions(id) { - const sessions = await channels.getAllSessions(id); +async function getAllSessions() { + const sessions = await channels.getAllSessions(); + return sessions; } @@ -628,23 +714,27 @@ async function getConversationCount() { return channels.getConversationCount(); } -async function saveConversation(data) { +async function saveConversation(data: ConversationType) { await channels.saveConversation(data); } -async function saveConversations(data) { - await channels.saveConversations(data); +async function saveConversations(array: Array) { + await channels.saveConversations(array); } -async function getConversationById(id, { Conversation }) { +async function getConversationById( + id: string, + { Conversation }: { Conversation: BackboneConversationModelType } +) { const data = await channels.getConversationById(id); + return new Conversation(data); } -const updateConversationBatcher = createBatcher({ +const updateConversationBatcher = createBatcher({ wait: 500, maxSize: 20, - processBatch: async items => { + processBatch: async (items: Array) => { // We only care about the most recent update for each conversation const byId = groupBy(items, item => item.id); const ids = Object.keys(byId); @@ -654,15 +744,18 @@ const updateConversationBatcher = createBatcher({ }, }); -function updateConversation(id, data) { +function updateConversation(data: ConversationType) { updateConversationBatcher.add(data); } -async function updateConversations(data) { - await channels.updateConversations(data); +async function updateConversations(array: Array) { + await channels.updateConversations(array); } -async function removeConversation(id, { Conversation }) { +async function removeConversation( + id: string, + { Conversation }: { Conversation: BackboneConversationModelType } +) { const existing = await getConversationById(id, { Conversation }); // Note: It's important to have a fully database-hydrated model to delete here because @@ -674,66 +767,91 @@ async function removeConversation(id, { Conversation }) { } // Note: this method will not clean up external files, just delete from SQL -async function _removeConversations(ids) { +async function _removeConversations(ids: Array) { await channels.removeConversation(ids); } -async function getAllConversations({ ConversationCollection }) { +async function getAllConversations({ + ConversationCollection, +}: { + ConversationCollection: BackboneConversationCollectionType; +}) { const conversations = await channels.getAllConversations(); const collection = new ConversationCollection(); collection.add(conversations); + return collection; } async function getAllConversationIds() { const ids = await channels.getAllConversationIds(); + return ids; } -async function getAllPrivateConversations({ ConversationCollection }) { +async function getAllPrivateConversations({ + ConversationCollection, +}: { + ConversationCollection: BackboneConversationCollectionType; +}) { const conversations = await channels.getAllPrivateConversations(); const collection = new ConversationCollection(); collection.add(conversations); + return collection; } -async function getAllGroupsInvolvingId(id, { ConversationCollection }) { +async function getAllGroupsInvolvingId( + id: string, + { + ConversationCollection, + }: { + ConversationCollection: BackboneConversationCollectionType; + } +) { const conversations = await channels.getAllGroupsInvolvingId(id); const collection = new ConversationCollection(); collection.add(conversations); + return collection; } -async function searchConversations(query) { +async function searchConversations(query: string) { const conversations = await channels.searchConversations(query); + return conversations; } -function handleSearchMessageJSON(messages) { +function handleSearchMessageJSON(messages: Array) { return messages.map(message => ({ ...JSON.parse(message.json), snippet: message.snippet, })); } -async function searchMessages(query, { limit } = {}) { +async function searchMessages( + query: string, + { limit }: { limit?: number } = {} +) { const messages = await channels.searchMessages(query, { limit }); + return handleSearchMessageJSON(messages); } async function searchMessagesInConversation( - query, - conversationId, - { limit } = {} + query: string, + conversationId: string, + { limit }: { limit?: number } = {} ) { const messages = await channels.searchMessagesInConversation( query, conversationId, { limit } ); + return handleSearchMessageJSON(messages); } @@ -743,54 +861,30 @@ async function getMessageCount() { return channels.getMessageCount(); } -async function saveMessage(data, { forceSave, Message } = {}) { +async function saveMessage( + data: MessageType, + { + forceSave, + Message, + }: { forceSave?: boolean; Message: BackboneMessageModelType } +) { const id = await channels.saveMessage(_cleanData(data), { forceSave }); Message.updateTimers(); + return id; } -async function saveLegacyMessage(data) { - const db = await window.Whisper.Database.open(); - try { - await new Promise((resolve, reject) => { - const transaction = db.transaction('messages', 'readwrite'); - - transaction.onerror = () => { - window.Whisper.Database.handleDOMException( - 'saveLegacyMessage transaction error', - transaction.error, - reject - ); - }; - transaction.oncomplete = resolve; - - const store = transaction.objectStore('messages'); - - if (!data.id) { - // eslint-disable-next-line no-param-reassign - data.id = window.getGuid(); - } - - const request = store.put(data, data.id); - request.onsuccess = resolve; - request.onerror = () => { - window.Whisper.Database.handleDOMException( - 'saveLegacyMessage request error', - request.error, - reject - ); - }; - }); - } finally { - db.close(); - } -} - -async function saveMessages(arrayOfMessages, { forceSave } = {}) { +async function saveMessages( + arrayOfMessages: Array, + { forceSave }: { forceSave?: boolean } = {} +) { await channels.saveMessages(_cleanData(arrayOfMessages), { forceSave }); } -async function removeMessage(id, { Message }) { +async function removeMessage( + id: string, + { Message }: { Message: BackboneMessageModelType } +) { const message = await getMessageById(id, { Message }); // Note: It's important to have a fully database-hydrated model to delete here because @@ -802,11 +896,14 @@ async function removeMessage(id, { Message }) { } // Note: this method will not clean up external files, just delete from SQL -async function _removeMessages(ids) { +async function _removeMessages(ids: Array) { await channels.removeMessage(ids); } -async function getMessageById(id, { Message }) { +async function getMessageById( + id: string, + { Message }: { Message: BackboneMessageModelType } +) { const message = await channels.getMessageById(id); if (!message) { return null; @@ -816,20 +913,35 @@ async function getMessageById(id, { Message }) { } // For testing only -async function getAllMessages({ MessageCollection }) { - const messages = await channels.getAllMessages(); +async function _getAllMessages({ + MessageCollection, +}: { + MessageCollection: BackboneMessageCollectionType; +}) { + const messages = await channels._getAllMessages(); + return new MessageCollection(messages); } async function getAllMessageIds() { const ids = await channels.getAllMessageIds(); + return ids; } async function getMessageBySender( - // eslint-disable-next-line camelcase - { source, sourceUuid, sourceDevice, sent_at }, - { Message } + { + source, + sourceUuid, + sourceDevice, + sent_at, + }: { + source: string; + sourceUuid: string; + sourceDevice: string; + sent_at: number; + }, + { Message }: { Message: BackboneMessageModelType } ) { const messages = await channels.getMessageBySender({ source, @@ -844,18 +956,30 @@ async function getMessageBySender( return new Message(messages[0]); } -async function getUnreadByConversation(conversationId, { MessageCollection }) { +async function getUnreadByConversation( + conversationId: string, + { MessageCollection }: { MessageCollection: BackboneMessageCollectionType } +) { const messages = await channels.getUnreadByConversation(conversationId); + return new MessageCollection(messages); } -function handleMessageJSON(messages) { +function handleMessageJSON(messages: Array) { return messages.map(message => JSON.parse(message.json)); } async function getOlderMessagesByConversation( - conversationId, - { limit = 100, receivedAt = Number.MAX_VALUE, MessageCollection } + conversationId: string, + { + limit = 100, + receivedAt = Number.MAX_VALUE, + MessageCollection, + }: { + limit?: number; + receivedAt?: number; + MessageCollection: BackboneMessageCollectionType; + } ) { const messages = await channels.getOlderMessagesByConversation( conversationId, @@ -868,8 +992,16 @@ async function getOlderMessagesByConversation( return new MessageCollection(handleMessageJSON(messages)); } async function getNewerMessagesByConversation( - conversationId, - { limit = 100, receivedAt = 0, MessageCollection } + conversationId: string, + { + limit = 100, + receivedAt = 0, + MessageCollection, + }: { + limit?: number; + receivedAt?: number; + MessageCollection: BackboneMessageCollectionType; + } ) { const messages = await channels.getNewerMessagesByConversation( conversationId, @@ -881,22 +1013,22 @@ async function getNewerMessagesByConversation( return new MessageCollection(handleMessageJSON(messages)); } -async function getMessageMetricsForConversation(conversationId) { +async function getMessageMetricsForConversation(conversationId: string) { const result = await channels.getMessageMetricsForConversation( conversationId ); + return result; } async function removeAllMessagesInConversation( - conversationId, - { MessageCollection } + conversationId: string, + { MessageCollection }: { MessageCollection: BackboneMessageCollectionType } ) { let messages; do { // Yes, we really want the await in the loop. We're deleting 100 at a // time so we don't use too much memory. - // eslint-disable-next-line no-await-in-loop messages = await getOlderMessagesByConversation(conversationId, { limit: 100, MessageCollection, @@ -906,39 +1038,66 @@ async function removeAllMessagesInConversation( return; } - const ids = messages.map(message => message.id); + const ids = messages.map((message: BackboneMessageModelType) => message.id); // Note: It's very important that these models are fully hydrated because // we need to delete all associated on-disk files along with the database delete. - // eslint-disable-next-line no-await-in-loop - await Promise.all(messages.map(message => message.cleanup())); + await Promise.all( + messages.map((message: BackboneMessageModelType) => message.cleanup()) + ); - // eslint-disable-next-line no-await-in-loop await channels.removeMessage(ids); } while (messages.length > 0); } -async function getMessagesBySentAt(sentAt, { MessageCollection }) { +async function getMessagesBySentAt( + sentAt: number, + { MessageCollection }: { MessageCollection: BackboneMessageCollectionType } +) { const messages = await channels.getMessagesBySentAt(sentAt); + return new MessageCollection(messages); } -async function getExpiredMessages({ MessageCollection }) { +async function getExpiredMessages({ + MessageCollection, +}: { + MessageCollection: BackboneMessageCollectionType; +}) { const messages = await channels.getExpiredMessages(); + return new MessageCollection(messages); } -async function getOutgoingWithoutExpiresAt({ MessageCollection }) { +async function getOutgoingWithoutExpiresAt({ + MessageCollection, +}: { + MessageCollection: BackboneMessageCollectionType; +}) { const messages = await channels.getOutgoingWithoutExpiresAt(); + return new MessageCollection(messages); } -async function getNextExpiringMessage({ MessageCollection }) { - const messages = await channels.getNextExpiringMessage(); - return new MessageCollection(messages); +async function getNextExpiringMessage({ + Message, +}: { + Message: BackboneMessageModelType; +}) { + const message = await channels.getNextExpiringMessage(); + + if (message) { + return new Message(message); + } + + return null; } -async function getNextTapToViewMessageToAgeOut({ Message }) { +async function getNextTapToViewMessageToAgeOut({ + Message, +}: { + Message: BackboneMessageModelType; +}) { const message = await channels.getNextTapToViewMessageToAgeOut(); if (!message) { return null; @@ -946,8 +1105,13 @@ async function getNextTapToViewMessageToAgeOut({ Message }) { return new Message(message); } -async function getTapToViewMessagesNeedingErase({ MessageCollection }) { +async function getTapToViewMessagesNeedingErase({ + MessageCollection, +}: { + MessageCollection: BackboneMessageCollectionType; +}) { const messages = await channels.getTapToViewMessagesNeedingErase(); + return new MessageCollection(messages); } @@ -961,32 +1125,39 @@ async function getAllUnprocessed() { return channels.getAllUnprocessed(); } -async function getUnprocessedById(id) { +async function getUnprocessedById(id: string) { return channels.getUnprocessedById(id); } -async function saveUnprocessed(data, { forceSave } = {}) { +async function saveUnprocessed( + data: UnprocessedType, + { forceSave }: { forceSave?: boolean } = {} +) { const id = await channels.saveUnprocessed(_cleanData(data), { forceSave }); + return id; } -async function saveUnprocesseds(arrayOfUnprocessed, { forceSave } = {}) { +async function saveUnprocesseds( + arrayOfUnprocessed: Array, + { forceSave }: { forceSave?: boolean } = {} +) { await channels.saveUnprocesseds(_cleanData(arrayOfUnprocessed), { forceSave, }); } -async function updateUnprocessedAttempts(id, attempts) { +async function updateUnprocessedAttempts(id: string, attempts: number) { await channels.updateUnprocessedAttempts(id, attempts); } -async function updateUnprocessedWithData(id, data) { +async function updateUnprocessedWithData(id: string, data: UnprocessedType) { await channels.updateUnprocessedWithData(id, data); } -async function updateUnprocessedsWithData(items) { - await channels.updateUnprocessedsWithData(items); +async function updateUnprocessedsWithData(array: Array) { + await channels.updateUnprocessedsWithData(array); } -async function removeUnprocessed(id) { +async function removeUnprocessed(id: string) { await channels.removeUnprocessed(id); } @@ -996,19 +1167,22 @@ async function removeAllUnprocessed() { // Attachment downloads -async function getNextAttachmentDownloadJobs(limit) { - return channels.getNextAttachmentDownloadJobs(limit); +async function getNextAttachmentDownloadJobs( + limit?: number, + options?: { timestamp?: number } +) { + return channels.getNextAttachmentDownloadJobs(limit, options); } -async function saveAttachmentDownloadJob(job) { +async function saveAttachmentDownloadJob(job: AttachmentDownloadJobType) { await channels.saveAttachmentDownloadJob(_cleanData(job)); } -async function setAttachmentDownloadJobPending(id, pending) { +async function setAttachmentDownloadJobPending(id: string, pending: boolean) { await channels.setAttachmentDownloadJobPending(id, pending); } async function resetAttachmentDownloadPending() { await channels.resetAttachmentDownloadPending(); } -async function removeAttachmentDownloadJob(id) { +async function removeAttachmentDownloadJob(id: string) { await channels.removeAttachmentDownloadJob(id); } async function removeAllAttachmentDownloadJobs() { @@ -1017,47 +1191,64 @@ async function removeAllAttachmentDownloadJobs() { // Stickers -async function createOrUpdateStickerPack(pack) { +async function getStickerCount() { + return channels.getStickerCount(); +} + +async function createOrUpdateStickerPack(pack: StickerPackType) { await channels.createOrUpdateStickerPack(pack); } -async function updateStickerPackStatus(packId, status, options) { +async function updateStickerPackStatus( + packId: string, + status: StickerPackStatusType, + options?: { timestamp: number } +) { await channels.updateStickerPackStatus(packId, status, options); } -async function createOrUpdateSticker(sticker) { +async function createOrUpdateSticker(sticker: StickerType) { await channels.createOrUpdateSticker(sticker); } -async function updateStickerLastUsed(packId, stickerId, timestamp) { +async function updateStickerLastUsed( + packId: string, + stickerId: number, + timestamp: number +) { await channels.updateStickerLastUsed(packId, stickerId, timestamp); } -async function addStickerPackReference(messageId, packId) { +async function addStickerPackReference(messageId: string, packId: string) { await channels.addStickerPackReference(messageId, packId); } -async function deleteStickerPackReference(messageId, packId) { +async function deleteStickerPackReference(messageId: string, packId: string) { const paths = await channels.deleteStickerPackReference(messageId, packId); + return paths; } -async function deleteStickerPack(packId) { +async function deleteStickerPack(packId: string) { const paths = await channels.deleteStickerPack(packId); + return paths; } async function getAllStickerPacks() { const packs = await channels.getAllStickerPacks(); + return packs; } async function getAllStickers() { const stickers = await channels.getAllStickers(); + return stickers; } async function getRecentStickers() { const recentStickers = await channels.getRecentStickers(); + return recentStickers; } // Emojis -async function updateEmojiUsage(shortName) { +async function updateEmojiUsage(shortName: string) { await channels.updateEmojiUsage(shortName); } -async function getRecentEmojis(limit = 32) { +async function getRecentEmojis(limit: number = 32) { return channels.getRecentEmojis(limit); } @@ -1090,35 +1281,67 @@ async function removeOtherData() { ]); } -async function callChannel(name) { +async function callChannel(name: string) { return new Promise((resolve, reject) => { ipcRenderer.send(name); - ipcRenderer.once(`${name}-done`, (event, error) => { + ipcRenderer.once(`${name}-done`, (_, error) => { if (error) { - return reject(error); + reject(error); + + return; } - return resolve(); + resolve(); + + return; }); - setTimeout( - () => reject(new Error(`callChannel call to ${name} timed out`)), - DATABASE_UPDATE_TIMEOUT - ); + setTimeout(() => { + reject(new Error(`callChannel call to ${name} timed out`)); + }, DATABASE_UPDATE_TIMEOUT); }); } -// Functions below here return plain JSON instead of Backbone Models +async function getMessagesNeedingUpgrade( + limit: number, + { maxVersion = CURRENT_SCHEMA_VERSION }: { maxVersion: number } +) { + const messages = await channels.getMessagesNeedingUpgrade(limit, { + maxVersion, + }); + + return messages; +} + +async function getMessagesWithVisualMediaAttachments( + conversationId: string, + { limit }: { limit: number } +) { + return channels.getMessagesWithVisualMediaAttachments(conversationId, { + limit, + }); +} + +async function getMessagesWithFileAttachments( + conversationId: string, + { limit }: { limit: number } +) { + return channels.getMessagesWithFileAttachments(conversationId, { + limit, + }); +} + +// Legacy IndexedDB Support async function getLegacyMessagesNeedingUpgrade( - limit, - { maxVersion = MessageType.CURRENT_SCHEMA_VERSION } -) { + limit: number, + { maxVersion = CURRENT_SCHEMA_VERSION }: { maxVersion: number } +): Promise { const db = await window.Whisper.Database.open(); try { return new Promise((resolve, reject) => { const transaction = db.transaction('messages', 'readonly'); - const messages = []; + const messages: Array = []; transaction.onerror = () => { window.Whisper.Database.handleDOMException( @@ -1139,6 +1362,7 @@ async function getLegacyMessagesNeedingUpgrade( let count = 0; request.onsuccess = event => { + // @ts-ignore const cursor = event.target.result; if (cursor) { @@ -1165,28 +1389,38 @@ async function getLegacyMessagesNeedingUpgrade( } } -async function getMessagesNeedingUpgrade( - limit, - { maxVersion = MessageType.CURRENT_SCHEMA_VERSION } -) { - const messages = await channels.getMessagesNeedingUpgrade(limit, { - maxVersion, - }); +async function saveLegacyMessage(data: MessageType) { + const db = await window.Whisper.Database.open(); + try { + await new Promise((resolve, reject) => { + const transaction = db.transaction('messages', 'readwrite'); - return messages; -} + transaction.onerror = () => { + window.Whisper.Database.handleDOMException( + 'saveLegacyMessage transaction error', + transaction.error, + reject + ); + }; + transaction.oncomplete = resolve; -async function getMessagesWithVisualMediaAttachments( - conversationId, - { limit } -) { - return channels.getMessagesWithVisualMediaAttachments(conversationId, { - limit, - }); -} + const store = transaction.objectStore('messages'); -async function getMessagesWithFileAttachments(conversationId, { limit }) { - return channels.getMessagesWithFileAttachments(conversationId, { - limit, - }); + if (!data.id) { + data.id = getGuid(); + } + + const request = store.put(data, data.id); + request.onsuccess = resolve; + request.onerror = () => { + window.Whisper.Database.handleDOMException( + 'saveLegacyMessage request error', + request.error, + reject + ); + }; + }); + } finally { + db.close(); + } } diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts new file mode 100644 index 000000000..193e4226c --- /dev/null +++ b/ts/sql/Interface.ts @@ -0,0 +1,382 @@ +import { LocaleMessagesType } from '../types/I18N'; + +export type AttachmentDownloadJobType = any; +export type ConverationMetricsType = any; +export type ConversationType = any; +export type EmojiType = any; +export type IdentityKeyType = any; +export type ItemType = any; +export type MessageType = any; +export type MessageTypeUnhydrated = any; +export type PreKeyType = any; +export type SearchResultMessageType = any; +export type SessionType = any; +export type SignedPreKeyType = any; +export type StickerPackStatusType = string; +export type StickerPackType = any; +export type StickerType = any; +export type UnprocessedType = any; + +export type BackboneConversationModelType = any; +export type BackboneConversationCollectionType = any; +export type BackboneMessageModelType = any; +export type BackboneMessageCollectionType = any; + +export interface DataInterface { + close: () => Promise; + removeDB: () => Promise; + removeIndexedDBFiles: () => Promise; + + createOrUpdateIdentityKey: (data: IdentityKeyType) => Promise; + getIdentityKeyById: (id: string) => Promise; + bulkAddIdentityKeys: (array: Array) => Promise; + removeIdentityKeyById: (id: string) => Promise; + removeAllIdentityKeys: () => Promise; + getAllIdentityKeys: () => Promise>; + + createOrUpdatePreKey: (data: PreKeyType) => Promise; + getPreKeyById: (id: number) => Promise; + bulkAddPreKeys: (array: Array) => Promise; + removePreKeyById: (id: number) => Promise; + removeAllPreKeys: () => Promise; + getAllPreKeys: () => Promise>; + + createOrUpdateSignedPreKey: (data: SignedPreKeyType) => Promise; + getSignedPreKeyById: (id: number) => Promise; + bulkAddSignedPreKeys: (array: Array) => Promise; + removeSignedPreKeyById: (id: number) => Promise; + removeAllSignedPreKeys: () => Promise; + getAllSignedPreKeys: () => Promise>; + + createOrUpdateItem: (data: ItemType) => Promise; + getItemById: (id: string) => Promise; + bulkAddItems: (array: Array) => Promise; + removeItemById: (id: string) => Promise; + removeAllItems: () => Promise; + getAllItems: () => Promise>; + + createOrUpdateSession: (data: SessionType) => Promise; + createOrUpdateSessions: (array: Array) => Promise; + getSessionById: (id: string) => Promise; + getSessionsById: (conversationId: string) => Promise>; + bulkAddSessions: (array: Array) => Promise; + removeSessionById: (id: string) => Promise; + removeSessionsByConversation: (conversationId: string) => Promise; + removeAllSessions: () => Promise; + getAllSessions: () => Promise>; + + getConversationCount: () => Promise; + saveConversation: (data: ConversationType) => Promise; + saveConversations: (array: Array) => Promise; + updateConversations: (array: Array) => Promise; + getAllConversationIds: () => Promise>; + + searchConversations: ( + query: string, + options?: { limit?: number } + ) => Promise>; + searchMessages: ( + query: string, + options?: { limit?: number } + ) => Promise>; + searchMessagesInConversation: ( + query: string, + conversationId: string, + options?: { limit?: number } + ) => Promise>; + + getMessageCount: () => Promise; + saveMessages: ( + arrayOfMessages: Array, + options: { forceSave?: boolean } + ) => Promise; + getAllMessageIds: () => Promise>; + getMessageMetricsForConversation: ( + conversationId: string + ) => Promise; + + getUnprocessedCount: () => Promise; + getAllUnprocessed: () => Promise>; + saveUnprocessed: ( + data: UnprocessedType, + options?: { forceSave?: boolean } + ) => Promise; + updateUnprocessedAttempts: (id: string, attempts: number) => Promise; + updateUnprocessedWithData: ( + id: string, + data: UnprocessedType + ) => Promise; + updateUnprocessedsWithData: (array: Array) => Promise; + getUnprocessedById: (id: string) => Promise; + saveUnprocesseds: ( + arrayOfUnprocessed: Array, + options?: { forceSave?: boolean } + ) => Promise; + removeUnprocessed: (id: string) => Promise; + removeAllUnprocessed: () => Promise; + + getNextAttachmentDownloadJobs: ( + limit?: number, + options?: { timestamp?: number } + ) => Promise>; + saveAttachmentDownloadJob: (job: AttachmentDownloadJobType) => Promise; + setAttachmentDownloadJobPending: ( + id: string, + pending: boolean + ) => Promise; + resetAttachmentDownloadPending: () => Promise; + removeAttachmentDownloadJob: (id: string) => Promise; + removeAllAttachmentDownloadJobs: () => Promise; + + createOrUpdateStickerPack: (pack: StickerPackType) => Promise; + updateStickerPackStatus: ( + id: string, + status: StickerPackStatusType, + options?: { timestamp: number } + ) => Promise; + createOrUpdateSticker: (sticker: StickerType) => Promise; + updateStickerLastUsed: ( + packId: string, + stickerId: number, + lastUsed: number + ) => Promise; + addStickerPackReference: (messageId: string, packId: string) => Promise; + deleteStickerPackReference: ( + messageId: string, + packId: string + ) => Promise>; + getStickerCount: () => Promise; + deleteStickerPack: (packId: string) => Promise>; + getAllStickerPacks: () => Promise>; + getAllStickers: () => Promise>; + getRecentStickers: (options?: { + limit?: number; + }) => Promise>; + + updateEmojiUsage: (shortName: string, timeUsed?: number) => Promise; + getRecentEmojis: (limit?: number) => Promise>; + + removeAll: () => Promise; + removeAllConfiguration: () => Promise; + + getMessagesNeedingUpgrade: ( + limit: number, + options: { maxVersion: number } + ) => Promise>; + getMessagesWithVisualMediaAttachments: ( + conversationId: string, + options: { limit: number } + ) => Promise>; + getMessagesWithFileAttachments: ( + conversationId: string, + options: { limit: number } + ) => Promise>; +} + +// The reason for client/server divergence is the need to inject Backbone models and +// collections into data calls so those are the objects returned. This was necessary in +// July 2018 when creating the Data API as a drop-in replacement for previous database +// requests via ORM. + +// Note: It is extremely important that items are duplicated between these two. Client.js +// loops over all of its local functions to generate the server-side IPC-based API. + +export type ServerInterface = DataInterface & { + getAllConversations: () => Promise>; + getAllGroupsInvolvingId: (id: string) => Promise>; + getAllPrivateConversations: () => Promise>; + getConversationById: (id: string) => Promise; + getExpiredMessages: () => Promise>; + getMessageById: (id: string) => Promise; + getMessageBySender: (options: { + source: string; + sourceUuid: string; + sourceDevice: string; + sent_at: number; + }) => Promise>; + getMessagesBySentAt: (sentAt: number) => Promise>; + getOlderMessagesByConversation: ( + conversationId: string, + options?: { limit?: number; receivedAt?: number } + ) => Promise>; + getNewerMessagesByConversation: ( + conversationId: string, + options?: { limit?: number; receivedAt?: number } + ) => Promise>; + getNextExpiringMessage: () => Promise; + getNextTapToViewMessageToAgeOut: () => Promise; + getOutgoingWithoutExpiresAt: () => Promise>; + getTapToViewMessagesNeedingErase: () => Promise>; + getUnreadByConversation: ( + conversationId: string + ) => Promise>; + removeConversation: (id: Array | string) => Promise; + removeMessage: (id: Array | string) => Promise; + saveMessage: ( + data: MessageType, + options: { forceSave?: boolean } + ) => Promise; + updateConversation: (data: ConversationType) => Promise; + + // For testing only + _getAllMessages: () => Promise>; + + // Server-only + + initialize: (options: { + configDir: string; + key: string; + messages: LocaleMessagesType; + }) => Promise; + + removeKnownAttachments: ( + allAttachments: Array + ) => Promise>; + removeKnownStickers: (allStickers: Array) => Promise>; + removeKnownDraftAttachments: ( + allStickers: Array + ) => Promise>; +}; + +export type ClientInterface = DataInterface & { + getAllConversations: ({ + ConversationCollection, + }: { + ConversationCollection: BackboneConversationCollectionType; + }) => Promise>; + getAllGroupsInvolvingId: ( + id: string, + { + ConversationCollection, + }: { + ConversationCollection: BackboneConversationCollectionType; + } + ) => Promise>; + getAllPrivateConversations: ({ + ConversationCollection, + }: { + ConversationCollection: BackboneConversationCollectionType; + }) => Promise>; + getConversationById: ( + id: string, + { Conversation }: { Conversation: BackboneConversationModelType } + ) => Promise; + getExpiredMessages: ({ + MessageCollection, + }: { + MessageCollection: BackboneMessageCollectionType; + }) => Promise>; + getMessageById: ( + id: string, + { Message }: { Message: BackboneMessageModelType } + ) => Promise; + getMessageBySender: ( + options: { + source: string; + sourceUuid: string; + sourceDevice: string; + sent_at: number; + }, + { Message }: { Message: BackboneMessageModelType } + ) => Promise>; + getMessagesBySentAt: ( + sentAt: number, + { MessageCollection }: { MessageCollection: BackboneMessageCollectionType } + ) => Promise>; + getOlderMessagesByConversation: ( + conversationId: string, + options: { + limit?: number; + receivedAt?: number; + MessageCollection: BackboneMessageCollectionType; + } + ) => Promise>; + getNewerMessagesByConversation: ( + conversationId: string, + options: { + limit?: number; + receivedAt?: number; + MessageCollection: BackboneMessageCollectionType; + } + ) => Promise>; + getNextExpiringMessage: ({ + Message, + }: { + Message: BackboneMessageModelType; + }) => Promise; + getNextTapToViewMessageToAgeOut: ({ + Message, + }: { + Message: BackboneMessageModelType; + }) => Promise; + getOutgoingWithoutExpiresAt: ({ + MessageCollection, + }: { + MessageCollection: BackboneMessageCollectionType; + }) => Promise>; + getTapToViewMessagesNeedingErase: ({ + MessageCollection, + }: { + MessageCollection: BackboneMessageCollectionType; + }) => Promise>; + getUnreadByConversation: ( + conversationId: string, + { MessageCollection }: { MessageCollection: BackboneMessageCollectionType } + ) => Promise>; + removeConversation: ( + id: string, + { Conversation }: { Conversation: BackboneConversationModelType } + ) => Promise; + removeMessage: ( + id: string, + { Message }: { Message: BackboneMessageModelType } + ) => Promise; + saveMessage: ( + data: MessageType, + options: { forceSave?: boolean; Message: BackboneMessageModelType } + ) => Promise; + updateConversation: (data: ConversationType) => void; + + // Test-only + + _getAllMessages: ({ + MessageCollection, + }: { + MessageCollection: BackboneMessageCollectionType; + }) => Promise>; + + // Client-side only + + shutdown: () => Promise; + removeAllMessagesInConversation: ( + conversationId: string, + { MessageCollection }: { MessageCollection: BackboneMessageCollectionType } + ) => Promise; + removeOtherData: () => Promise; + cleanupOrphanedAttachments: () => Promise; + ensureFilePermissions: () => Promise; + + getLegacyMessagesNeedingUpgrade: ( + limit: number, + options: { maxVersion: number } + ) => Promise>; + saveLegacyMessage: (data: MessageType) => Promise; + + // Client-side only, and test-only + + _removeConversations: (ids: Array) => Promise; + _removeMessages: (ids: Array) => Promise; + _cleanData: (data: any, path?: string) => any; + _jobs: { [id: string]: ClientJobType }; +}; + +export type ClientJobType = { + fnName: string; + start: number; + resolve?: Function; + reject?: Function; + + // Only in DEBUG mode + complete?: boolean; + args?: Array; +}; diff --git a/app/sql.js b/ts/sql/Server.ts similarity index 81% rename from app/sql.js rename to ts/sql/Server.ts index d0f47aec9..a6a163ac5 100644 --- a/app/sql.js +++ b/ts/sql/Server.ts @@ -1,16 +1,21 @@ -const { join } = require('path'); -const mkdirp = require('mkdirp'); -const rimraf = require('rimraf'); -const Queue = require('p-queue').default; -const sql = require('@journeyapps/sqlcipher'); -const { app, dialog, clipboard } = require('electron'); -const { redactAll } = require('../js/modules/privacy'); -const { remove: removeUserConfig } = require('./user_config'); -const { combineNames } = require('../ts/util/combineNames'); +// tslint:disable no-backbone-get-set-outside-model no-console no-default-export no-unnecessary-local-variable -const pify = require('pify'); -const uuidv4 = require('uuid/v4'); -const { +import { join } from 'path'; +import mkdirp from 'mkdirp'; +import rimraf from 'rimraf'; +import PQueue from 'p-queue'; +import sql from '@journeyapps/sqlcipher'; +import { app, clipboard, dialog } from 'electron'; +import { redactAll } from '../../js/modules/privacy'; +import { remove as removeUserConfig } from '../../app/user_config'; +import { combineNames } from '../util/combineNames'; + +import { LocaleMessagesType } from '../types/I18N'; + +import pify from 'pify'; +import { v4 as generateUUID } from 'uuid'; +import { + Dictionary, forEach, fromPairs, isNumber, @@ -20,10 +25,36 @@ const { last, map, pick, -} = require('lodash'); +} from 'lodash'; -module.exports = { - initialize, +import { + AttachmentDownloadJobType, + ConversationType, + IdentityKeyType, + ItemType, + MessageType, + PreKeyType, + SearchResultMessageType, + ServerInterface, + SessionType, + SignedPreKeyType, + StickerPackStatusType, + StickerPackType, + StickerType, + UnprocessedType, +} from './Interface'; + +declare global { + interface Function { + needsSerial?: boolean; + } +} + +// Because we can't force this module to conform to an interface, we narrow our exports +// to this one default export, which does conform to the interface. +// Note: In Javascript, you need to access the .default property when requiring it +// https://github.com/microsoft/TypeScript/issues/420 +const dataInterface: ServerInterface = { close, removeDB, removeIndexedDBFiles, @@ -62,7 +93,7 @@ module.exports = { getSessionsById, bulkAddSessions, removeSessionById, - removeSessionsById, + removeSessionsByConversation, removeAllSessions, getAllSessions, @@ -89,7 +120,7 @@ module.exports = { getUnreadByConversation, getMessageBySender, getMessageById, - getAllMessages, + _getAllMessages, getAllMessageIds, getMessagesBySentAt, getExpiredMessages, @@ -125,6 +156,7 @@ module.exports = { updateStickerLastUsed, addStickerPackReference, deleteStickerPackReference, + getStickerCount, deleteStickerPack, getAllStickerPacks, getAllStickers, @@ -140,74 +172,99 @@ module.exports = { getMessagesWithVisualMediaAttachments, getMessagesWithFileAttachments, + // Server-only + + initialize, + removeKnownAttachments, removeKnownStickers, removeKnownDraftAttachments, }; -function generateUUID() { - return uuidv4(); -} +export default dataInterface; -function objectToJSON(data) { +function objectToJSON(data: any) { return JSON.stringify(data); } -function jsonToObject(json) { +function jsonToObject(json: string): any { return JSON.parse(json); } -async function openDatabase(filePath) { +async function openDatabase(filePath: string): Promise { return new Promise((resolve, reject) => { - const instance = new sql.Database(filePath, error => { + let instance: sql.Database | undefined; + const callback = (error: Error | null) => { if (error) { - return reject(error); + reject(error); + + return; + } + if (!instance) { + reject(new Error('openDatabase: Unable to get database instance')); + + return; } - return resolve(instance); - }); + resolve(instance); + + return; + }; + + instance = new sql.Database(filePath, callback); }); } -function promisify(rawInstance) { - /* eslint-disable no-param-reassign */ - rawInstance.close = pify(rawInstance.close.bind(rawInstance)); - rawInstance.run = pify(rawInstance.run.bind(rawInstance)); - rawInstance.get = pify(rawInstance.get.bind(rawInstance)); - rawInstance.all = pify(rawInstance.all.bind(rawInstance)); - rawInstance.each = pify(rawInstance.each.bind(rawInstance)); - rawInstance.exec = pify(rawInstance.exec.bind(rawInstance)); - rawInstance.prepare = pify(rawInstance.prepare.bind(rawInstance)); - /* eslint-enable */ +type PromisifiedSQLDatabase = { + close: () => Promise; + run: (statement: string, params?: { [key: string]: any }) => Promise; + get: (statement: string, params?: { [key: string]: any }) => Promise; + all: ( + statement: string, + params?: { [key: string]: any } + ) => Promise>; +}; - return rawInstance; +function promisify(rawInstance: sql.Database): PromisifiedSQLDatabase { + return { + close: pify(rawInstance.close.bind(rawInstance)), + run: pify(rawInstance.run.bind(rawInstance)), + get: pify(rawInstance.get.bind(rawInstance)), + all: pify(rawInstance.all.bind(rawInstance)), + }; } -async function getSQLiteVersion(instance) { +async function getSQLiteVersion(instance: PromisifiedSQLDatabase) { const row = await instance.get('select sqlite_version() AS sqlite_version'); + return row.sqlite_version; } -async function getSchemaVersion(instance) { +async function getSchemaVersion(instance: PromisifiedSQLDatabase) { const row = await instance.get('PRAGMA schema_version;'); + return row.schema_version; } -async function setUserVersion(instance, version) { +async function setUserVersion( + instance: PromisifiedSQLDatabase, + version: number +) { if (!isNumber(version)) { throw new Error(`setUserVersion: version ${version} is not a number`); } await instance.get(`PRAGMA user_version = ${version};`); } -async function keyDatabase(instance, key) { +async function keyDatabase(instance: PromisifiedSQLDatabase, key: string) { // https://www.zetetic.net/sqlcipher/sqlcipher-api/#key await instance.run(`PRAGMA key = "x'${key}'";`); } -async function getUserVersion(instance) { +async function getUserVersion(instance: PromisifiedSQLDatabase) { const row = await instance.get('PRAGMA user_version;'); + return row.user_version; } -async function getSQLCipherVersion(instance) { +async function getSQLCipherVersion(instance: PromisifiedSQLDatabase) { const row = await instance.get('PRAGMA cipher_version;'); try { return row.cipher_version; @@ -216,7 +273,7 @@ async function getSQLCipherVersion(instance) { } } -async function getSQLCipherIntegrityCheck(instance) { +async function getSQLCipherIntegrityCheck(instance: PromisifiedSQLDatabase) { const row = await instance.get('PRAGMA cipher_integrity_check;'); if (row) { return row.cipher_integrity_check; @@ -225,7 +282,7 @@ async function getSQLCipherIntegrityCheck(instance) { return null; } -async function getSQLIntegrityCheck(instance) { +async function getSQLIntegrityCheck(instance: PromisifiedSQLDatabase) { const row = await instance.get('PRAGMA integrity_check;'); if (row && row.integrity_check !== 'ok') { return row.integrity_check; @@ -234,7 +291,7 @@ async function getSQLIntegrityCheck(instance) { return null; } -async function migrateSchemaVersion(instance) { +async function migrateSchemaVersion(instance: PromisifiedSQLDatabase) { const userVersion = await getUserVersion(instance); if (userVersion > 0) { return; @@ -249,14 +306,14 @@ async function migrateSchemaVersion(instance) { await setUserVersion(instance, newUserVersion); } -async function openAndMigrateDatabase(filePath, key) { - let promisified; +async function openAndMigrateDatabase(filePath: string, key: string) { + let promisified: PromisifiedSQLDatabase | undefined; // First, we try to open the database without any cipher changes try { const instance = await openDatabase(filePath); promisified = promisify(instance); - keyDatabase(promisified, key); + await keyDatabase(promisified, key); await migrateSchemaVersion(promisified); @@ -270,9 +327,9 @@ async function openAndMigrateDatabase(filePath, key) { // If that fails, we try to open the database with 3.x compatibility to extract the // user_version (previously stored in schema_version, blown away by cipher_migrate). - const instance = await openDatabase(filePath); - promisified = promisify(instance); - keyDatabase(promisified, key); + const instance1 = await openDatabase(filePath); + promisified = promisify(instance1); + await keyDatabase(promisified, key); // https://www.zetetic.net/blog/2018/11/30/sqlcipher-400-release/#compatability-sqlcipher-4-0-0 await promisified.run('PRAGMA cipher_compatibility = 3;'); @@ -283,14 +340,18 @@ async function openAndMigrateDatabase(filePath, key) { // migrate to the latest ciphers after we've modified the defaults. const instance2 = await openDatabase(filePath); promisified = promisify(instance2); - keyDatabase(promisified, key); + await keyDatabase(promisified, key); await promisified.run('PRAGMA cipher_migrate;'); + return promisified; } const INVALID_KEY = /[^0-9A-Fa-f]/; -async function openAndSetUpSQLCipher(filePath, { key }) { +async function openAndSetUpSQLCipher( + filePath: string, + { key }: { key: string } +) { const match = INVALID_KEY.exec(key); if (match) { throw new Error(`setupSQLCipher: key '${key}' is not valid`); @@ -304,7 +365,10 @@ async function openAndSetUpSQLCipher(filePath, { key }) { return instance; } -async function updateToSchemaVersion1(currentVersion, instance) { +async function updateToSchemaVersion1( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 1) { return; } @@ -394,7 +458,10 @@ async function updateToSchemaVersion1(currentVersion, instance) { } } -async function updateToSchemaVersion2(currentVersion, instance) { +async function updateToSchemaVersion2( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 2) { return; } @@ -442,7 +509,10 @@ async function updateToSchemaVersion2(currentVersion, instance) { } } -async function updateToSchemaVersion3(currentVersion, instance) { +async function updateToSchemaVersion3( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 3) { return; } @@ -477,7 +547,10 @@ async function updateToSchemaVersion3(currentVersion, instance) { } } -async function updateToSchemaVersion4(currentVersion, instance) { +async function updateToSchemaVersion4( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 4) { return; } @@ -518,7 +591,10 @@ async function updateToSchemaVersion4(currentVersion, instance) { } } -async function updateToSchemaVersion6(currentVersion, instance) { +async function updateToSchemaVersion6( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 6) { return; } @@ -582,7 +658,10 @@ async function updateToSchemaVersion6(currentVersion, instance) { } } -async function updateToSchemaVersion7(currentVersion, instance) { +async function updateToSchemaVersion7( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 7) { return; } @@ -623,7 +702,10 @@ async function updateToSchemaVersion7(currentVersion, instance) { } } -async function updateToSchemaVersion8(currentVersion, instance) { +async function updateToSchemaVersion8( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 8) { return; } @@ -693,7 +775,10 @@ async function updateToSchemaVersion8(currentVersion, instance) { } } -async function updateToSchemaVersion9(currentVersion, instance) { +async function updateToSchemaVersion9( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 9) { return; } @@ -726,7 +811,10 @@ async function updateToSchemaVersion9(currentVersion, instance) { } } -async function updateToSchemaVersion10(currentVersion, instance) { +async function updateToSchemaVersion10( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 10) { return; } @@ -791,7 +879,10 @@ async function updateToSchemaVersion10(currentVersion, instance) { } } -async function updateToSchemaVersion11(currentVersion, instance) { +async function updateToSchemaVersion11( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 11) { return; } @@ -810,7 +901,10 @@ async function updateToSchemaVersion11(currentVersion, instance) { } } -async function updateToSchemaVersion12(currentVersion, instance) { +async function updateToSchemaVersion12( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 12) { return; } @@ -875,7 +969,10 @@ async function updateToSchemaVersion12(currentVersion, instance) { } } -async function updateToSchemaVersion13(currentVersion, instance) { +async function updateToSchemaVersion13( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 13) { return; } @@ -897,7 +994,10 @@ async function updateToSchemaVersion13(currentVersion, instance) { } } -async function updateToSchemaVersion14(currentVersion, instance) { +async function updateToSchemaVersion14( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 14) { return; } @@ -925,7 +1025,10 @@ async function updateToSchemaVersion14(currentVersion, instance) { } } -async function updateToSchemaVersion15(currentVersion, instance) { +async function updateToSchemaVersion15( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 15) { return; } @@ -965,7 +1068,10 @@ async function updateToSchemaVersion15(currentVersion, instance) { } } -async function updateToSchemaVersion16(currentVersion, instance) { +async function updateToSchemaVersion16( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 16) { return; } @@ -1046,7 +1152,10 @@ async function updateToSchemaVersion16(currentVersion, instance) { } } -async function updateToSchemaVersion17(currentVersion, instance) { +async function updateToSchemaVersion17( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 17) { return; } @@ -1121,7 +1230,10 @@ async function updateToSchemaVersion17(currentVersion, instance) { } } -async function updateToSchemaVersion18(currentVersion, instance) { +async function updateToSchemaVersion18( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 18) { return; } @@ -1184,7 +1296,10 @@ async function updateToSchemaVersion18(currentVersion, instance) { } } -async function updateToSchemaVersion19(currentVersion, instance) { +async function updateToSchemaVersion19( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 19) { return; } @@ -1214,7 +1329,11 @@ async function updateToSchemaVersion19(currentVersion, instance) { } } -async function updateToSchemaVersion20(currentVersion, instance) { +// tslint:disable-next-line max-func-body-length +async function updateToSchemaVersion20( + currentVersion: number, + instance: PromisifiedSQLDatabase +) { if (currentVersion >= 20) { return; } @@ -1223,7 +1342,7 @@ async function updateToSchemaVersion20(currentVersion, instance) { await instance.run('BEGIN TRANSACTION;'); try { - const migrationJobQueue = new Queue({ concurrency: 10 }); + const migrationJobQueue = new PQueue({ concurrency: 10 }); // The triggers on the messages table slow down this migration // significantly, so we drop them and recreate them later. // Drop triggers @@ -1231,9 +1350,7 @@ async function updateToSchemaVersion20(currentVersion, instance) { 'SELECT * FROM sqlite_master WHERE type = "trigger" AND tbl_name = "messages"' ); - // eslint-disable-next-line no-restricted-syntax for (const trigger of triggers) { - // eslint-disable-next-line no-await-in-loop await instance.run(`DROP TRIGGER ${trigger.name}`); } @@ -1270,19 +1387,15 @@ async function updateToSchemaVersion20(currentVersion, instance) { const maybeInvalidGroups = await instance.all( "SELECT * FROM conversations WHERE type = 'group' AND members IS NULL;" ); - // eslint-disable-next-line no-restricted-syntax for (const group of maybeInvalidGroups) { const json = JSON.parse(group.json); if (!json.members || !json.members.length) { - // eslint-disable-next-line no-await-in-loop await instance.run('DELETE FROM conversations WHERE id = $id;', { $id: json.id, }); - // eslint-disable-next-line no-await-in-loop await instance.run('DELETE FROM messages WHERE conversationId = $id;', { $id: json.id, }); - // eslint-disable-next-line no-await-in-loop // await instance.run('DELETE FROM sessions WHERE conversationId = $id;', { // $id: json.id, // }); @@ -1293,19 +1406,18 @@ async function updateToSchemaVersion20(currentVersion, instance) { const allConversations = await instance.all('SELECT * FROM conversations;'); const allConversationsByOldId = keyBy(allConversations, 'id'); - // eslint-disable-next-line no-restricted-syntax for (const row of allConversations) { const oldId = row.id; const newId = generateUUID(); allConversationsByOldId[oldId].id = newId; - const patchObj = { id: newId }; + const patchObj: any = { id: newId }; if (row.type === 'private') { patchObj.e164 = `+${oldId}`; } else if (row.type === 'group') { patchObj.groupId = oldId; } const patch = JSON.stringify(patchObj); - // eslint-disable-next-line no-await-in-loop + await instance.run( 'UPDATE conversations SET id = $newId, json = JSON_PATCH(json, $patch) WHERE id = $oldId', { @@ -1315,7 +1427,6 @@ async function updateToSchemaVersion20(currentVersion, instance) { } ); const messagePatch = JSON.stringify({ conversationId: newId }); - // eslint-disable-next-line no-await-in-loop await instance.run( 'UPDATE messages SET conversationId = $newId, json = JSON_PATCH(json, $patch) WHERE conversationId = $oldId', { $newId: newId, $oldId: oldId, $patch: messagePatch } @@ -1327,11 +1438,11 @@ async function updateToSchemaVersion20(currentVersion, instance) { ); // Update group conversations, point members at new conversation ids + // tslint:disable-next-line no-floating-promises migrationJobQueue.addAll( groupConverations.map(groupRow => async () => { const members = groupRow.members.split(/\s?\+/).filter(Boolean); const newMembers = []; - // eslint-disable-next-line no-restricted-syntax for (const m of members) { const memberRow = allConversationsByOldId[m]; @@ -1341,7 +1452,6 @@ async function updateToSchemaVersion20(currentVersion, instance) { // We didn't previously have a private conversation for this member, // we need to create one const id = generateUUID(); - // eslint-disable-next-line no-await-in-loop await saveConversation( { id, @@ -1374,7 +1484,6 @@ async function updateToSchemaVersion20(currentVersion, instance) { // Update sessions to stable IDs const allSessions = await instance.all('SELECT * FROM sessions;'); - // eslint-disable-next-line no-restricted-syntax for (const session of allSessions) { // Not using patch here so we can explicitly delete a property rather than // implicitly delete via null @@ -1385,7 +1494,6 @@ async function updateToSchemaVersion20(currentVersion, instance) { newJson.id = `${newJson.conversationId}.${newJson.deviceId}`; } delete newJson.number; - // eslint-disable-next-line no-await-in-loop await instance.run( ` UPDATE sessions @@ -1403,11 +1511,9 @@ async function updateToSchemaVersion20(currentVersion, instance) { // Update identity keys to stable IDs const allIdentityKeys = await instance.all('SELECT * FROM identityKeys;'); - // eslint-disable-next-line no-restricted-syntax for (const identityKey of allIdentityKeys) { const newJson = JSON.parse(identityKey.json); newJson.id = allConversationsByOldId[newJson.id]; - // eslint-disable-next-line no-await-in-loop await instance.run( ` UPDATE identityKeys @@ -1423,9 +1529,7 @@ async function updateToSchemaVersion20(currentVersion, instance) { } // Recreate triggers - // eslint-disable-next-line no-restricted-syntax for (const trigger of triggers) { - // eslint-disable-next-line no-await-in-loop await instance.run(trigger.sql); } @@ -1442,7 +1546,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion2, updateToSchemaVersion3, updateToSchemaVersion4, - () => null, // version 5 was dropped + (_v: number, _i: PromisifiedSQLDatabase) => null, // version 5 was dropped updateToSchemaVersion6, updateToSchemaVersion7, updateToSchemaVersion8, @@ -1460,7 +1564,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion20, ]; -async function updateSchema(instance) { +async function updateSchema(instance: PromisifiedSQLDatabase) { const sqliteVersion = await getSQLiteVersion(instance); const sqlcipherVersion = await getSQLCipherVersion(instance); const userVersion = await getUserVersion(instance); @@ -1486,17 +1590,25 @@ async function updateSchema(instance) { const runSchemaUpdate = SCHEMA_VERSIONS[index]; // Yes, we really want to do this asynchronously, in order - // eslint-disable-next-line no-await-in-loop await runSchemaUpdate(userVersion, instance); } } -let db; -let filePath; -let indexedDBPath; +let globalInstance: PromisifiedSQLDatabase | undefined; +let databaseFilePath: string | undefined; +let indexedDBPath: string | undefined; -async function initialize({ configDir, key, messages }) { - if (db) { +// tslint:disable-next-line max-func-body-length +async function initialize({ + configDir, + key, + messages, +}: { + configDir: string; + key: string; + messages: LocaleMessagesType; +}) { + if (globalInstance) { throw new Error('Cannot initialize more than once!'); } @@ -1515,16 +1627,16 @@ async function initialize({ configDir, key, messages }) { const dbDir = join(configDir, 'sql'); mkdirp.sync(dbDir); - filePath = join(dbDir, 'db.sqlite'); + databaseFilePath = join(dbDir, 'db.sqlite'); - let promisified; + let promisified: PromisifiedSQLDatabase | undefined; try { - promisified = await openAndSetUpSQLCipher(filePath, { key }); + promisified = await openAndSetUpSQLCipher(databaseFilePath, { key }); // promisified.on('trace', async statement => { // if ( - // !db || + // !globalInstance || // statement.startsWith('--') || // statement.includes('COMMIT') || // statement.includes('BEGIN') || @@ -1562,7 +1674,7 @@ async function initialize({ configDir, key, messages }) { } // At this point we can allow general access to the database - db = promisified; + globalInstance = promisified; // test database await getMessageCount(); @@ -1594,6 +1706,7 @@ async function initialize({ configDir, key, messages }) { } app.exit(1); + return false; } @@ -1601,21 +1714,26 @@ async function initialize({ configDir, key, messages }) { } async function close() { - if (!db) { + if (!globalInstance) { return; } - const dbRef = db; - db = null; + const dbRef = globalInstance; + globalInstance = undefined; await dbRef.close(); } async function removeDB() { - if (db) { + if (globalInstance) { throw new Error('removeDB: Cannot erase database when it is open!'); } + if (!databaseFilePath) { + throw new Error( + 'removeDB: Cannot erase database without a databaseFilePath!' + ); + } - rimraf.sync(filePath); + rimraf.sync(databaseFilePath); } async function removeIndexedDBFiles() { @@ -1627,20 +1745,28 @@ async function removeIndexedDBFiles() { const pattern = join(indexedDBPath, '*.leveldb'); rimraf.sync(pattern); - indexedDBPath = null; + indexedDBPath = undefined; +} + +function getInstance(): PromisifiedSQLDatabase { + if (!globalInstance) { + throw new Error('getInstance: globalInstance not set!'); + } + + return globalInstance; } const IDENTITY_KEYS_TABLE = 'identityKeys'; -async function createOrUpdateIdentityKey(data) { +async function createOrUpdateIdentityKey(data: IdentityKeyType) { return createOrUpdate(IDENTITY_KEYS_TABLE, data); } -async function getIdentityKeyById(id) { +async function getIdentityKeyById(id: string) { return getById(IDENTITY_KEYS_TABLE, id); } -async function bulkAddIdentityKeys(array) { +async function bulkAddIdentityKeys(array: Array) { return bulkAdd(IDENTITY_KEYS_TABLE, array); } -async function removeIdentityKeyById(id) { +async function removeIdentityKeyById(id: string) { return removeById(IDENTITY_KEYS_TABLE, id); } async function removeAllIdentityKeys() { @@ -1651,16 +1777,16 @@ async function getAllIdentityKeys() { } const PRE_KEYS_TABLE = 'preKeys'; -async function createOrUpdatePreKey(data) { +async function createOrUpdatePreKey(data: PreKeyType) { return createOrUpdate(PRE_KEYS_TABLE, data); } -async function getPreKeyById(id) { +async function getPreKeyById(id: number) { return getById(PRE_KEYS_TABLE, id); } -async function bulkAddPreKeys(array) { +async function bulkAddPreKeys(array: Array) { return bulkAdd(PRE_KEYS_TABLE, array); } -async function removePreKeyById(id) { +async function removePreKeyById(id: number) { return removeById(PRE_KEYS_TABLE, id); } async function removeAllPreKeys() { @@ -1671,41 +1797,45 @@ async function getAllPreKeys() { } const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys'; -async function createOrUpdateSignedPreKey(data) { +async function createOrUpdateSignedPreKey(data: SignedPreKeyType) { return createOrUpdate(SIGNED_PRE_KEYS_TABLE, data); } -async function getSignedPreKeyById(id) { +async function getSignedPreKeyById(id: number) { return getById(SIGNED_PRE_KEYS_TABLE, id); } -async function getAllSignedPreKeys() { - const rows = await db.all('SELECT json FROM signedPreKeys ORDER BY id ASC;'); - return map(rows, row => jsonToObject(row.json)); -} -async function bulkAddSignedPreKeys(array) { +async function bulkAddSignedPreKeys(array: Array) { return bulkAdd(SIGNED_PRE_KEYS_TABLE, array); } -async function removeSignedPreKeyById(id) { +async function removeSignedPreKeyById(id: number) { return removeById(SIGNED_PRE_KEYS_TABLE, id); } async function removeAllSignedPreKeys() { return removeAllFromTable(SIGNED_PRE_KEYS_TABLE); } +async function getAllSignedPreKeys() { + const db = getInstance(); + const rows = await db.all('SELECT json FROM signedPreKeys ORDER BY id ASC;'); + + return map(rows, row => jsonToObject(row.json)); +} const ITEMS_TABLE = 'items'; -async function createOrUpdateItem(data) { +async function createOrUpdateItem(data: ItemType) { return createOrUpdate(ITEMS_TABLE, data); } -async function getItemById(id) { +async function getItemById(id: string) { return getById(ITEMS_TABLE, id); } async function getAllItems() { + const db = getInstance(); const rows = await db.all('SELECT json FROM items ORDER BY id ASC;'); + return map(rows, row => jsonToObject(row.json)); } -async function bulkAddItems(array) { +async function bulkAddItems(array: Array) { return bulkAdd(ITEMS_TABLE, array); } -async function removeItemById(id) { +async function removeItemById(id: string) { return removeById(ITEMS_TABLE, id); } async function removeAllItems() { @@ -1713,7 +1843,8 @@ async function removeAllItems() { } const SESSIONS_TABLE = 'sessions'; -async function createOrUpdateSession(data) { +async function createOrUpdateSession(data: SessionType) { + const db = getInstance(); const { id, conversationId } = data; if (!id) { throw new Error( @@ -1743,11 +1874,14 @@ async function createOrUpdateSession(data) { } ); } -async function createOrUpdateSessions(array) { +async function createOrUpdateSessions(array: Array) { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { - await Promise.all([...map(array, item => createOrUpdateSession(item))]); + await Promise.all([ + ...map(array, async item => createOrUpdateSession(item)), + ]); await db.run('COMMIT TRANSACTION;'); } catch (error) { await db.run('ROLLBACK;'); @@ -1756,27 +1890,30 @@ async function createOrUpdateSessions(array) { } createOrUpdateSessions.needsSerial = true; -async function getSessionById(id) { +async function getSessionById(id: string) { return getById(SESSIONS_TABLE, id); } -async function getSessionsById(id) { +async function getSessionsById(conversationId: string) { + const db = getInstance(); const rows = await db.all( - 'SELECT * FROM sessions WHERE conversationId = $id;', + 'SELECT * FROM sessions WHERE conversationId = $conversationId;', { - $id: id, + $conversationId: conversationId, } ); + return map(rows, row => jsonToObject(row.json)); } -async function bulkAddSessions(array) { +async function bulkAddSessions(array: Array) { return bulkAdd(SESSIONS_TABLE, array); } -async function removeSessionById(id) { +async function removeSessionById(id: string) { return removeById(SESSIONS_TABLE, id); } -async function removeSessionsById(id) { - await db.run('DELETE FROM sessions WHERE conversationId = $id;', { - $id: id, +async function removeSessionsByConversation(conversationId: string) { + const db = getInstance(); + await db.run('DELETE FROM sessions WHERE conversationId = $conversationId;', { + $conversationId: conversationId, }); } async function removeAllSessions() { @@ -1786,7 +1923,8 @@ async function getAllSessions() { return getAllFromTable(SESSIONS_TABLE); } -async function createOrUpdate(table, data) { +async function createOrUpdate(table: string, data: any) { + const db = getInstance(); const { id } = data; if (!id) { throw new Error('createOrUpdate: Provided data did not have a truthy id'); @@ -1807,11 +1945,14 @@ async function createOrUpdate(table, data) { ); } -async function bulkAdd(table, array) { +async function bulkAdd(table: string, array: Array) { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { - await Promise.all([...map(array, data => createOrUpdate(table, data))]); + await Promise.all([ + ...map(array, async data => createOrUpdate(table, data)), + ]); await db.run('COMMIT TRANSACTION;'); } catch (error) { @@ -1820,21 +1961,24 @@ async function bulkAdd(table, array) { } } -async function getById(table, id) { +async function getById(table: string, id: string | number) { + const db = getInstance(); const row = await db.get(`SELECT * FROM ${table} WHERE id = $id;`, { $id: id, }); if (!row) { - return null; + return undefined; } return jsonToObject(row.json); } -async function removeById(table, id) { +async function removeById(table: string, id: string | number) { + const db = getInstance(); if (!Array.isArray(id)) { await db.run(`DELETE FROM ${table} WHERE id = $id;`, { $id: id }); + return; } @@ -1849,18 +1993,22 @@ async function removeById(table, id) { ); } -async function removeAllFromTable(table) { +async function removeAllFromTable(table: string) { + const db = getInstance(); await db.run(`DELETE FROM ${table};`); } -async function getAllFromTable(table) { +async function getAllFromTable(table: string) { + const db = getInstance(); const rows = await db.all(`SELECT json FROM ${table};`); + return rows.map(row => jsonToObject(row.json)); } // Conversations async function getConversationCount() { + const db = getInstance(); const row = await db.get('SELECT count(*) from conversations;'); if (!row) { @@ -1872,9 +2020,11 @@ async function getConversationCount() { return row['count(*)']; } -async function saveConversation(data, instance = db) { +async function saveConversation( + data: ConversationType, + instance = getInstance() +) { const { - // eslint-disable-next-line camelcase active_at, e164, groupId, @@ -1938,12 +2088,15 @@ async function saveConversation(data, instance = db) { ); } -async function saveConversations(arrayOfConversations) { +async function saveConversations( + arrayOfConversations: Array +) { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { await Promise.all([ - ...map(arrayOfConversations, conversation => + ...map(arrayOfConversations, async conversation => saveConversation(conversation) ), ]); @@ -1956,10 +2109,10 @@ async function saveConversations(arrayOfConversations) { } saveConversations.needsSerial = true; -async function updateConversation(data) { +async function updateConversation(data: ConversationType) { + const db = getInstance(); const { id, - // eslint-disable-next-line camelcase active_at, type, members, @@ -2002,11 +2155,12 @@ async function updateConversation(data) { } ); } -async function updateConversations(array) { +async function updateConversations(array: Array) { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { - await Promise.all([...map(array, item => updateConversation(item))]); + await Promise.all([...map(array, async item => updateConversation(item))]); await db.run('COMMIT TRANSACTION;'); } catch (error) { await db.run('ROLLBACK;'); @@ -2015,9 +2169,11 @@ async function updateConversations(array) { } updateConversations.needsSerial = true; -async function removeConversation(id) { +async function removeConversation(id: Array | string) { + const db = getInstance(); if (!Array.isArray(id)) { await db.run('DELETE FROM conversations WHERE id = $id;', { $id: id }); + return; } @@ -2034,7 +2190,8 @@ async function removeConversation(id) { ); } -async function getConversationById(id) { +async function getConversationById(id: string) { + const db = getInstance(); const row = await db.get('SELECT * FROM conversations WHERE id = $id;', { $id: id, }); @@ -2047,16 +2204,21 @@ async function getConversationById(id) { } async function getAllConversations() { + const db = getInstance(); const rows = await db.all('SELECT json FROM conversations ORDER BY id ASC;'); + return map(rows, row => jsonToObject(row.json)); } async function getAllConversationIds() { + const db = getInstance(); const rows = await db.all('SELECT id FROM conversations ORDER BY id ASC;'); + return map(rows, row => row.id); } async function getAllPrivateConversations() { + const db = getInstance(); const rows = await db.all( `SELECT json FROM conversations WHERE type = 'private' @@ -2066,7 +2228,8 @@ async function getAllPrivateConversations() { return map(rows, row => jsonToObject(row.json)); } -async function getAllGroupsInvolvingId(id) { +async function getAllGroupsInvolvingId(id: string) { + const db = getInstance(); const rows = await db.all( `SELECT json FROM conversations WHERE type = 'group' AND @@ -2080,7 +2243,11 @@ async function getAllGroupsInvolvingId(id) { return map(rows, row => jsonToObject(row.json)); } -async function searchConversations(query, { limit } = {}) { +async function searchConversations( + query: string, + { limit }: { limit?: number } = {} +): Promise> { + const db = getInstance(); const rows = await db.all( `SELECT json FROM conversations WHERE ( @@ -2101,7 +2268,11 @@ async function searchConversations(query, { limit } = {}) { return map(rows, row => jsonToObject(row.json)); } -async function searchMessages(query, { limit } = {}) { +async function searchMessages( + query: string, + { limit }: { limit?: number } = {} +): Promise> { + const db = getInstance(); const rows = await db.all( `SELECT messages.json, @@ -2125,10 +2296,11 @@ async function searchMessages(query, { limit } = {}) { } async function searchMessagesInConversation( - query, - conversationId, - { limit } = {} -) { + query: string, + conversationId: string, + { limit }: { limit?: number } = {} +): Promise> { + const db = getInstance(); const rows = await db.all( `SELECT messages.json, @@ -2154,6 +2326,7 @@ async function searchMessagesInConversation( } async function getMessageCount() { + const db = getInstance(); const row = await db.get('SELECT count(*) from messages;'); if (!row) { @@ -2163,11 +2336,15 @@ async function getMessageCount() { return row['count(*)']; } -async function saveMessage(data, { forceSave } = {}) { +// tslint:disable-next-line max-func-body-length +async function saveMessage( + data: MessageType, + { forceSave }: { forceSave?: boolean } = {} +) { + const db = getInstance(); const { body, conversationId, - // eslint-disable-next-line camelcase expires_at, hasAttachments, hasFileAttachments, @@ -2175,10 +2352,8 @@ async function saveMessage(data, { forceSave } = {}) { id, isErased, isViewOnce, - // eslint-disable-next-line camelcase received_at, schemaVersion, - // eslint-disable-next-line camelcase sent_at, source, sourceUuid, @@ -2305,12 +2480,18 @@ async function saveMessage(data, { forceSave } = {}) { return toCreate.id; } -async function saveMessages(arrayOfMessages, { forceSave } = {}) { +async function saveMessages( + arrayOfMessages: Array, + { forceSave }: { forceSave?: boolean } = {} +) { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { await Promise.all([ - ...map(arrayOfMessages, message => saveMessage(message, { forceSave })), + ...map(arrayOfMessages, async message => + saveMessage(message, { forceSave }) + ), ]); await db.run('COMMIT TRANSACTION;'); @@ -2321,9 +2502,11 @@ async function saveMessages(arrayOfMessages, { forceSave } = {}) { } saveMessages.needsSerial = true; -async function removeMessage(id) { +async function removeMessage(id: Array | string) { + const db = getInstance(); if (!Array.isArray(id)) { await db.run('DELETE FROM messages WHERE id = $id;', { $id: id }); + return; } @@ -2338,7 +2521,8 @@ async function removeMessage(id) { ); } -async function getMessageById(id) { +async function getMessageById(id: string) { + const db = getInstance(); const row = await db.get('SELECT * FROM messages WHERE id = $id;', { $id: id, }); @@ -2350,24 +2534,32 @@ async function getMessageById(id) { return jsonToObject(row.json); } -async function getAllMessages() { +async function _getAllMessages() { + const db = getInstance(); const rows = await db.all('SELECT json FROM messages ORDER BY id ASC;'); + return map(rows, row => jsonToObject(row.json)); } async function getAllMessageIds() { + const db = getInstance(); const rows = await db.all('SELECT id FROM messages ORDER BY id ASC;'); + return map(rows, row => row.id); } -// eslint-disable-next-line camelcase async function getMessageBySender({ source, sourceUuid, sourceDevice, - // eslint-disable-next-line camelcase sent_at, +}: { + source: string; + sourceUuid: string; + sourceDevice: string; + sent_at: number; }) { + const db = getInstance(); const rows = await db.all( `SELECT json FROM messages WHERE (source = $source OR sourceUuid = $sourceUuid) AND @@ -2384,7 +2576,8 @@ async function getMessageBySender({ return map(rows, row => jsonToObject(row.json)); } -async function getUnreadByConversation(conversationId) { +async function getUnreadByConversation(conversationId: string) { + const db = getInstance(); const rows = await db.all( `SELECT json FROM messages WHERE unread = $unread AND @@ -2400,9 +2593,13 @@ async function getUnreadByConversation(conversationId) { } async function getOlderMessagesByConversation( - conversationId, - { limit = 100, receivedAt = Number.MAX_VALUE } = {} + conversationId: string, + { + limit = 100, + receivedAt = Number.MAX_VALUE, + }: { limit?: number; receivedAt?: number } = {} ) { + const db = getInstance(); const rows = await db.all( `SELECT json FROM messages WHERE conversationId = $conversationId AND @@ -2420,9 +2617,10 @@ async function getOlderMessagesByConversation( } async function getNewerMessagesByConversation( - conversationId, - { limit = 100, receivedAt = 0 } = {} + conversationId: string, + { limit = 100, receivedAt = 0 }: { limit?: number; receivedAt?: number } = {} ) { + const db = getInstance(); const rows = await db.all( `SELECT json FROM messages WHERE conversationId = $conversationId AND @@ -2438,7 +2636,8 @@ async function getNewerMessagesByConversation( return rows; } -async function getOldestMessageForConversation(conversationId) { +async function getOldestMessageForConversation(conversationId: string) { + const db = getInstance(); const row = await db.get( `SELECT * FROM messages WHERE conversationId = $conversationId @@ -2455,7 +2654,8 @@ async function getOldestMessageForConversation(conversationId) { return row; } -async function getNewestMessageForConversation(conversationId) { +async function getNewestMessageForConversation(conversationId: string) { + const db = getInstance(); const row = await db.get( `SELECT * FROM messages WHERE conversationId = $conversationId @@ -2472,7 +2672,8 @@ async function getNewestMessageForConversation(conversationId) { return row; } -async function getOldestUnreadMessageForConversation(conversationId) { +async function getOldestUnreadMessageForConversation(conversationId: string) { + const db = getInstance(); const row = await db.get( `SELECT * FROM messages WHERE conversationId = $conversationId AND @@ -2491,7 +2692,8 @@ async function getOldestUnreadMessageForConversation(conversationId) { return row; } -async function getTotalUnreadForConversation(conversationId) { +async function getTotalUnreadForConversation(conversationId: string) { + const db = getInstance(); const row = await db.get( `SELECT count(id) from messages WHERE conversationId = $conversationId AND @@ -2509,7 +2711,7 @@ async function getTotalUnreadForConversation(conversationId) { return row['count(id)']; } -async function getMessageMetricsForConversation(conversationId) { +async function getMessageMetricsForConversation(conversationId: string) { const results = await Promise.all([ getOldestMessageForConversation(conversationId), getNewestMessageForConversation(conversationId), @@ -2530,7 +2732,8 @@ async function getMessageMetricsForConversation(conversationId) { } getMessageMetricsForConversation.needsSerial = true; -async function getMessagesBySentAt(sentAt) { +async function getMessagesBySentAt(sentAt: number) { + const db = getInstance(); const rows = await db.all( `SELECT * FROM messages WHERE sent_at = $sent_at @@ -2544,6 +2747,7 @@ async function getMessagesBySentAt(sentAt) { } async function getExpiredMessages() { + const db = getInstance(); const now = Date.now(); const rows = await db.all( @@ -2560,6 +2764,7 @@ async function getExpiredMessages() { } async function getOutgoingWithoutExpiresAt() { + const db = getInstance(); const rows = await db.all(` SELECT json FROM messages INDEXED BY messages_without_timer @@ -2574,6 +2779,8 @@ async function getOutgoingWithoutExpiresAt() { } async function getNextExpiringMessage() { + const db = getInstance(); + // Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index const rows = await db.all(` SELECT json FROM messages @@ -2582,10 +2789,15 @@ async function getNextExpiringMessage() { LIMIT 1; `); - return map(rows, row => jsonToObject(row.json)); + if (!rows || rows.length < 1) { + return null; + } + + return jsonToObject(rows[0].json); } async function getNextTapToViewMessageToAgeOut() { + const db = getInstance(); const rows = await db.all(` SELECT json FROM messages WHERE @@ -2603,6 +2815,7 @@ async function getNextTapToViewMessageToAgeOut() { } async function getTapToViewMessagesNeedingErase() { + const db = getInstance(); const THIRTY_DAYS_AGO = Date.now() - 30 * 24 * 60 * 60 * 1000; const rows = await db.all( @@ -2620,7 +2833,11 @@ async function getTapToViewMessagesNeedingErase() { return map(rows, row => jsonToObject(row.json)); } -async function saveUnprocessed(data, { forceSave } = {}) { +async function saveUnprocessed( + data: UnprocessedType, + { forceSave }: { forceSave?: boolean } = {} +) { + const db = getInstance(); const { id, timestamp, version, attempts, envelope } = data; if (!id) { throw new Error('saveUnprocessed: id was falsey'); @@ -2672,12 +2889,16 @@ async function saveUnprocessed(data, { forceSave } = {}) { return id; } -async function saveUnprocesseds(arrayOfUnprocessed, { forceSave } = {}) { +async function saveUnprocesseds( + arrayOfUnprocessed: Array, + { forceSave }: { forceSave?: boolean } = {} +) { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { await Promise.all([ - ...map(arrayOfUnprocessed, unprocessed => + ...map(arrayOfUnprocessed, async unprocessed => saveUnprocessed(unprocessed, { forceSave }) ), ]); @@ -2690,13 +2911,15 @@ async function saveUnprocesseds(arrayOfUnprocessed, { forceSave } = {}) { } saveUnprocesseds.needsSerial = true; -async function updateUnprocessedAttempts(id, attempts) { +async function updateUnprocessedAttempts(id: string, attempts: number) { + const db = getInstance(); await db.run('UPDATE unprocessed SET attempts = $attempts WHERE id = $id;', { $id: id, $attempts: attempts, }); } -async function updateUnprocessedWithData(id, data = {}) { +async function updateUnprocessedWithData(id: string, data: UnprocessedType) { + const db = getInstance(); const { source, sourceDevice, serverTimestamp, decrypted } = data; await db.run( @@ -2715,12 +2938,15 @@ async function updateUnprocessedWithData(id, data = {}) { } ); } -async function updateUnprocessedsWithData(arrayOfUnprocessed) { +async function updateUnprocessedsWithData( + arrayOfUnprocessed: Array +) { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { await Promise.all([ - ...map(arrayOfUnprocessed, ({ id, data }) => + ...map(arrayOfUnprocessed, async ({ id, data }) => updateUnprocessedWithData(id, data) ), ]); @@ -2733,7 +2959,8 @@ async function updateUnprocessedsWithData(arrayOfUnprocessed) { } updateUnprocessedsWithData.needsSerial = true; -async function getUnprocessedById(id) { +async function getUnprocessedById(id: string) { + const db = getInstance(); const row = await db.get('SELECT * FROM unprocessed WHERE id = $id;', { $id: id, }); @@ -2742,6 +2969,7 @@ async function getUnprocessedById(id) { } async function getUnprocessedCount() { + const db = getInstance(); const row = await db.get('SELECT count(*) from unprocessed;'); if (!row) { @@ -2752,6 +2980,7 @@ async function getUnprocessedCount() { } async function getAllUnprocessed() { + const db = getInstance(); const rows = await db.all( 'SELECT * FROM unprocessed ORDER BY timestamp ASC;' ); @@ -2759,9 +2988,12 @@ async function getAllUnprocessed() { return rows; } -async function removeUnprocessed(id) { +async function removeUnprocessed(id: string) { + const db = getInstance(); + if (!Array.isArray(id)) { await db.run('DELETE FROM unprocessed WHERE id = $id;', { $id: id }); + return; } @@ -2777,14 +3009,20 @@ async function removeUnprocessed(id) { } async function removeAllUnprocessed() { + const db = getInstance(); await db.run('DELETE FROM unprocessed;'); } // Attachment Downloads const ATTACHMENT_DOWNLOADS_TABLE = 'attachment_downloads'; -async function getNextAttachmentDownloadJobs(limit, options = {}) { - const timestamp = options.timestamp || Date.now(); +async function getNextAttachmentDownloadJobs( + limit?: number, + options: { timestamp?: number } = {} +) { + const db = getInstance(); + const timestamp = + options && options.timestamp ? options.timestamp : Date.now(); const rows = await db.all( `SELECT json FROM attachment_downloads @@ -2792,14 +3030,15 @@ async function getNextAttachmentDownloadJobs(limit, options = {}) { ORDER BY timestamp DESC LIMIT $limit;`, { - $limit: limit, + $limit: limit || 3, $timestamp: timestamp, } ); return map(rows, row => jsonToObject(row.json)); } -async function saveAttachmentDownloadJob(job) { +async function saveAttachmentDownloadJob(job: AttachmentDownloadJobType) { + const db = getInstance(); const { id, pending, timestamp } = job; if (!id) { throw new Error( @@ -2827,7 +3066,8 @@ async function saveAttachmentDownloadJob(job) { } ); } -async function setAttachmentDownloadJobPending(id, pending) { +async function setAttachmentDownloadJobPending(id: string, pending: boolean) { + const db = getInstance(); await db.run( 'UPDATE attachment_downloads SET pending = $pending WHERE id = $id;', { @@ -2837,11 +3077,12 @@ async function setAttachmentDownloadJobPending(id, pending) { ); } async function resetAttachmentDownloadPending() { + const db = getInstance(); await db.run( 'UPDATE attachment_downloads SET pending = 0 WHERE pending != 0;' ); } -async function removeAttachmentDownloadJob(id) { +async function removeAttachmentDownloadJob(id: string) { return removeById(ATTACHMENT_DOWNLOADS_TABLE, id); } async function removeAllAttachmentDownloadJobs() { @@ -2850,7 +3091,8 @@ async function removeAllAttachmentDownloadJobs() { // Stickers -async function createOrUpdateStickerPack(pack) { +async function createOrUpdateStickerPack(pack: StickerPackType) { + const db = getInstance(); const { attemptedStatus, author, @@ -2906,6 +3148,7 @@ async function createOrUpdateStickerPack(pack) { WHERE id = $id;`, payload ); + return; } @@ -2940,9 +3183,13 @@ async function createOrUpdateStickerPack(pack) { payload ); } -async function updateStickerPackStatus(id, status, options) { - // Strange, but an undefined parameter gets coerced into null via ipc - const timestamp = (options || {}).timestamp || Date.now(); +async function updateStickerPackStatus( + id: string, + status: StickerPackStatusType, + options?: { timestamp: number } +) { + const db = getInstance(); + const timestamp = options ? options.timestamp || Date.now() : Date.now(); const installedAt = status === 'installed' ? timestamp : null; await db.run( @@ -2957,7 +3204,8 @@ async function updateStickerPackStatus(id, status, options) { } ); } -async function createOrUpdateSticker(sticker) { +async function createOrUpdateSticker(sticker: StickerType) { + const db = getInstance(); const { emoji, height, @@ -2968,6 +3216,7 @@ async function createOrUpdateSticker(sticker) { path, width, } = sticker; + if (!isNumber(id)) { throw new Error( 'createOrUpdateSticker: Provided data did not have a numeric id' @@ -3011,7 +3260,12 @@ async function createOrUpdateSticker(sticker) { } ); } -async function updateStickerLastUsed(packId, stickerId, lastUsed) { +async function updateStickerLastUsed( + packId: string, + stickerId: number, + lastUsed: number +) { + const db = getInstance(); await db.run( `UPDATE stickers SET lastUsed = $lastUsed @@ -3032,7 +3286,9 @@ async function updateStickerLastUsed(packId, stickerId, lastUsed) { } ); } -async function addStickerPackReference(messageId, packId) { +async function addStickerPackReference(messageId: string, packId: string) { + const db = getInstance(); + if (!messageId) { throw new Error( 'addStickerPackReference: Provided data did not have a truthy messageId' @@ -3058,7 +3314,9 @@ async function addStickerPackReference(messageId, packId) { } ); } -async function deleteStickerPackReference(messageId, packId) { +async function deleteStickerPackReference(messageId: string, packId: string) { + const db = getInstance(); + if (!messageId) { throw new Error( 'addStickerPackReference: Provided data did not have a truthy messageId' @@ -3106,7 +3364,8 @@ async function deleteStickerPackReference(messageId, packId) { const count = countRow['count(*)']; if (count > 0) { await db.run('COMMIT TRANSACTION;'); - return null; + + return []; } const packRow = await db.get( @@ -3117,13 +3376,15 @@ async function deleteStickerPackReference(messageId, packId) { if (!packRow) { console.log('deleteStickerPackReference: did not find referenced pack'); await db.run('COMMIT TRANSACTION;'); - return null; + + return []; } const { status } = packRow; if (status === 'installed') { await db.run('COMMIT TRANSACTION;'); - return null; + + return []; } const stickerPathRows = await db.all( @@ -3149,7 +3410,9 @@ async function deleteStickerPackReference(messageId, packId) { } deleteStickerPackReference.needsSerial = true; -async function deleteStickerPack(packId) { +async function deleteStickerPack(packId: string) { + const db = getInstance(); + if (!packId) { throw new Error( 'deleteStickerPack: Provided data did not have a truthy packId' @@ -3190,6 +3453,8 @@ async function deleteStickerPack(packId) { deleteStickerPack.needsSerial = true; async function getStickerCount() { + const db = getInstance(); + const row = await db.get('SELECT count(*) from stickers;'); if (!row) { @@ -3199,6 +3464,8 @@ async function getStickerCount() { return row['count(*)']; } async function getAllStickerPacks() { + const db = getInstance(); + const rows = await db.all( `SELECT * FROM sticker_packs ORDER BY installedAt DESC, createdAt DESC` @@ -3207,6 +3474,8 @@ async function getAllStickerPacks() { return rows || []; } async function getAllStickers() { + const db = getInstance(); + const rows = await db.all( `SELECT * FROM stickers ORDER BY packId ASC, id ASC` @@ -3214,7 +3483,9 @@ async function getAllStickers() { return rows || []; } -async function getRecentStickers({ limit } = {}) { +async function getRecentStickers({ limit }: { limit?: number } = {}) { + const db = getInstance(); + // Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index const rows = await db.all( `SELECT stickers.* FROM stickers @@ -3231,7 +3502,11 @@ async function getRecentStickers({ limit } = {}) { } // Emojis -async function updateEmojiUsage(shortName, timeUsed = Date.now()) { +async function updateEmojiUsage( + shortName: string, + timeUsed: number = Date.now() +) { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { @@ -3262,7 +3537,8 @@ async function updateEmojiUsage(shortName, timeUsed = Date.now()) { } updateEmojiUsage.needsSerial = true; -async function getRecentEmojis(limit = 32) { +async function getRecentEmojis(limit: number = 32) { + const db = getInstance(); const rows = await db.all( 'SELECT * FROM emojis ORDER BY lastUsage DESC LIMIT $limit;', { @@ -3275,6 +3551,7 @@ async function getRecentEmojis(limit = 32) { // All data in database async function removeAll() { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { @@ -3304,6 +3581,7 @@ removeAll.needsSerial = true; // Anything that isn't user-visible data async function removeAllConfiguration() { + const db = getInstance(); await db.run('BEGIN TRANSACTION;'); try { @@ -3324,7 +3602,11 @@ async function removeAllConfiguration() { } removeAllConfiguration.needsSerial = true; -async function getMessagesNeedingUpgrade(limit, { maxVersion }) { +async function getMessagesNeedingUpgrade( + limit: number, + { maxVersion }: { maxVersion: number } +) { + const db = getInstance(); const rows = await db.all( `SELECT json FROM messages WHERE schemaVersion IS NULL OR schemaVersion < $maxVersion @@ -3339,9 +3621,10 @@ async function getMessagesNeedingUpgrade(limit, { maxVersion }) { } async function getMessagesWithVisualMediaAttachments( - conversationId, - { limit } + conversationId: string, + { limit }: { limit: number } ) { + const db = getInstance(); const rows = await db.all( `SELECT json FROM messages WHERE conversationId = $conversationId AND @@ -3357,7 +3640,11 @@ async function getMessagesWithVisualMediaAttachments( return map(rows, row => jsonToObject(row.json)); } -async function getMessagesWithFileAttachments(conversationId, { limit }) { +async function getMessagesWithFileAttachments( + conversationId: string, + { limit }: { limit: number } +) { + const db = getInstance(); const rows = await db.all( `SELECT json FROM messages WHERE conversationId = $conversationId AND @@ -3373,9 +3660,9 @@ async function getMessagesWithFileAttachments(conversationId, { limit }) { return map(rows, row => jsonToObject(row.json)); } -function getExternalFilesForMessage(message) { +function getExternalFilesForMessage(message: MessageType) { const { attachments, contact, quote, preview, sticker } = message; - const files = []; + const files: Array = []; forEach(attachments, attachment => { const { path: file, thumbnail, screenshot } = attachment; @@ -3433,9 +3720,9 @@ function getExternalFilesForMessage(message) { return files; } -function getExternalFilesForConversation(conversation) { +function getExternalFilesForConversation(conversation: ConversationType) { const { avatar, profileAvatar } = conversation; - const files = []; + const files: Array = []; if (avatar && avatar.path) { files.push(avatar.path); @@ -3448,9 +3735,9 @@ function getExternalFilesForConversation(conversation) { return files; } -function getExternalDraftFilesForConversation(conversation) { +function getExternalDraftFilesForConversation(conversation: ConversationType) { const draftAttachments = conversation.draftAttachments || []; - const files = []; + const files: Array = []; forEach(draftAttachments, attachment => { const { path: file, screenshotPath } = attachment; @@ -3466,8 +3753,11 @@ function getExternalDraftFilesForConversation(conversation) { return files; } -async function removeKnownAttachments(allAttachments) { - const lookup = fromPairs(map(allAttachments, file => [file, true])); +async function removeKnownAttachments(allAttachments: Array) { + const db = getInstance(); + const lookup: Dictionary = fromPairs( + map(allAttachments, file => [file, true]) + ); const chunkSize = 50; const total = await getMessageCount(); @@ -3477,10 +3767,9 @@ async function removeKnownAttachments(allAttachments) { let count = 0; let complete = false; - let id = ''; + let id: string | number = ''; while (!complete) { - // eslint-disable-next-line no-await-in-loop const rows = await db.all( `SELECT json FROM messages WHERE id > $id @@ -3492,15 +3781,18 @@ async function removeKnownAttachments(allAttachments) { } ); - const messages = map(rows, row => jsonToObject(row.json)); + const messages: Array = map(rows, row => + jsonToObject(row.json) + ); forEach(messages, message => { const externalFiles = getExternalFilesForMessage(message); forEach(externalFiles, file => { + // tslint:disable-next-line no-dynamic-delete delete lookup[file]; }); }); - const lastMessage = last(messages); + const lastMessage: MessageType | undefined = last(messages); if (lastMessage) { ({ id } = lastMessage); } @@ -3522,7 +3814,6 @@ async function removeKnownAttachments(allAttachments) { ); while (!complete) { - // eslint-disable-next-line no-await-in-loop const rows = await db.all( `SELECT json FROM conversations WHERE id > $id @@ -3534,15 +3825,18 @@ async function removeKnownAttachments(allAttachments) { } ); - const conversations = map(rows, row => jsonToObject(row.json)); + const conversations: Array = map(rows, row => + jsonToObject(row.json) + ); forEach(conversations, conversation => { const externalFiles = getExternalFilesForConversation(conversation); forEach(externalFiles, file => { + // tslint:disable-next-line no-dynamic-delete delete lookup[file]; }); }); - const lastMessage = last(conversations); + const lastMessage: ConversationType | undefined = last(conversations); if (lastMessage) { ({ id } = lastMessage); } @@ -3555,8 +3849,11 @@ async function removeKnownAttachments(allAttachments) { return Object.keys(lookup); } -async function removeKnownStickers(allStickers) { - const lookup = fromPairs(map(allStickers, file => [file, true])); +async function removeKnownStickers(allStickers: Array) { + const db = getInstance(); + const lookup: Dictionary = fromPairs( + map(allStickers, file => [file, true]) + ); const chunkSize = 50; const total = await getStickerCount(); @@ -3569,7 +3866,6 @@ async function removeKnownStickers(allStickers) { let rowid = 0; while (!complete) { - // eslint-disable-next-line no-await-in-loop const rows = await db.all( `SELECT rowid, path FROM stickers WHERE rowid > $rowid @@ -3581,12 +3877,13 @@ async function removeKnownStickers(allStickers) { } ); - const files = map(rows, row => row.path); + const files: Array = map(rows, row => row.path); forEach(files, file => { + // tslint:disable-next-line no-dynamic-delete delete lookup[file]; }); - const lastSticker = last(rows); + const lastSticker: StickerType | undefined = last(rows); if (lastSticker) { ({ rowid } = lastSticker); } @@ -3599,8 +3896,11 @@ async function removeKnownStickers(allStickers) { return Object.keys(lookup); } -async function removeKnownDraftAttachments(allStickers) { - const lookup = fromPairs(map(allStickers, file => [file, true])); +async function removeKnownDraftAttachments(allStickers: Array) { + const db = getInstance(); + const lookup: Dictionary = fromPairs( + map(allStickers, file => [file, true]) + ); const chunkSize = 50; const total = await getConversationCount(); @@ -3615,7 +3915,6 @@ async function removeKnownDraftAttachments(allStickers) { let id = 0; while (!complete) { - // eslint-disable-next-line no-await-in-loop const rows = await db.all( `SELECT json FROM conversations WHERE id > $id @@ -3627,15 +3926,18 @@ async function removeKnownDraftAttachments(allStickers) { } ); - const conversations = map(rows, row => jsonToObject(row.json)); + const conversations: Array = map(rows, row => + jsonToObject(row.json) + ); forEach(conversations, conversation => { const externalFiles = getExternalDraftFilesForConversation(conversation); forEach(externalFiles, file => { + // tslint:disable-next-line no-dynamic-delete delete lookup[file]; }); }); - const lastMessage = last(conversations); + const lastMessage: ConversationType | undefined = last(conversations); if (lastMessage) { ({ id } = lastMessage); } diff --git a/ts/sqlcipher.d.ts b/ts/sqlcipher.d.ts new file mode 100644 index 000000000..c086a4cd2 --- /dev/null +++ b/ts/sqlcipher.d.ts @@ -0,0 +1,181 @@ +// Taken from: +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8bf8aedba75ada257428c4846d2bc7d14e3b4be8/types/sqlite3/index.d.ts + +declare module '@journeyapps/sqlcipher' { + // Type definitions for sqlite3 3.1 + // Project: http://github.com/mapbox/node-sqlite3 + // Definitions by: Nick Malaguti + // Sumant Manne + // Behind The Math + // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + /// + + import events = require('events'); + + export const OPEN_READONLY: number; + export const OPEN_READWRITE: number; + export const OPEN_CREATE: number; + export const OPEN_SHAREDCACHE: number; + export const OPEN_PRIVATECACHE: number; + export const OPEN_URI: number; + + export const cached: { + Database( + filename: string, + callback?: (this: Database, err: Error | null) => void + ): Database; + Database( + filename: string, + mode?: number, + callback?: (this: Database, err: Error | null) => void + ): Database; + }; + + export interface RunResult extends Statement { + lastID: number; + changes: number; + } + + export class Statement { + bind(callback?: (err: Error | null) => void): this; + bind(...params: any[]): this; + + reset(callback?: (err: null) => void): this; + + finalize(callback?: (err: Error) => void): Database; + + run(callback?: (err: Error | null) => void): this; + run( + params: any, + callback?: (this: RunResult, err: Error | null) => void + ): this; + run(...params: any[]): this; + + get(callback?: (err: Error | null, row?: any) => void): this; + get( + params: any, + callback?: (this: RunResult, err: Error | null, row?: any) => void + ): this; + get(...params: any[]): this; + + all(callback?: (err: Error | null, rows: any[]) => void): this; + all( + params: any, + callback?: (this: RunResult, err: Error | null, rows: any[]) => void + ): this; + all(...params: any[]): this; + + each( + callback?: (err: Error | null, row: any) => void, + complete?: (err: Error | null, count: number) => void + ): this; + each( + params: any, + callback?: (this: RunResult, err: Error | null, row: any) => void, + complete?: (err: Error | null, count: number) => void + ): this; + each(...params: any[]): this; + } + + export class Database extends events.EventEmitter { + constructor(filename: string, callback?: (err: Error | null) => void); + constructor( + filename: string, + mode?: number, + callback?: (err: Error | null) => void + ); + + close(callback?: (err: Error | null) => void): void; + + run( + sql: string, + callback?: (this: RunResult, err: Error | null) => void + ): this; + run( + sql: string, + params: any, + callback?: (this: RunResult, err: Error | null) => void + ): this; + run(sql: string, ...params: any[]): this; + + get( + sql: string, + callback?: (this: Statement, err: Error | null, row: any) => void + ): this; + get( + sql: string, + params: any, + callback?: (this: Statement, err: Error | null, row: any) => void + ): this; + get(sql: string, ...params: any[]): this; + + all( + sql: string, + callback?: (this: Statement, err: Error | null, rows: any[]) => void + ): this; + all( + sql: string, + params: any, + callback?: (this: Statement, err: Error | null, rows: any[]) => void + ): this; + all(sql: string, ...params: any[]): this; + + each( + sql: string, + callback?: (this: Statement, err: Error | null, row: any) => void, + complete?: (err: Error | null, count: number) => void + ): this; + each( + sql: string, + params: any, + callback?: (this: Statement, err: Error | null, row: any) => void, + complete?: (err: Error | null, count: number) => void + ): this; + each(sql: string, ...params: any[]): this; + + exec( + sql: string, + callback?: (this: Statement, err: Error | null) => void + ): this; + + prepare( + sql: string, + callback?: (this: Statement, err: Error | null) => void + ): Statement; + prepare( + sql: string, + params: any, + callback?: (this: Statement, err: Error | null) => void + ): Statement; + prepare(sql: string, ...params: any[]): Statement; + + serialize(callback?: () => void): void; + parallelize(callback?: () => void): void; + + on(event: 'trace', listener: (sql: string) => void): this; + on(event: 'profile', listener: (sql: string, time: number) => void): this; + on(event: 'error', listener: (err: Error) => void): this; + on(event: 'open' | 'close', listener: () => void): this; + on(event: string, listener: (...args: any[]) => void): this; + + configure(option: 'busyTimeout', value: number): void; + interrupt(): void; + } + + export function verbose(): sqlite3; + + export interface sqlite3 { + OPEN_READONLY: number; + OPEN_READWRITE: number; + OPEN_CREATE: number; + OPEN_SHAREDCACHE: number; + OPEN_PRIVATECACHE: number; + OPEN_URI: number; + cached: typeof cached; + RunResult: RunResult; + Statement: typeof Statement; + Database: typeof Database; + verbose(): this; + } +} diff --git a/ts/state/ducks/emojis.ts b/ts/state/ducks/emojis.ts index fcad5ac01..1f689ce49 100644 --- a/ts/state/ducks/emojis.ts +++ b/ts/state/ducks/emojis.ts @@ -1,6 +1,8 @@ import { take, uniq } from 'lodash'; import { EmojiPickDataType } from '../../components/emoji/EmojiPicker'; -import { updateEmojiUsage } from '../../../js/modules/data'; +import dataInterface from '../../sql/Client'; + +const { updateEmojiUsage } = dataInterface; // State diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 0b05ec695..f31a03c68 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -3,11 +3,7 @@ import { omit, reject } from 'lodash'; import { normalize } from '../../types/PhoneNumber'; import { trigger } from '../../shims/events'; import { cleanSearchTerm } from '../../util/cleanSearchTerm'; -import { - searchConversations as dataSearchConversations, - searchMessages as dataSearchMessages, - searchMessagesInConversation, -} from '../../../js/modules/data'; +import dataInterface from '../../sql/Client'; import { makeLookup } from '../../util/makeLookup'; import { @@ -20,6 +16,12 @@ import { ShowArchivedConversationsActionType, } from './conversations'; +const { + searchConversations: dataSearchConversations, + searchMessages: dataSearchMessages, + searchMessagesInConversation, +} = dataInterface; + // State export type MessageSearchResultType = MessageType & { diff --git a/ts/state/ducks/stickers.ts b/ts/state/ducks/stickers.ts index 2303d662b..472138d3f 100644 --- a/ts/state/ducks/stickers.ts +++ b/ts/state/ducks/stickers.ts @@ -1,9 +1,5 @@ import { Dictionary, omit, reject } from 'lodash'; -import { - getRecentStickers, - updateStickerLastUsed, - updateStickerPackStatus, -} from '../../../js/modules/data'; +import dataInterface from '../../sql/Client'; import { downloadStickerPack as externalDownloadStickerPack, maybeDeletePack, @@ -13,6 +9,12 @@ import { trigger } from '../../shims/events'; import { NoopActionType } from './noop'; +const { + getRecentStickers, + updateStickerLastUsed, + updateStickerPackStatus, +} = dataInterface; + // State export type StickerDBType = { diff --git a/ts/types/I18N.ts b/ts/types/I18N.ts new file mode 100644 index 000000000..3d3a49042 --- /dev/null +++ b/ts/types/I18N.ts @@ -0,0 +1,11 @@ +export type LocaleMessagesType = { + [key: string]: { + message: string; + description?: string; + }; +}; + +export type LocaleType = { + i18n: (key: string, placeholders: Array) => string; + messages: LocaleMessagesType; +}; diff --git a/ts/types/Logging.ts b/ts/types/Logging.ts new file mode 100644 index 000000000..c90163901 --- /dev/null +++ b/ts/types/Logging.ts @@ -0,0 +1,10 @@ +type LogFunction = (...args: Array) => void; + +export type LoggerType = { + fatal: LogFunction; + error: LogFunction; + warn: LogFunction; + info: LogFunction; + debug: LogFunction; + trace: LogFunction; +}; diff --git a/ts/updater/common.ts b/ts/updater/common.ts index c1941eecb..cbec18a44 100644 --- a/ts/updater/common.ts +++ b/ts/updater/common.ts @@ -27,26 +27,8 @@ import { Dialogs } from '../types/Dialogs'; import * as packageJson from '../../package.json'; import { getSignatureFileName } from './signature'; -export type LocaleType = { - i18n: (key: string, placeholders: Array) => string; - messages: { - [key: string]: { - message: string; - description?: string; - }; - }; -}; - -type LogFunction = (...args: Array) => void; - -export type LoggerType = { - fatal: LogFunction; - error: LogFunction; - warn: LogFunction; - info: LogFunction; - debug: LogFunction; - trace: LogFunction; -}; +import { LocaleType } from '../types/I18N'; +import { LoggerType } from '../types/Logging'; const writeFile = pify(writeFileCallback); const mkdirpPromise = pify(mkdirp); diff --git a/ts/updater/index.ts b/ts/updater/index.ts index 17276260e..ee224510b 100644 --- a/ts/updater/index.ts +++ b/ts/updater/index.ts @@ -3,7 +3,8 @@ import { BrowserWindow } from 'electron'; import { start as startMacOS } from './macos'; import { start as startWindows } from './windows'; -import { LocaleType, LoggerType } from './common'; +import { LocaleType } from '../types/I18N'; +import { LoggerType } from '../types/Logging'; let initialized = false; diff --git a/ts/updater/macos.ts b/ts/updater/macos.ts index c175c0837..b020a580a 100644 --- a/ts/updater/macos.ts +++ b/ts/updater/macos.ts @@ -15,11 +15,11 @@ import { deleteTempDir, downloadUpdate, getPrintableError, - LocaleType, - LoggerType, showCannotUpdateDialog, showUpdateDialog, } from './common'; +import { LocaleType } from '../types/I18N'; +import { LoggerType } from '../types/Logging'; import { hexToBinary, verifySignature } from './signature'; import { markShouldQuit } from '../../app/window_state'; import { Dialogs } from '../types/Dialogs'; diff --git a/ts/updater/windows.ts b/ts/updater/windows.ts index ca65147d5..8c95938f5 100644 --- a/ts/updater/windows.ts +++ b/ts/updater/windows.ts @@ -12,11 +12,11 @@ import { deleteTempDir, downloadUpdate, getPrintableError, - LocaleType, - LoggerType, showCannotUpdateDialog, showUpdateDialog, } from './common'; +import { LocaleType } from '../types/I18N'; +import { LoggerType } from '../types/Logging'; import { hexToBinary, verifySignature } from './signature'; import { markShouldQuit } from '../../app/window_state'; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index ae0af7931..150dc5e6b 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -172,7 +172,7 @@ "rule": "jQuery-load(", "path": "js/conversation_controller.js", "line": " this._initialPromise = load();", - "lineNumber": 260, + "lineNumber": 257, "reasonCategory": "falseMatch", "updated": "2020-03-24T20:06:31.391Z" }, diff --git a/ts/window.d.ts b/ts/window.d.ts index e93c5db3e..68c15d4a7 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -12,6 +12,7 @@ declare global { warn: LoggerType; error: LoggerType; }; + restart: () => void; storage: { put: (key: string, value: any) => void; remove: (key: string) => void; @@ -25,6 +26,7 @@ declare global { } export type ConversationControllerType = { + getConversationId: (identifier: string) => string | null; prepareForSend: ( id: string, options: Object @@ -95,4 +97,12 @@ export type WhisperType = { events: { trigger: (name: string, param1: any, param2: any) => void; }; + Database: { + open: () => Promise; + handleDOMException: ( + context: string, + error: DOMException | null, + reject: Function + ) => void; + }; };