Types, better-sqlite3, and worker_threads for our sqlite

This commit is contained in:
Fedor Indutny 2021-04-05 15:18:19 -07:00 committed by Josh Perez
parent fc3004a183
commit 37c8c1727f
24 changed files with 2823 additions and 3121 deletions

View File

@ -5,34 +5,6 @@
Signal Desktop makes use of the following open source projects.
## @journeyapps/sqlcipher
Copyright (c) MapBox
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
- Neither the name "MapBox" nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## @sindresorhus/is
MIT License
@ -154,6 +126,30 @@ Signal Desktop makes use of the following open source projects.
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
## better-sqlite3
The MIT License (MIT)
Copyright (c) 2017 Joshua Wise
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## blob-util
Apache License

View File

@ -2,13 +2,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
const electron = require('electron');
const Queue = require('p-queue').default;
const sql = require('../ts/sql/Server').default;
const { remove: removeUserConfig } = require('./user_config');
const { remove: removeEphemeralConfig } = require('./ephemeral_config');
const { ipcMain } = electron;
let sql;
module.exports = {
initialize,
};
@ -18,99 +18,17 @@ let initialized = false;
const SQL_CHANNEL_KEY = 'sql-channel';
const ERASE_SQL_KEY = 'erase-sql-key';
let singleQueue = null;
let multipleQueue = null;
// Note: we don't want queue timeouts, because delays here are due to in-progress sql
// operations. For example we might try to start a transaction when the prevous isn't
// done, causing that database operation to fail.
function makeNewSingleQueue() {
singleQueue = new Queue({ concurrency: 1 });
return singleQueue;
}
function makeNewMultipleQueue() {
multipleQueue = new Queue({ concurrency: 10 });
return multipleQueue;
}
function makeSQLJob(fn, callName, jobId, args) {
// console.log(`Job ${jobId} (${callName}) queued`);
return async () => {
// const start = Date.now();
// console.log(`Job ${jobId} (${callName}) started`);
const result = await fn(...args);
// const end = Date.now();
// console.log(`Job ${jobId} (${callName}) succeeded in ${end - start}ms`);
return result;
};
}
async function handleCall(callName, jobId, args) {
const fn = sql[callName];
if (!fn) {
throw new Error(`sql channel: ${callName} is not an available function`);
}
let result;
// We queue here to keep multi-query operations atomic. Without it, any multistage
// data operation (even within a BEGIN/COMMIT) can become interleaved, since all
// requests share one database connection.
// A needsSerial method must be run in our single concurrency queue.
if (fn.needsSerial) {
if (singleQueue) {
result = await singleQueue.add(makeSQLJob(fn, callName, jobId, args));
} else if (multipleQueue) {
makeNewSingleQueue();
singleQueue.add(() => multipleQueue.onIdle());
multipleQueue = null;
result = await singleQueue.add(makeSQLJob(fn, callName, jobId, args));
} else {
makeNewSingleQueue();
result = await singleQueue.add(makeSQLJob(fn, callName, jobId, args));
}
} else {
// The request can be parallelized. To keep the same structure as the above block
// we force this section into the 'lonely if' pattern.
// eslint-disable-next-line no-lonely-if
if (multipleQueue) {
result = await multipleQueue.add(makeSQLJob(fn, callName, jobId, args));
} else if (singleQueue) {
makeNewMultipleQueue();
multipleQueue.pause();
const multipleQueueRef = multipleQueue;
const singleQueueRef = singleQueue;
singleQueue = null;
const promise = multipleQueueRef.add(
makeSQLJob(fn, callName, jobId, args)
);
await singleQueueRef.onIdle();
multipleQueueRef.start();
result = await promise;
} else {
makeNewMultipleQueue();
result = await multipleQueue.add(makeSQLJob(fn, callName, jobId, args));
}
}
return result;
}
function initialize() {
function initialize(mainSQL) {
if (initialized) {
throw new Error('sqlChannels: already initialized!');
}
initialized = true;
sql = mainSQL;
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
try {
const result = await handleCall(callName, jobId, args);
const result = await sql.sqlCall(callName, args);
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, null, result);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;

36
main.js
View File

@ -93,7 +93,7 @@ const createTrayIcon = require('./app/tray_icon');
const dockIcon = require('./ts/dock_icon');
const ephemeralConfig = require('./app/ephemeral_config');
const logging = require('./ts/logging/main_process_logging');
const sql = require('./ts/sql/Server').default;
const { MainSQL } = require('./ts/sql/main');
const sqlChannels = require('./app/sql_channel');
const windowState = require('./app/window_state');
const { createTemplate } = require('./app/menu');
@ -119,6 +119,8 @@ const {
} = require('./ts/types/Settings');
const { Environment } = require('./ts/environment');
const sql = new MainSQL();
let appStartInitialSpellcheckSetting = true;
const defaultWebPrefs = {
@ -128,7 +130,7 @@ const defaultWebPrefs = {
};
async function getSpellCheckSetting() {
const json = await sql.getItemById('spell-check');
const json = await sql.sqlCall('getItemById', ['spell-check']);
// Default to `true` if setting doesn't exist yet
if (!json) {
@ -500,6 +502,7 @@ async function createWindow() {
if (mainWindow) {
mainWindow.readyForShutdown = true;
}
await sql.close();
app.quit();
});
@ -765,8 +768,8 @@ function showSettingsWindow() {
async function getIsLinked() {
try {
const number = await sql.getItemById('number_id');
const password = await sql.getItemById('password');
const number = await sql.sqlCall('getItemById', ['number_id']);
const password = await sql.sqlCall('getItemById', ['password']);
return Boolean(number && password);
} catch (e) {
return false;
@ -1090,7 +1093,7 @@ app.on('ready', async () => {
`Database startup error:\n\n${redactAll(error.stack)}`
);
} else {
await sql.removeDB();
await sql.sqlCall('removeDB', []);
removeUserConfig();
app.relaunch();
}
@ -1102,14 +1105,14 @@ app.on('ready', async () => {
// eslint-disable-next-line more/no-then
appStartInitialSpellcheckSetting = await getSpellCheckSetting();
await sqlChannels.initialize();
await sqlChannels.initialize(sql);
try {
const IDB_KEY = 'indexeddb-delete-needed';
const item = await sql.getItemById(IDB_KEY);
const item = await sql.sqlCall('getItemById', [IDB_KEY]);
if (item && item.value) {
await sql.removeIndexedDBFiles();
await sql.removeItemById(IDB_KEY);
await sql.sqlCall('removeIndexedDBFiles', []);
await sql.sqlCall('removeItemById', [IDB_KEY]);
}
} catch (err) {
console.log(
@ -1120,16 +1123,18 @@ app.on('ready', async () => {
async function cleanupOrphanedAttachments() {
const allAttachments = await attachments.getAllAttachments(userDataPath);
const orphanedAttachments = await sql.removeKnownAttachments(
allAttachments
);
const orphanedAttachments = await sql.sqlCall('removeKnownAttachments', [
allAttachments,
]);
await attachments.deleteAll({
userDataPath,
attachments: orphanedAttachments,
});
const allStickers = await attachments.getAllStickers(userDataPath);
const orphanedStickers = await sql.removeKnownStickers(allStickers);
const orphanedStickers = await sql.sqlCall('removeKnownStickers', [
allStickers,
]);
await attachments.deleteAllStickers({
userDataPath,
stickers: orphanedStickers,
@ -1138,8 +1143,9 @@ app.on('ready', async () => {
const allDraftAttachments = await attachments.getAllDraftAttachments(
userDataPath
);
const orphanedDraftAttachments = await sql.removeKnownDraftAttachments(
allDraftAttachments
const orphanedDraftAttachments = await sql.sqlCall(
'removeKnownDraftAttachments',
[allDraftAttachments]
);
await attachments.deleteAllDraftAttachments({
userDataPath,

View File

@ -64,13 +64,13 @@
"fs-xattr": "0.3.0"
},
"dependencies": {
"@journeyapps/sqlcipher": "https://github.com/EvanHahn-signal/node-sqlcipher.git#16916949f0c010f6e6d3d5869b10a0ab813eae75",
"@sindresorhus/is": "0.8.0",
"@types/pino": "6.3.6",
"@types/pino-multi-stream": "5.1.0",
"abort-controller": "3.0.0",
"array-move": "2.1.0",
"backbone": "1.3.3",
"better-sqlite3": "https://github.com/indutny/better-sqlite3#a78376d86b5856c14ab4e2f3995f41e1f80df846",
"blob-util": "1.3.0",
"blueimp-canvas-to-blob": "3.14.0",
"blueimp-load-image": "5.14.0",
@ -171,6 +171,7 @@
"@storybook/addons": "5.1.11",
"@storybook/react": "5.1.11",
"@types/backbone": "1.4.3",
"@types/better-sqlite3": "5.4.1",
"@types/blueimp-load-image": "5.14.1",
"@types/chai": "4.1.2",
"@types/classnames": "2.2.3",
@ -364,6 +365,20 @@
"sgnl"
]
},
"asarUnpack": [
"js/modules/privacy.js",
"ts/environment.js",
"ts/logging/log.js",
"ts/logging/shared.js",
"ts/sql/Server.js",
"ts/sql/mainWorker.js",
"ts/util/assert.js",
"ts/util/combineNames.js",
"ts/util/enum.js",
"ts/util/isNormalNumber.js",
"ts/util/missingCaseError.js",
"ts/util/reallyJsonStringify.js"
],
"files": [
"package.json",
"config/default.json",
@ -405,7 +420,7 @@
"!node_modules/spellchecker/vendor/hunspell/**/*",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme,test,__tests__,tests,powered-test,example,examples,*.d.ts,.snyk-*.flag,benchmark}",
"!**/node_modules/.bin",
"!**/node_modules/*/build/**",
"!**/node_modules/**/build/**",
"!**/*.{o,hprof,orig,pyc,pyo,rbc}",
"!**/._*",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.gitignore,.gitattributes,.flowconfig,.yarn-metadata.json,.idea,appveyor.yml,.travis.yml,circle.yml,npm-debug.log,.nyc_output,yarn.lock,.yarn-integrity}",
@ -413,6 +428,7 @@
"node_modules/websocket/build/Release/*.node",
"!node_modules/websocket/builderror.log",
"node_modules/ref-napi/build/Release/*.node",
"node_modules/ref-array-napi/node_modules/ref-napi/build/Release/*.node",
"node_modules/ffi-napi/build/Release/*.node",
"node_modules/socks/build/*.js",
"node_modules/socks/build/common/*.js",
@ -420,10 +436,9 @@
"node_modules/smart-buffer/build/*.js",
"node_modules/sharp/build/**",
"!node_modules/sharp/{install,src,vendor/include,vendor/*/include}",
"!node_modules/@journeyapps/sqlcipher/deps/*",
"!node_modules/@journeyapps/sqlcipher/build/*",
"!node_modules/@journeyapps/sqlcipher/build-tmp-napi-*",
"!node_modules/@journeyapps/sqlcipher/lib/binding/node-*",
"!node_modules/better-sqlite3/deps/*",
"!node_modules/better-sqlite3/src/*",
"node_modules/better-sqlite3/build/Release/*.node",
"node_modules/libsignal-client/build/*${platform}*.node",
"node_modules/ringrtc/build/${platform}/**",
"!**/node_modules/ffi-napi/deps",

View File

@ -6,6 +6,13 @@
import { fromEncodedBinaryToArrayBuffer, constantTimeEqual } from './Crypto';
import { isNotNil } from './util/isNotNil';
import { isMoreRecentThan } from './util/timestamp';
import {
IdentityKeyType,
SignedPreKeyType,
PreKeyType,
UnprocessedType,
SessionType,
} from './sql/Interface';
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
const Direction = {
@ -126,30 +133,6 @@ type KeyPairType = {
pubKey: ArrayBuffer;
};
type IdentityKeyType = {
firstUse: boolean;
id: string;
nonblockingApproval: boolean;
publicKey: ArrayBuffer;
timestamp: number;
verified: number;
};
type SessionType = {
conversationId: string;
deviceId: number;
id: string;
record: string;
};
type SignedPreKeyType = {
confirmed: boolean;
// eslint-disable-next-line camelcase
created_at: number;
id: number;
privateKey: ArrayBuffer;
publicKey: ArrayBuffer;
};
type OuterSignedPrekeyType = {
confirmed: boolean;
// eslint-disable-next-line camelcase
@ -158,23 +141,6 @@ type OuterSignedPrekeyType = {
privKey: ArrayBuffer;
pubKey: ArrayBuffer;
};
type PreKeyType = {
id: number;
privateKey: ArrayBuffer;
publicKey: ArrayBuffer;
};
type UnprocessedType = {
id: string;
timestamp: number;
version: number;
attempts: number;
envelope: string;
decrypted?: string;
source?: string;
sourceDevice: string;
serverTimestamp: number;
};
// We add a this parameter to avoid an 'implicit any' error on the next line
const EventsMixin = (function EventsMixin(this: unknown) {
@ -1175,7 +1141,7 @@ export class SignalProtocolStore extends EventsMixin {
return window.Signal.Data.getUnprocessedById(id);
}
addUnprocessed(data: UnprocessedType): Promise<number> {
addUnprocessed(data: UnprocessedType): Promise<string> {
// We need to pass forceSave because the data has an id already, which will cause
// an update instead of an insert.
return window.Signal.Data.saveUnprocessed(data, {
@ -1199,7 +1165,9 @@ export class SignalProtocolStore extends EventsMixin {
return window.Signal.Data.updateUnprocessedWithData(id, data);
}
updateUnprocessedsWithData(items: Array<UnprocessedType>): Promise<void> {
updateUnprocessedsWithData(
items: Array<{ id: string; data: UnprocessedType }>
): Promise<void> {
return window.Signal.Data.updateUnprocessedsWithData(items);
}

View File

@ -3071,6 +3071,10 @@ export async function startApp(): Promise<void> {
reconnectTimer = setTimeout(connect, 60000);
window.Whisper.events.trigger('reconnectTimer');
// If we couldn't connect during startup - we should still switch SQL to
// the main process to avoid stalling UI.
window.sqlInitializer.goBackToMainProcess();
}
return;
}

View File

@ -2319,7 +2319,7 @@ export async function joinGroupV2ViaLinkAndMigrate({
derivedGroupV2Id: undefined,
members: undefined,
};
const groupChangeMessages = [
const groupChangeMessages: Array<MessageAttributesType> = [
{
...generateBasicMessage(),
type: 'group-v1-migration',
@ -3018,7 +3018,7 @@ async function generateLeftGroupChanges(
const isNewlyRemoved =
existingMembers.length > (newAttributes.membersV2 || []).length;
const youWereRemovedMessage = {
const youWereRemovedMessage: MessageAttributesType = {
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {

74
ts/model-types.d.ts vendored
View File

@ -63,7 +63,7 @@ export type MessageAttributesType = {
deletedForEveryoneTimestamp?: number;
delivered: number;
delivered_to: Array<string | null>;
errors: Array<CustomError> | null;
errors?: Array<CustomError>;
expirationStartTimestamp: number | null;
expireTimer: number;
expires_at: number;
@ -86,7 +86,7 @@ export type MessageAttributesType = {
message: unknown;
messageTimer: unknown;
profileChange: ProfileNameChangeType;
quote: {
quote?: {
attachments: Array<typeof window.WhatIsThis>;
// `author` is an old attribute that holds the author's E164. We shouldn't use it for
// new messages, but old messages might have this attribute.
@ -96,8 +96,21 @@ export type MessageAttributesType = {
id: string;
referencedMessageNotFound: boolean;
text: string;
} | null;
reactions: Array<{ fromId: string; emoji: string; timestamp: number }>;
};
reactions?: Array<{
emoji: string;
timestamp: number;
fromId: string;
from: {
id: string;
color?: string;
avatarPath?: string;
name?: string;
profileName?: string;
isMe?: boolean;
phoneNumber?: string;
};
}>;
read_by: Array<string | null>;
requiredProtocolVersion: number;
sent: boolean;
@ -110,7 +123,19 @@ export type MessageAttributesType = {
verifiedChanged: string;
id: string;
type?: string;
type?:
| 'incoming'
| 'outgoing'
| 'group'
| 'keychange'
| 'verified-change'
| 'message-history-unsynced'
| 'call-history'
| 'chat-session-refreshed'
| 'group-v1-migration'
| 'group-v2-change'
| 'profile-change'
| 'timer-notification';
body: string;
attachments: Array<WhatIsThis>;
preview: Array<WhatIsThis>;
@ -135,7 +160,7 @@ export type MessageAttributesType = {
flags?: number;
groupV2Change?: GroupV2ChangeType;
// Required. Used to sort messages in the database for the conversation timeline.
received_at?: number;
received_at: number;
received_at_ms?: number;
// More of a legacy feature, needed as we were updating the schema of messages in the
// background, when we were still in IndexedDB, before attachments had gone to disk
@ -145,7 +170,7 @@ export type MessageAttributesType = {
source?: string;
sourceUuid?: string;
unread: number;
unread: boolean;
timestamp: number;
// Backwards-compatibility with prerelease data schema
@ -156,34 +181,37 @@ export type MessageAttributesType = {
export type ConversationAttributesTypeType = 'private' | 'group';
export type ConversationAttributesType = {
accessKey: string | null;
accessKey?: string | null;
addedBy?: string;
capabilities?: CapabilitiesType;
color?: string;
discoveredUnregisteredAt?: number;
draftAttachments: Array<unknown>;
draftBodyRanges: Array<BodyRangeType>;
draftTimestamp: number | null;
draftAttachments?: Array<{
path?: string;
screenshotPath?: string;
}>;
draftBodyRanges?: Array<BodyRangeType>;
draftTimestamp?: number | null;
inbox_position: number;
isPinned: boolean;
lastMessageDeletedForEveryone: boolean;
lastMessageStatus: LastMessageStatus | null;
lastMessageStatus?: LastMessageStatus | null;
markedUnread: boolean;
messageCount: number;
messageCountBeforeMessageRequests: number | null;
messageRequestResponseType: number;
muteExpiresAt: number | undefined;
profileAvatar: WhatIsThis;
profileKeyCredential: string | null;
profileKeyVersion: string | null;
quotedMessageId: string | null;
sealedSender: unknown;
messageCountBeforeMessageRequests?: number | null;
messageRequestResponseType?: number;
muteExpiresAt?: number;
profileAvatar?: WhatIsThis;
profileKeyCredential?: string | null;
profileKeyVersion?: string | null;
quotedMessageId?: string | null;
sealedSender?: unknown;
sentMessageCount: number;
sharedGroupNames: Array<string>;
sharedGroupNames?: Array<string>;
id: string;
type: ConversationAttributesTypeType;
timestamp: number | null;
timestamp?: number | null;
// Shared fields
active_at?: number | null;
@ -217,7 +245,7 @@ export type ConversationAttributesType = {
// A shorthand, representing whether the user is part of the group. Not strictly for
// when the user manually left the group. But historically, that was the only way
// to leave a group.
left: boolean;
left?: boolean;
groupVersion?: number;
// GroupV1 only

View File

@ -1388,7 +1388,7 @@ export class ConversationModel extends window.Backbone.Model<
profileSharing: this.get('profileSharing'),
publicParams: this.get('publicParams'),
secretParams: this.get('secretParams'),
sharedGroupNames: this.get('sharedGroupNames')!,
sharedGroupNames: this.get('sharedGroupNames'),
shouldShowDraft,
sortedGroupMembers,
timestamp,
@ -2574,7 +2574,7 @@ export class ConversationModel extends window.Backbone.Model<
sent_at: now,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: now,
unread: true,
unread: 1,
changedId: conversationId || this.id,
profileChange,
// TODO: DESKTOP-722

View File

@ -1745,7 +1745,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
body: '',
bodyRanges: undefined,
attachments: [],
quote: null,
quote: undefined,
contact: [],
sticker: null,
preview: [],
@ -2034,7 +2034,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return null;
}
this.set({ errors: null });
this.set({ errors: undefined });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conversation = this.getConversation()!;
@ -3934,7 +3934,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.clearNotifications(reaction.get('fromId'));
}
const newCount = this.get('reactions').length;
const newCount = (this.get('reactions') || []).length;
window.log.info(
`Done processing reaction for message ${messageId}. Went from ${count} to ${newCount} reactions.`
);

View File

@ -36,6 +36,7 @@ import {
import {
AttachmentDownloadJobType,
ClientInterface,
ClientSearchResultMessageType,
ClientJobType,
ConversationType,
IdentityKeyType,
@ -55,7 +56,6 @@ import {
import Server from './Server';
import { MessageModel } from '../models/messages';
import { ConversationModel } from '../models/conversations';
import { waitForPendingQueries } from './Queueing';
// 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.
@ -243,12 +243,14 @@ const dataInterface: ClientInterface = {
export default dataInterface;
async function goBackToMainProcess(): Promise<void> {
window.log.info('data.goBackToMainProcess: waiting for pending queries');
// Let pending queries finish before we'll give write access to main process.
// We don't want to be writing from two processes at the same time!
await waitForPendingQueries();
if (!shouldUseRendererProcess) {
window.log.info(
'data.goBackToMainProcess: already switched to main process'
);
return;
}
// We don't need to wait for pending queries since they are synchronous.
window.log.info('data.goBackToMainProcess: switching to main process');
shouldUseRendererProcess = false;
@ -514,8 +516,6 @@ function keysFromArrayBuffer(keys: Array<string>, data: any) {
// Top-level calls
async function shutdown() {
await waitForPendingQueries();
// Stop accepting new SQL jobs, flush outstanding queue
await _shutdown();
@ -761,7 +761,13 @@ const updateConversationBatcher = createBatcher<ConversationType>({
// We only care about the most recent update for each conversation
const byId = groupBy(items, item => item.id);
const ids = Object.keys(byId);
const mostRecent = ids.map(id => last(byId[id]));
const mostRecent = ids.map(
(id: string): ConversationType => {
const maybeLast = last(byId[id]);
assert(maybeLast !== undefined, 'Empty array in `groupBy` result');
return maybeLast;
}
);
await updateConversations(mostRecent);
},
@ -857,9 +863,13 @@ async function searchConversations(query: string) {
return conversations;
}
function handleSearchMessageJSON(messages: Array<SearchResultMessageType>) {
function handleSearchMessageJSON(
messages: Array<SearchResultMessageType>
): Array<ClientSearchResultMessageType> {
return messages.map(message => ({
json: message.json,
...JSON.parse(message.json),
bodyRanges: [],
snippet: message.snippet,
}));
}
@ -940,7 +950,7 @@ async function getMessageById(
) {
const message = await channels.getMessageById(id);
if (!message) {
return null;
return undefined;
}
return new Message(message);
@ -1262,7 +1272,9 @@ async function updateUnprocessedAttempts(id: string, attempts: number) {
async function updateUnprocessedWithData(id: string, data: UnprocessedType) {
await channels.updateUnprocessedWithData(id, data);
}
async function updateUnprocessedsWithData(array: Array<UnprocessedType>) {
async function updateUnprocessedsWithData(
array: Array<{ id: string; data: UnprocessedType }>
) {
await channels.updateUnprocessedsWithData(array);
}

View File

@ -4,31 +4,129 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable camelcase */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { LocaleMessagesType } from '../types/I18N';
import {
ConversationAttributesType,
ConversationModelCollectionType,
MessageAttributesType,
MessageModelCollectionType,
} from '../model-types.d';
import { MessageModel } from '../models/messages';
import { ConversationModel } from '../models/conversations';
export type AttachmentDownloadJobType = any;
export type ConverationMetricsType = any;
export type ConversationType = any;
export type EmojiType = any;
export type IdentityKeyType = any;
export type AttachmentDownloadJobType = {
id: string;
timestamp: number;
pending: number;
attempts: number;
};
export type MessageMetricsType = {
id: string;
// eslint-disable-next-line camelcase
received_at: number;
// eslint-disable-next-line camelcase
sent_at: number;
};
export type ConversationMetricsType = {
oldest?: MessageMetricsType;
newest?: MessageMetricsType;
oldestUnread?: MessageMetricsType;
totalUnread: number;
};
export type ConversationType = ConversationAttributesType;
export type EmojiType = {
shortName: string;
lastUsage: number;
};
export type IdentityKeyType = {
firstUse: boolean;
id: string;
nonblockingApproval: boolean;
publicKey: ArrayBuffer;
timestamp: number;
verified: number;
};
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 MessageType = MessageAttributesType;
export type MessageTypeUnhydrated = {
json: string;
};
export type PreKeyType = {
id: number;
privateKey: ArrayBuffer;
publicKey: ArrayBuffer;
};
export type SearchResultMessageType = {
json: string;
snippet: string;
};
export type ClientSearchResultMessageType = MessageType & {
json: string;
bodyRanges: [];
snippet: string;
};
export type SessionType = {
id: string;
conversationId: string;
deviceId: number;
record: string;
};
export type SignedPreKeyType = {
confirmed: boolean;
// eslint-disable-next-line camelcase
created_at: number;
id: number;
privateKey: ArrayBuffer;
publicKey: ArrayBuffer;
};
export type StickerPackStatusType =
| 'known'
| 'ephemeral'
| 'downloaded'
| 'installed'
| 'pending'
| 'error';
export type StickerType = {
id: number;
packId: string;
emoji: string;
isCoverOnly: string;
lastUsed: number;
path: string;
width: number;
height: number;
};
export type StickerPackType = {
id: string;
key: string;
attemptedStatus: 'downloaded' | 'installed' | 'ephemeral';
author: string;
coverStickerId: number;
createdAt: number;
downloadAttempts: number;
installedAt: number | null;
lastUsed: number;
status: StickerPackStatusType;
stickerCount: number;
stickers: ReadonlyArray<string>;
title: string;
};
export type UnprocessedType = {
id: string;
timestamp: number;
version: number;
attempts: number;
envelope: string;
source?: string;
sourceUuid?: string;
sourceDevice?: string;
serverTimestamp?: number;
decrypted?: string;
};
export type DataInterface = {
close: () => Promise<void>;
@ -84,15 +182,6 @@ export type DataInterface = {
query: string,
options?: { limit?: number }
) => Promise<Array<ConversationType>>;
searchMessages: (
query: string,
options?: { limit?: number }
) => Promise<Array<SearchResultMessageType>>;
searchMessagesInConversation: (
query: string,
conversationId: string,
options?: { limit?: number }
) => Promise<Array<SearchResultMessageType>>;
getMessageCount: (conversationId?: string) => Promise<number>;
saveMessages: (
@ -102,7 +191,7 @@ export type DataInterface = {
getAllMessageIds: () => Promise<Array<string>>;
getMessageMetricsForConversation: (
conversationId: string
) => Promise<ConverationMetricsType>;
) => Promise<ConversationMetricsType>;
hasGroupCallHistoryMessage: (
conversationId: string,
eraId: string
@ -117,13 +206,15 @@ export type DataInterface = {
saveUnprocessed: (
data: UnprocessedType,
options?: { forceSave?: boolean }
) => Promise<number>;
) => Promise<string>;
updateUnprocessedAttempts: (id: string, attempts: number) => Promise<void>;
updateUnprocessedWithData: (
id: string,
data: UnprocessedType
) => Promise<void>;
updateUnprocessedsWithData: (array: Array<UnprocessedType>) => Promise<void>;
updateUnprocessedsWithData: (
array: Array<{ id: string; data: UnprocessedType }>
) => Promise<void>;
getUnprocessedById: (id: string) => Promise<UnprocessedType | undefined>;
saveUnprocesseds: (
arrayOfUnprocessed: Array<UnprocessedType>,
@ -203,7 +294,7 @@ export type ServerInterface = DataInterface & {
getAllConversations: () => Promise<Array<ConversationType>>;
getAllGroupsInvolvingId: (id: string) => Promise<Array<ConversationType>>;
getAllPrivateConversations: () => Promise<Array<ConversationType>>;
getConversationById: (id: string) => Promise<ConversationType | null>;
getConversationById: (id: string) => Promise<ConversationType | undefined>;
getExpiredMessages: () => Promise<Array<MessageType>>;
getMessageById: (id: string) => Promise<MessageType | undefined>;
getMessageBySender: (options: {
@ -234,8 +325,8 @@ export type ServerInterface = DataInterface & {
conversationId: string;
ourConversationId: string;
}) => Promise<MessageType | undefined>;
getNextExpiringMessage: () => Promise<MessageType>;
getNextTapToViewMessageToAgeOut: () => Promise<MessageType>;
getNextExpiringMessage: () => Promise<MessageType | undefined>;
getNextTapToViewMessageToAgeOut: () => Promise<MessageType | undefined>;
getOutgoingWithoutExpiresAt: () => Promise<Array<MessageType>>;
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
getUnreadByConversation: (
@ -244,6 +335,15 @@ export type ServerInterface = DataInterface & {
removeConversation: (id: Array<string> | string) => Promise<void>;
removeMessage: (id: string) => Promise<void>;
removeMessages: (ids: Array<string>) => Promise<void>;
searchMessages: (
query: string,
options?: { limit?: number }
) => Promise<Array<SearchResultMessageType>>;
searchMessagesInConversation: (
query: string,
conversationId: string,
options?: { limit?: number }
) => Promise<Array<SearchResultMessageType>>;
saveMessage: (
data: MessageType,
options: { forceSave?: boolean }
@ -255,11 +355,7 @@ export type ServerInterface = DataInterface & {
// Server-only
initialize: (options: {
configDir: string;
key: string;
messages: LocaleMessagesType;
}) => Promise<void>;
initialize: (options: { configDir: string; key: string }) => Promise<void>;
initializeRenderer: (options: {
configDir: string;
@ -298,7 +394,7 @@ export type ClientInterface = DataInterface & {
getMessageById: (
id: string,
options: { Message: typeof MessageModel }
) => Promise<MessageType | undefined>;
) => Promise<MessageModel | undefined>;
getMessageBySender: (
data: {
source: string;
@ -373,6 +469,15 @@ export type ClientInterface = DataInterface & {
data: MessageType,
options: { forceSave?: boolean; Message: typeof MessageModel }
) => Promise<string>;
searchMessages: (
query: string,
options?: { limit?: number }
) => Promise<Array<ClientSearchResultMessageType>>;
searchMessagesInConversation: (
query: string,
conversationId: string,
options?: { limit?: number }
) => Promise<Array<ClientSearchResultMessageType>>;
updateConversation: (data: ConversationType, extra?: unknown) => void;
// Test-only

View File

@ -1,141 +0,0 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Queue from 'p-queue';
import { ServerInterface } from './Interface';
let allQueriesDone: () => void | undefined;
let sqlQueries = 0;
let singleQueue: Queue | null = null;
let multipleQueue: Queue | null = null;
// Note: we don't want queue timeouts, because delays here are due to in-progress sql
// operations. For example we might try to start a transaction when the previous isn't
// done, causing that database operation to fail.
function makeNewSingleQueue(): Queue {
singleQueue = new Queue({ concurrency: 1 });
return singleQueue;
}
function makeNewMultipleQueue(): Queue {
multipleQueue = new Queue({ concurrency: 10 });
return multipleQueue;
}
const DEBUG = false;
function makeSQLJob(
fn: ServerInterface[keyof ServerInterface],
args: Array<unknown>,
callName: keyof ServerInterface
) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log(`SQL(${callName}) queued`);
}
return async () => {
sqlQueries += 1;
const start = Date.now();
if (DEBUG) {
// eslint-disable-next-line no-console
console.log(`SQL(${callName}) started`);
}
let result;
try {
// Ignoring this error TS2556: Expected 3 arguments, but got 0 or more.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
result = await fn(...args);
} finally {
sqlQueries -= 1;
if (allQueriesDone && sqlQueries <= 0) {
allQueriesDone();
}
}
const end = Date.now();
const delta = end - start;
if (DEBUG || delta > 10) {
// eslint-disable-next-line no-console
console.log(`SQL(${callName}) succeeded in ${end - start}ms`);
}
return result;
};
}
async function handleCall(
fn: ServerInterface[keyof ServerInterface],
args: Array<unknown>,
callName: keyof ServerInterface
) {
if (!fn) {
throw new Error(`sql channel: ${callName} is not an available function`);
}
let result;
// We queue here to keep multi-query operations atomic. Without it, any multistage
// data operation (even within a BEGIN/COMMIT) can become interleaved, since all
// requests share one database connection.
// A needsSerial method must be run in our single concurrency queue.
if (fn.needsSerial) {
if (singleQueue) {
result = await singleQueue.add(makeSQLJob(fn, args, callName));
} else if (multipleQueue) {
const queue = makeNewSingleQueue();
const multipleQueueLocal = multipleQueue;
queue.add(() => multipleQueueLocal.onIdle());
multipleQueue = null;
result = await queue.add(makeSQLJob(fn, args, callName));
} else {
const queue = makeNewSingleQueue();
result = await queue.add(makeSQLJob(fn, args, callName));
}
} else {
// The request can be parallelized. To keep the same structure as the above block
// we force this section into the 'lonely if' pattern.
// eslint-disable-next-line no-lonely-if
if (multipleQueue) {
result = await multipleQueue.add(makeSQLJob(fn, args, callName));
} else if (singleQueue) {
const queue = makeNewMultipleQueue();
queue.pause();
const singleQueueRef = singleQueue;
singleQueue = null;
const promise = queue.add(makeSQLJob(fn, args, callName));
if (singleQueueRef) {
await singleQueueRef.onIdle();
}
queue.start();
result = await promise;
} else {
const queue = makeNewMultipleQueue();
result = await queue.add(makeSQLJob(fn, args, callName));
}
}
return result;
}
export async function waitForPendingQueries(): Promise<void> {
return new Promise<void>(resolve => {
if (sqlQueries === 0) {
resolve();
} else {
allQueriesDone = () => resolve();
}
});
}
export function applyQueueing(dataInterface: ServerInterface): ServerInterface {
return Object.keys(dataInterface).reduce((acc, callName) => {
const serverInterfaceKey = callName as keyof ServerInterface;
acc[serverInterfaceKey] = async (...args: Array<unknown>) =>
handleCall(dataInterface[serverInterfaceKey], args, serverInterfaceKey);
return acc;
}, {} as ServerInterface);
}

File diff suppressed because it is too large Load Diff

113
ts/sql/main.ts Normal file
View File

@ -0,0 +1,113 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { join } from 'path';
import { Worker } from 'worker_threads';
export type InitializeOptions = {
readonly configDir: string;
readonly key: string;
};
export type WorkerRequest =
| {
readonly type: 'init';
readonly options: InitializeOptions;
}
| {
readonly type: 'close';
}
| {
readonly type: 'sqlCall';
readonly method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly args: ReadonlyArray<any>;
};
export type WrappedWorkerRequest = {
readonly seq: number;
readonly request: WorkerRequest;
};
export type WrappedWorkerResponse = {
readonly seq: number;
readonly error: string | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly response: any;
};
type PromisePair<T> = {
resolve: (response: T) => void;
reject: (error: Error) => void;
};
export class MainSQL {
private readonly worker: Worker;
private readonly onExit: Promise<void>;
private seq = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private onResponse = new Map<number, PromisePair<any>>();
constructor() {
const appDir = join(__dirname, '..', '..').replace(
/app\.asar$/,
'app.asar.unpacked'
);
this.worker = new Worker(join(appDir, 'ts', 'sql', 'mainWorker.js'));
this.worker.on('message', (wrappedResponse: WrappedWorkerResponse) => {
const { seq, error, response } = wrappedResponse;
const pair = this.onResponse.get(seq);
this.onResponse.delete(seq);
if (!pair) {
throw new Error(`Unexpected worker response with seq: ${seq}`);
}
if (error) {
pair.reject(new Error(error));
} else {
pair.resolve(response);
}
});
this.onExit = new Promise<void>(resolve => {
this.worker.once('exit', resolve);
});
}
public async initialize(options: InitializeOptions): Promise<void> {
return this.send({ type: 'init', options });
}
public async close(): Promise<void> {
await this.send({ type: 'close' });
await this.onExit;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async sqlCall(method: string, args: ReadonlyArray<any>): Promise<any> {
return this.send({ type: 'sqlCall', method, args });
}
private async send<Response>(request: WorkerRequest): Promise<Response> {
const { seq } = this;
this.seq += 1;
const result = new Promise<Response>((resolve, reject) => {
this.onResponse.set(seq, { resolve, reject });
});
const wrappedRequest: WrappedWorkerRequest = {
seq,
request,
};
this.worker.postMessage(wrappedRequest);
return result;
}
}

56
ts/sql/mainWorker.ts Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { parentPort } from 'worker_threads';
import { WrappedWorkerRequest, WrappedWorkerResponse } from './main';
import db from './Server';
if (!parentPort) {
throw new Error('Must run as a worker thread');
}
const port = parentPort;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function respond(seq: number, error: Error | undefined, response?: any) {
const wrappedResponse: WrappedWorkerResponse = {
seq,
error: error ? error.stack : undefined,
response,
};
port.postMessage(wrappedResponse);
}
port.on('message', async ({ seq, request }: WrappedWorkerRequest) => {
try {
if (request.type === 'init') {
await db.initialize(request.options);
respond(seq, undefined, undefined);
return;
}
if (request.type === 'close') {
await db.close();
respond(seq, undefined, undefined);
process.exit(0);
return;
}
if (request.type === 'sqlCall') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const method = (db as any)[request.method];
if (typeof method !== 'function') {
throw new Error(`Invalid sql method: ${method}`);
}
respond(seq, undefined, await method.apply(db, request.args));
} else {
throw new Error('Unexpected request type');
}
} catch (error) {
respond(seq, error, undefined);
}
});

184
ts/sqlcipher.d.ts vendored
View File

@ -1,184 +0,0 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// 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 <https://github.com/nmalaguti>
// Sumant Manne <https://github.com/dpyro>
// Behind The Math <https://github.com/BehindTheMath>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
/// <reference types="node" />
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 type RunResult = 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;
}
}

View File

@ -42,7 +42,7 @@ import { toggleSelectedContactForGroupAddition } from '../../groups/toggleSelect
export type DBConversationType = {
id: string;
activeAt?: number;
lastMessage: string;
lastMessage?: string | null;
type: string;
};
@ -138,19 +138,28 @@ export type ConversationType = {
export type ConversationLookupType = {
[key: string]: ConversationType;
};
export type CustomError = Error & {
identifier?: string;
number?: string;
};
export type MessageType = {
id: string;
conversationId: string;
source?: string;
sourceUuid?: string;
type:
type?:
| 'incoming'
| 'outgoing'
| 'group'
| 'keychange'
| 'verified-change'
| 'message-history-unsynced'
| 'call-history';
| 'call-history'
| 'chat-session-refreshed'
| 'group-v1-migration'
| 'group-v2-change'
| 'profile-change'
| 'timer-notification';
quote?: { author?: string; authorUuid?: string };
received_at: number;
sent_at?: number;
@ -179,7 +188,7 @@ export type MessageType = {
}>;
deletedForEveryone?: boolean;
errors?: Array<Error>;
errors?: Array<CustomError>;
group_update?: unknown;
callHistoryDetails?: CallHistoryDetailsFromDiskType;

View File

@ -5,6 +5,10 @@ import { omit, reject } from 'lodash';
import { normalize } from '../../types/PhoneNumber';
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import {
ClientSearchResultMessageType,
ClientInterface,
} from '../../sql/Interface';
import dataInterface from '../../sql/Client';
import { makeLookup } from '../../util/makeLookup';
import { BodyRangesType } from '../../types/Util';
@ -23,7 +27,7 @@ const {
searchConversations: dataSearchConversations,
searchMessages: dataSearchMessages,
searchMessagesInConversation,
} = dataInterface;
}: ClientInterface = dataInterface;
// State
@ -244,7 +248,10 @@ function updateSearchTerm(query: string): UpdateSearchTermActionType {
};
}
async function queryMessages(query: string, searchConversationId?: string) {
async function queryMessages(
query: string,
searchConversationId?: string
): Promise<Array<ClientSearchResultMessageType>> {
try {
const normalized = cleanSearchTerm(query);

View File

@ -36,7 +36,7 @@ const Hangul_Syllables = /[\uAC00-\uD7AF]/;
const isIdeographic = /[\u3006\u3007\u3021-\u3029\u3038-\u303A\u3400-\u4DB5\u4E00-\u9FEF\uF900-\uFA6D\uFA70-\uFAD9]|[\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD821[\uDC00-\uDFF7]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDD70-\uDEFB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/;
export function combineNames(
given: string,
given?: string,
family?: string
): undefined | string {
if (!given) {

View File

@ -10236,78 +10236,6 @@
"reasonCategory": "falseMatch",
"updated": "2019-07-19T17:16:02.404Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/node-pre-gyp/node_modules/debug/dist/debug.js",
"line": " createDebug.enable(createDebug.load());",
"lineNumber": 694,
"reasonCategory": "falseMatch",
"updated": "2021-01-21T16:16:34.352Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/node-pre-gyp/node_modules/debug/dist/debug.js",
"line": " function load() {",
"lineNumber": 828,
"reasonCategory": "falseMatch",
"updated": "2021-01-21T16:16:34.352Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/node-pre-gyp/node_modules/debug/src/browser.js",
"line": "function load() {",
"lineNumber": 129,
"reasonCategory": "falseMatch",
"updated": "2021-01-21T16:16:34.352Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/node-pre-gyp/node_modules/debug/src/common.js",
"line": " createDebug.enable(createDebug.load());",
"lineNumber": 244,
"reasonCategory": "falseMatch",
"updated": "2021-01-21T16:16:34.352Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/node-pre-gyp/node_modules/debug/src/node.js",
"line": "function load() {",
"lineNumber": 135,
"reasonCategory": "falseMatch",
"updated": "2021-01-21T16:16:34.352Z"
},
{
"rule": "jQuery-append(",
"path": "node_modules/node-pre-gyp/node_modules/needle/lib/multipart.js",
"line": " function append(data, filename) {",
"lineNumber": 42,
"reasonCategory": "falseMatch",
"updated": "2020-04-30T22:35:27.860Z"
},
{
"rule": "jQuery-append(",
"path": "node_modules/node-pre-gyp/node_modules/needle/lib/multipart.js",
"line": " if (part.buffer) return append(part.buffer, filename);",
"lineNumber": 58,
"reasonCategory": "falseMatch",
"updated": "2020-04-30T22:35:27.860Z"
},
{
"rule": "jQuery-append(",
"path": "node_modules/node-pre-gyp/node_modules/needle/lib/multipart.js",
"line": " append(data, filename);",
"lineNumber": 62,
"reasonCategory": "falseMatch",
"updated": "2020-04-30T22:35:27.860Z"
},
{
"rule": "jQuery-append(",
"path": "node_modules/node-pre-gyp/node_modules/needle/lib/multipart.js",
"line": " append();",
"lineNumber": 77,
"reasonCategory": "falseMatch",
"updated": "2020-04-30T22:35:27.860Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/nugget/node_modules/ajv/dist/ajv.min.js",
@ -15681,7 +15609,7 @@
"rule": "jQuery-load(",
"path": "ts/LibSignalStore.ts",
"line": " await window.ConversationController.load();",
"lineNumber": 1222,
"lineNumber": 1190,
"reasonCategory": "falseMatch",
"updated": "2021-02-27T00:48:49.313Z"
},
@ -16785,4 +16713,4 @@
"updated": "2021-01-08T15:46:32.143Z",
"reasonDetail": "Doesn't manipulate the DOM. This is just a function."
}
]
]

View File

@ -44,6 +44,8 @@ const basePath = join(__dirname, '../../..');
const searchPattern = normalizePath(join(basePath, '**/*.{js,ts,tsx}'));
const excludedFilesRegexps = [
'^release/',
// Non-distributed files
'\\.d\\.ts$',

View File

@ -1138,7 +1138,7 @@ Whisper.ConversationView = Whisper.View.extend({
// If newest in-memory message is unread, scrolling down would mean going to
// the very bottom, not the oldest unread.
if (newestInMemoryMessage.isUnread()) {
if (newestInMemoryMessage && newestInMemoryMessage.isUnread()) {
scrollToLatestUnread = false;
}
}
@ -3247,9 +3247,13 @@ Whisper.ConversationView = Whisper.View.extend({
? await getMessageById(messageId, {
Message: Whisper.Message,
})
: null;
: undefined;
try {
if (!messageModel) {
throw new Error('Message not found');
}
await this.model.sendReactionMessage(reaction, {
targetAuthorUuid: messageModel.getSourceUuid(),
targetTimestamp: messageModel.get('sent_at'),
@ -3329,7 +3333,7 @@ Whisper.ConversationView = Whisper.View.extend({
? await getMessageById(messageId, {
Message: Whisper.Message,
})
: null;
: undefined;
if (model && !model.canReply()) {
return;

126
yarn.lock
View File

@ -1348,13 +1348,6 @@
resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
"@journeyapps/sqlcipher@https://github.com/EvanHahn-signal/node-sqlcipher.git#16916949f0c010f6e6d3d5869b10a0ab813eae75":
version "5.0.0"
resolved "https://github.com/EvanHahn-signal/node-sqlcipher.git#16916949f0c010f6e6d3d5869b10a0ab813eae75"
dependencies:
node-addon-api "^3.0.0"
node-pre-gyp "^0.15.0"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@ -2143,6 +2136,13 @@
"@types/jquery" "*"
"@types/underscore" "*"
"@types/better-sqlite3@5.4.1":
version "5.4.1"
resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-5.4.1.tgz#d45600bc19f8f41397263d037ca9b0d05df85e58"
integrity sha512-8hje3Rhsg/9veTkALfCwiWn7VMrP1QDwHhBSgerttYPABEvrHsMQnU9dlqoM6QX3x4uw3Y06dDVz8uDQo1J4Ng==
dependencies:
"@types/integer" "*"
"@types/blueimp-load-image@5.14.1":
version "5.14.1"
resolved "https://registry.yarnpkg.com/@types/blueimp-load-image/-/blueimp-load-image-5.14.1.tgz#3963813699b574e757a140ed75a51050177ac780"
@ -2323,6 +2323,11 @@
dependencies:
"@types/node" "*"
"@types/integer@*":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/integer/-/integer-1.0.1.tgz#025d87e30d97f539fcc6087372af7d3672ffbbe6"
integrity sha512-DmZDpSVnsuBrOhtHwE1oKmUJ3qVjHhhNQ7WnZy9/RhH3A24Ar+9o4SoaCWcTzQhalpRDIAMsfdoZLWNJtdBR7A==
"@types/jquery@*", "@types/jquery@3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.0.tgz#ccb7dfd317d02d4227dd3803c75297d0c10dad68"
@ -4198,6 +4203,13 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
"better-sqlite3@https://github.com/indutny/better-sqlite3#a78376d86b5856c14ab4e2f3995f41e1f80df846":
version "7.1.4"
resolved "https://github.com/indutny/better-sqlite3#a78376d86b5856c14ab4e2f3995f41e1f80df846"
dependencies:
bindings "^1.5.0"
tar "^6.1.0"
big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
@ -4911,6 +4923,11 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
chrome-trace-event@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@ -5874,7 +5891,7 @@ debug@0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
debug@2, debug@2.6.9, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
@ -7984,6 +8001,13 @@ fs-minipass@^1.2.5:
dependencies:
minipass "^2.2.1"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs-write-stream-atomic@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@ -11157,13 +11181,12 @@ minipass@^2.3.4, minipass@^2.3.5:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minipass@^2.8.6:
version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
minipass@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
dependencies:
safe-buffer "^5.1.2"
yallist "^3.0.0"
yallist "^4.0.0"
minizlib@^1.1.0:
version "1.1.0"
@ -11185,6 +11208,14 @@ minizlib@^1.2.1:
dependencies:
minipass "^2.2.1"
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mississippi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
@ -11241,13 +11272,18 @@ mkdirp@0.5.2:
dependencies:
minimist "^1.2.5"
mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
mkdirp@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mkpath@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-0.1.0.tgz#7554a6f8d871834cc97b5462b122c4c124d6de91"
@ -11434,15 +11470,7 @@ nconf@^0.10.0:
secure-keys "^1.0.0"
yargs "^3.19.0"
needle@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
dependencies:
debug "^2.1.2"
iconv-lite "^0.4.4"
sax "^1.2.4"
needle@^2.2.4, needle@^2.3.3, needle@^2.4.0:
needle@^2.2.1, needle@^2.2.4, needle@^2.3.3, needle@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.1.tgz#14af48732463d7475696f937626b1b993247a56a"
integrity sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==
@ -11451,15 +11479,6 @@ needle@^2.2.4, needle@^2.3.3, needle@^2.4.0:
iconv-lite "^0.4.4"
sax "^1.2.4"
needle@^2.5.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe"
integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==
dependencies:
debug "^3.2.6"
iconv-lite "^0.4.4"
sax "^1.2.4"
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
@ -11645,22 +11664,6 @@ node-pre-gyp@^0.12.0:
semver "^5.3.0"
tar "^4"
node-pre-gyp@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz#c2fc383276b74c7ffa842925241553e8b40f1087"
integrity sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.3"
needle "^2.5.0"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4.4.2"
node-releases@^1.1.25:
version "1.1.27"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.27.tgz#b19ec8add2afe9a826a99dceccc516104c1edaf4"
@ -16166,19 +16169,6 @@ tar@^4:
safe-buffer "^5.1.2"
yallist "^3.0.2"
tar@^4.4.2:
version "4.4.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
dependencies:
chownr "^1.1.1"
fs-minipass "^1.2.5"
minipass "^2.8.6"
minizlib "^1.2.1"
mkdirp "^0.5.0"
safe-buffer "^5.1.2"
yallist "^3.0.3"
tar@^4.4.8:
version "4.4.10"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1"
@ -16192,6 +16182,18 @@ tar@^4.4.8:
safe-buffer "^5.1.2"
yallist "^3.0.3"
tar@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
telejson@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/telejson/-/telejson-2.2.2.tgz#d61d721d21849a6e4070d547aab302a9bd22c720"