Types, better-sqlite3, and worker_threads for our sqlite
This commit is contained in:
parent
fc3004a183
commit
37c8c1727f
24 changed files with 2823 additions and 3121 deletions
|
@ -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
|
||||
|
|
|
@ -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
36
main.js
|
@ -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,
|
||||
|
|
27
package.json
27
package.json
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
74
ts/model-types.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.`
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
4636
ts/sql/Server.ts
4636
ts/sql/Server.ts
File diff suppressed because it is too large
Load diff
113
ts/sql/main.ts
Normal file
113
ts/sql/main.ts
Normal 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
56
ts/sql/mainWorker.ts
Normal 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
184
ts/sqlcipher.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -44,6 +44,8 @@ const basePath = join(__dirname, '../../..');
|
|||
const searchPattern = normalizePath(join(basePath, '**/*.{js,ts,tsx}'));
|
||||
|
||||
const excludedFilesRegexps = [
|
||||
'^release/',
|
||||
|
||||
// Non-distributed files
|
||||
'\\.d\\.ts$',
|
||||
|
||||
|
|
|
@ -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
126
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue