Improve cold start performance
This commit is contained in:
parent
c73e35b1b6
commit
d82ce07942
39 changed files with 911 additions and 628 deletions
|
@ -25,4 +25,4 @@ window.closeAbout = () => ipcRenderer.send('close-about');
|
||||||
|
|
||||||
window.i18n = i18n.setup(locale, localeMessages);
|
window.i18n = i18n.setup(locale, localeMessages);
|
||||||
|
|
||||||
require('./ts/logging/set_up_renderer_logging');
|
require('./ts/logging/set_up_renderer_logging').initialize();
|
||||||
|
|
|
@ -32,7 +32,7 @@ window.getEnvironment = getEnvironment;
|
||||||
window.Backbone = require('backbone');
|
window.Backbone = require('backbone');
|
||||||
require('./ts/backbone/views/whisper_view');
|
require('./ts/backbone/views/whisper_view');
|
||||||
require('./ts/backbone/views/toast_view');
|
require('./ts/backbone/views/toast_view');
|
||||||
require('./ts/logging/set_up_renderer_logging');
|
require('./ts/logging/set_up_renderer_logging').initialize();
|
||||||
|
|
||||||
window.closeDebugLog = () => ipcRenderer.send('close-debug-log');
|
window.closeDebugLog = () => ipcRenderer.send('close-debug-log');
|
||||||
window.Backbone = require('backbone');
|
window.Backbone = require('backbone');
|
||||||
|
|
|
@ -18,18 +18,25 @@
|
||||||
MessageCollection: Whisper.MessageCollection,
|
MessageCollection: Whisper.MessageCollection,
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(
|
const messageIds = [];
|
||||||
messages.map(async fromDB => {
|
const inMemoryMessages = [];
|
||||||
const message = MessageController.register(fromDB.id, fromDB);
|
const messageCleanup = [];
|
||||||
|
|
||||||
window.log.info('Message expired', {
|
messages.forEach(dbMessage => {
|
||||||
sentAt: message.get('sent_at'),
|
const message = MessageController.register(dbMessage.id, dbMessage);
|
||||||
|
messageIds.push(message.id);
|
||||||
|
inMemoryMessages.push(message);
|
||||||
|
messageCleanup.push(message.cleanup());
|
||||||
});
|
});
|
||||||
|
|
||||||
// We delete after the trigger to allow the conversation time to process
|
// We delete after the trigger to allow the conversation time to process
|
||||||
// the expiration before the message is removed from the database.
|
// the expiration before the message is removed from the database.
|
||||||
await window.Signal.Data.removeMessage(message.id, {
|
await window.Signal.Data.removeMessages(messageIds);
|
||||||
Message: Whisper.Message,
|
await Promise.all(messageCleanup);
|
||||||
|
|
||||||
|
inMemoryMessages.forEach(message => {
|
||||||
|
window.log.info('Message expired', {
|
||||||
|
sentAt: message.get('sent_at'),
|
||||||
});
|
});
|
||||||
|
|
||||||
Whisper.events.trigger(
|
Whisper.events.trigger(
|
||||||
|
@ -42,8 +49,7 @@
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
conversation.trigger('expired', message);
|
conversation.trigger('expired', message);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'destroyExpiredMessages: Error deleting expired messages',
|
'destroyExpiredMessages: Error deleting expired messages',
|
||||||
|
|
|
@ -58,7 +58,9 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextCheck = toAgeOut.get('received_at') + THIRTY_DAYS;
|
const receivedAt =
|
||||||
|
toAgeOut.get('received_at_ms') || toAgeOut.get('received_at');
|
||||||
|
const nextCheck = receivedAt + THIRTY_DAYS;
|
||||||
|
|
||||||
Whisper.TapToViewMessagesListener.nextCheck = nextCheck;
|
Whisper.TapToViewMessagesListener.nextCheck = nextCheck;
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
const messageLookup = Object.create(null);
|
const messageLookup = Object.create(null);
|
||||||
|
const msgIDsBySender = new Map();
|
||||||
|
const msgIDsBySentAt = new Map();
|
||||||
|
|
||||||
const SECOND = 1000;
|
const SECOND = 1000;
|
||||||
const MINUTE = SECOND * 60;
|
const MINUTE = SECOND * 60;
|
||||||
|
@ -31,10 +33,18 @@
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
msgIDsBySentAt.set(message.get('sent_at'), id);
|
||||||
|
msgIDsBySender.set(message.getSenderIdentifier(), id);
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unregister(id) {
|
function unregister(id) {
|
||||||
|
const { message } = messageLookup[id] || {};
|
||||||
|
if (message) {
|
||||||
|
msgIDsBySender.delete(message.getSenderIdentifier());
|
||||||
|
msgIDsBySentAt.delete(message.get('sent_at'));
|
||||||
|
}
|
||||||
delete messageLookup[id];
|
delete messageLookup[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +60,7 @@
|
||||||
now - timestamp > FIVE_MINUTES &&
|
now - timestamp > FIVE_MINUTES &&
|
||||||
(!conversation || !conversation.messageCollection.length)
|
(!conversation || !conversation.messageCollection.length)
|
||||||
) {
|
) {
|
||||||
delete messageLookup[message.id];
|
unregister(message.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,6 +70,22 @@
|
||||||
return existing && existing.message ? existing.message : null;
|
return existing && existing.message ? existing.message : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findBySentAt(sentAt) {
|
||||||
|
const id = msgIDsBySentAt.get(sentAt);
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findBySender(sender) {
|
||||||
|
const id = msgIDsBySender.get(sender);
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
function _get() {
|
function _get() {
|
||||||
return messageLookup;
|
return messageLookup;
|
||||||
}
|
}
|
||||||
|
@ -70,6 +96,8 @@
|
||||||
register,
|
register,
|
||||||
unregister,
|
unregister,
|
||||||
cleanup,
|
cleanup,
|
||||||
|
findBySender,
|
||||||
|
findBySentAt,
|
||||||
getById,
|
getById,
|
||||||
_get,
|
_get,
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,3 +67,16 @@ window.Whisper.events = {
|
||||||
on() {},
|
on() {},
|
||||||
trigger() {},
|
trigger() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
try {
|
||||||
|
window.log.info('Initializing SQL in renderer');
|
||||||
|
await window.sqlInitializer.initialize();
|
||||||
|
window.log.info('SQL initialized in renderer');
|
||||||
|
} catch (err) {
|
||||||
|
window.log.error(
|
||||||
|
'SQL failed to initialize',
|
||||||
|
err && err.stack ? err.stack : err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
53
main.js
53
main.js
|
@ -19,6 +19,8 @@ const electron = require('electron');
|
||||||
const packageJson = require('./package.json');
|
const packageJson = require('./package.json');
|
||||||
const GlobalErrors = require('./app/global_errors');
|
const GlobalErrors = require('./app/global_errors');
|
||||||
const { setup: setupSpellChecker } = require('./app/spell_check');
|
const { setup: setupSpellChecker } = require('./app/spell_check');
|
||||||
|
const { redactAll } = require('./js/modules/privacy');
|
||||||
|
const removeUserConfig = require('./app/user_config').remove;
|
||||||
|
|
||||||
GlobalErrors.addHandler();
|
GlobalErrors.addHandler();
|
||||||
|
|
||||||
|
@ -30,6 +32,7 @@ const getRealPath = pify(fs.realpath);
|
||||||
const {
|
const {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
|
clipboard,
|
||||||
dialog,
|
dialog,
|
||||||
ipcMain: ipc,
|
ipcMain: ipc,
|
||||||
Menu,
|
Menu,
|
||||||
|
@ -1058,12 +1061,37 @@ app.on('ready', async () => {
|
||||||
loadingWindow.loadURL(prepareURL([__dirname, 'loading.html']));
|
loadingWindow.loadURL(prepareURL([__dirname, 'loading.html']));
|
||||||
});
|
});
|
||||||
|
|
||||||
const success = await sqlInitPromise;
|
try {
|
||||||
|
await sqlInitPromise;
|
||||||
if (!success) {
|
} catch (error) {
|
||||||
console.log('sql.initialize was unsuccessful; returning early');
|
console.log('sql.initialize was unsuccessful; returning early');
|
||||||
|
const buttonIndex = dialog.showMessageBoxSync({
|
||||||
|
buttons: [
|
||||||
|
locale.messages.copyErrorAndQuit.message,
|
||||||
|
locale.messages.deleteAndRestart.message,
|
||||||
|
],
|
||||||
|
defaultId: 0,
|
||||||
|
detail: redactAll(error.stack),
|
||||||
|
message: locale.messages.databaseError.message,
|
||||||
|
noLink: true,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (buttonIndex === 0) {
|
||||||
|
clipboard.writeText(
|
||||||
|
`Database startup error:\n\n${redactAll(error.stack)}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await sql.removeDB();
|
||||||
|
removeUserConfig();
|
||||||
|
app.relaunch();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.exit(1);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line more/no-then
|
// eslint-disable-next-line more/no-then
|
||||||
appStartInitialSpellcheckSetting = await getSpellCheckSetting();
|
appStartInitialSpellcheckSetting = await getSpellCheckSetting();
|
||||||
await sqlChannels.initialize();
|
await sqlChannels.initialize();
|
||||||
|
@ -1075,10 +1103,10 @@ app.on('ready', async () => {
|
||||||
await sql.removeIndexedDBFiles();
|
await sql.removeIndexedDBFiles();
|
||||||
await sql.removeItemById(IDB_KEY);
|
await sql.removeItemById(IDB_KEY);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.log(
|
console.log(
|
||||||
'(ready event handler) error deleting IndexedDB:',
|
'(ready event handler) error deleting IndexedDB:',
|
||||||
error && error.stack ? error.stack : error
|
err && err.stack ? err.stack : err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1113,10 +1141,10 @@ app.on('ready', async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await attachments.clearTempPath(userDataPath);
|
await attachments.clearTempPath(userDataPath);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'main/ready: Error deleting temp dir:',
|
'main/ready: Error deleting temp dir:',
|
||||||
error && error.stack ? error.stack : error
|
err && err.stack ? err.stack : err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await attachmentChannel.initialize({
|
await attachmentChannel.initialize({
|
||||||
|
@ -1458,6 +1486,17 @@ ipc.on('locale-data', event => {
|
||||||
event.returnValue = locale.messages;
|
event.returnValue = locale.messages;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Used once to initialize SQL in the renderer process
|
||||||
|
ipc.once('user-config-key', event => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
event.returnValue = userConfig.get('key');
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.on('get-user-data-path', event => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
event.returnValue = app.getPath('userData');
|
||||||
|
});
|
||||||
|
|
||||||
function getDataFromMainWindow(name, callback) {
|
function getDataFromMainWindow(name, callback) {
|
||||||
ipc.once(`get-success-${name}`, (_event, error, value) =>
|
ipc.once(`get-success-${name}`, (_event, error, value) =>
|
||||||
callback(error, value)
|
callback(error, value)
|
||||||
|
|
|
@ -49,7 +49,7 @@ window.subscribeToSystemThemeChange = fn => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
require('./ts/logging/set_up_renderer_logging');
|
require('./ts/logging/set_up_renderer_logging').initialize();
|
||||||
|
|
||||||
window.closePermissionsPopup = () =>
|
window.closePermissionsPopup = () =>
|
||||||
ipcRenderer.send('close-permissions-popup');
|
ipcRenderer.send('close-permissions-popup');
|
||||||
|
|
|
@ -22,6 +22,8 @@ try {
|
||||||
const { app } = remote;
|
const { app } = remote;
|
||||||
const { nativeTheme } = remote.require('electron');
|
const { nativeTheme } = remote.require('electron');
|
||||||
|
|
||||||
|
window.sqlInitializer = require('./ts/sql/initialize');
|
||||||
|
|
||||||
window.PROTO_ROOT = 'protos';
|
window.PROTO_ROOT = 'protos';
|
||||||
const config = require('url').parse(window.location.toString(), true).query;
|
const config = require('url').parse(window.location.toString(), true).query;
|
||||||
|
|
||||||
|
@ -400,7 +402,7 @@ try {
|
||||||
|
|
||||||
// We pull these dependencies in now, from here, because they have Node.js dependencies
|
// We pull these dependencies in now, from here, because they have Node.js dependencies
|
||||||
|
|
||||||
require('./ts/logging/set_up_renderer_logging');
|
require('./ts/logging/set_up_renderer_logging').initialize();
|
||||||
|
|
||||||
if (config.proxyUrl) {
|
if (config.proxyUrl) {
|
||||||
window.log.info('Using provided proxy url');
|
window.log.info('Using provided proxy url');
|
||||||
|
|
|
@ -129,4 +129,4 @@ function makeSetter(name) {
|
||||||
window.Backbone = require('backbone');
|
window.Backbone = require('backbone');
|
||||||
require('./ts/backbone/views/whisper_view');
|
require('./ts/backbone/views/whisper_view');
|
||||||
require('./ts/backbone/views/toast_view');
|
require('./ts/backbone/views/toast_view');
|
||||||
require('./ts/logging/set_up_renderer_logging');
|
require('./ts/logging/set_up_renderer_logging').initialize();
|
||||||
|
|
|
@ -76,6 +76,16 @@ function deleteIndexedDB() {
|
||||||
/* Delete the database before running any tests */
|
/* Delete the database before running any tests */
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await deleteIndexedDB();
|
await deleteIndexedDB();
|
||||||
|
try {
|
||||||
|
window.log.info('Initializing SQL in renderer');
|
||||||
|
await window.sqlInitializer.initialize();
|
||||||
|
window.log.info('SQL initialized in renderer');
|
||||||
|
} catch (err) {
|
||||||
|
window.log.error(
|
||||||
|
'SQL failed to initialize',
|
||||||
|
err && err.stack ? err.stack : err
|
||||||
|
);
|
||||||
|
}
|
||||||
await window.Signal.Data.removeAll();
|
await window.Signal.Data.removeAll();
|
||||||
await window.storage.fetch();
|
await window.storage.fetch();
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,8 +3,19 @@
|
||||||
|
|
||||||
import { DataMessageClass } from './textsecure.d';
|
import { DataMessageClass } from './textsecure.d';
|
||||||
import { WhatIsThis } from './window.d';
|
import { WhatIsThis } from './window.d';
|
||||||
|
import { assert } from './util/assert';
|
||||||
|
|
||||||
export async function startApp(): Promise<void> {
|
export async function startApp(): Promise<void> {
|
||||||
|
try {
|
||||||
|
window.log.info('Initializing SQL in renderer');
|
||||||
|
await window.sqlInitializer.initialize();
|
||||||
|
window.log.info('SQL initialized in renderer');
|
||||||
|
} catch (err) {
|
||||||
|
window.log.error(
|
||||||
|
'SQL failed to initialize',
|
||||||
|
err && err.stack ? err.stack : err
|
||||||
|
);
|
||||||
|
}
|
||||||
const eventHandlerQueue = new window.PQueue({
|
const eventHandlerQueue = new window.PQueue({
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
timeout: 1000 * 60 * 2,
|
timeout: 1000 * 60 * 2,
|
||||||
|
@ -1615,6 +1626,9 @@ export async function startApp(): Promise<void> {
|
||||||
let connectCount = 0;
|
let connectCount = 0;
|
||||||
let connecting = false;
|
let connecting = false;
|
||||||
async function connect(firstRun?: boolean) {
|
async function connect(firstRun?: boolean) {
|
||||||
|
window.receivedAtCounter =
|
||||||
|
window.storage.get('lastReceivedAtCounter') || Date.now();
|
||||||
|
|
||||||
if (connecting) {
|
if (connecting) {
|
||||||
window.log.warn('connect already running', { connectCount });
|
window.log.warn('connect already running', { connectCount });
|
||||||
return;
|
return;
|
||||||
|
@ -2038,7 +2052,7 @@ export async function startApp(): Promise<void> {
|
||||||
logger: window.log,
|
logger: window.log,
|
||||||
});
|
});
|
||||||
|
|
||||||
let interval: NodeJS.Timer | null = setInterval(() => {
|
let interval: NodeJS.Timer | null = setInterval(async () => {
|
||||||
const view = window.owsDesktopApp.appView;
|
const view = window.owsDesktopApp.appView;
|
||||||
if (view) {
|
if (view) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
@ -2046,6 +2060,24 @@ export async function startApp(): Promise<void> {
|
||||||
interval = null;
|
interval = null;
|
||||||
view.onEmpty();
|
view.onEmpty();
|
||||||
window.logAppLoadedEvent();
|
window.logAppLoadedEvent();
|
||||||
|
const attachmentDownloadQueue = window.attachmentDownloadQueue || [];
|
||||||
|
const THREE_DAYS_AGO = Date.now() - 3600 * 72 * 1000;
|
||||||
|
const MAX_ATTACHMENT_MSGS_TO_DOWNLOAD = 250;
|
||||||
|
const attachmentsToDownload = attachmentDownloadQueue.filter(
|
||||||
|
(message, index) =>
|
||||||
|
index <= MAX_ATTACHMENT_MSGS_TO_DOWNLOAD ||
|
||||||
|
message.getReceivedAt() < THREE_DAYS_AGO
|
||||||
|
);
|
||||||
|
window.log.info(
|
||||||
|
'Downloading recent attachments of total attachments',
|
||||||
|
attachmentsToDownload.length,
|
||||||
|
attachmentDownloadQueue.length
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
attachmentsToDownload.map(message =>
|
||||||
|
message.queueAttachmentDownloads()
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
@ -2646,14 +2678,15 @@ export async function startApp(): Promise<void> {
|
||||||
sent_at: data.timestamp,
|
sent_at: data.timestamp,
|
||||||
serverTimestamp: data.serverTimestamp,
|
serverTimestamp: data.serverTimestamp,
|
||||||
sent_to: sentTo,
|
sent_to: sentTo,
|
||||||
received_at: now,
|
received_at: data.receivedAtCounter,
|
||||||
|
received_at_ms: data.receivedAtDate,
|
||||||
conversationId: descriptor.id,
|
conversationId: descriptor.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
sent: true,
|
sent: true,
|
||||||
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
||||||
expirationStartTimestamp: Math.min(
|
expirationStartTimestamp: Math.min(
|
||||||
data.expirationStartTimestamp || data.timestamp || Date.now(),
|
data.expirationStartTimestamp || data.timestamp || now,
|
||||||
Date.now()
|
now
|
||||||
),
|
),
|
||||||
} as WhatIsThis);
|
} as WhatIsThis);
|
||||||
}
|
}
|
||||||
|
@ -2856,13 +2889,18 @@ export async function startApp(): Promise<void> {
|
||||||
data: WhatIsThis,
|
data: WhatIsThis,
|
||||||
descriptor: MessageDescriptor
|
descriptor: MessageDescriptor
|
||||||
) {
|
) {
|
||||||
|
assert(
|
||||||
|
Boolean(data.receivedAtCounter),
|
||||||
|
`Did not receive receivedAtCounter for message: ${data.timestamp}`
|
||||||
|
);
|
||||||
return new window.Whisper.Message({
|
return new window.Whisper.Message({
|
||||||
source: data.source,
|
source: data.source,
|
||||||
sourceUuid: data.sourceUuid,
|
sourceUuid: data.sourceUuid,
|
||||||
sourceDevice: data.sourceDevice,
|
sourceDevice: data.sourceDevice,
|
||||||
sent_at: data.timestamp,
|
sent_at: data.timestamp,
|
||||||
serverTimestamp: data.serverTimestamp,
|
serverTimestamp: data.serverTimestamp,
|
||||||
received_at: Date.now(),
|
received_at: data.receivedAtCounter,
|
||||||
|
received_at_ms: data.receivedAtDate,
|
||||||
conversationId: descriptor.id,
|
conversationId: descriptor.id,
|
||||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||||
type: 'incoming',
|
type: 'incoming',
|
||||||
|
|
|
@ -40,7 +40,8 @@ story.add('Image and Video', () => {
|
||||||
message: {
|
message: {
|
||||||
attachments: [],
|
attachments: [],
|
||||||
id: 'image-msg',
|
id: 'image-msg',
|
||||||
received_at: Date.now(),
|
received_at: 1,
|
||||||
|
received_at_ms: Date.now(),
|
||||||
},
|
},
|
||||||
objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||||
},
|
},
|
||||||
|
@ -55,7 +56,8 @@ story.add('Image and Video', () => {
|
||||||
message: {
|
message: {
|
||||||
attachments: [],
|
attachments: [],
|
||||||
id: 'video-msg',
|
id: 'video-msg',
|
||||||
received_at: Date.now(),
|
received_at: 2,
|
||||||
|
received_at_ms: Date.now(),
|
||||||
},
|
},
|
||||||
objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
||||||
},
|
},
|
||||||
|
@ -79,7 +81,8 @@ story.add('Missing Media', () => {
|
||||||
message: {
|
message: {
|
||||||
attachments: [],
|
attachments: [],
|
||||||
id: 'image-msg',
|
id: 'image-msg',
|
||||||
received_at: Date.now(),
|
received_at: 3,
|
||||||
|
received_at_ms: Date.now(),
|
||||||
},
|
},
|
||||||
objectURL: undefined,
|
objectURL: undefined,
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,7 +52,8 @@ const createRandomFile = (
|
||||||
contentType,
|
contentType,
|
||||||
message: {
|
message: {
|
||||||
id: random(now).toString(),
|
id: random(now).toString(),
|
||||||
received_at: random(startTime, startTime + timeWindow),
|
received_at: Math.floor(Math.random() * 10),
|
||||||
|
received_at_ms: random(startTime, startTime + timeWindow),
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { MediaGridItem } from './MediaGridItem';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../LightboxGallery';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError';
|
import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
|
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -58,7 +59,7 @@ export class AttachmentSection extends React.Component<Props> {
|
||||||
fileSize={attachment.size}
|
fileSize={attachment.size}
|
||||||
shouldShowSeparator={shouldShowSeparator}
|
shouldShowSeparator={shouldShowSeparator}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
timestamp={message.received_at}
|
timestamp={getMessageTimestamp(message)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { groupMediaItemsByDate } from './groupMediaItemsByDate';
|
||||||
import { ItemClickEvent } from './types/ItemClickEvent';
|
import { ItemClickEvent } from './types/ItemClickEvent';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError';
|
import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
|
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
||||||
|
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../LightboxGallery';
|
||||||
|
|
||||||
|
@ -145,7 +146,7 @@ export class MediaGallery extends React.Component<Props, State> {
|
||||||
const sections = groupMediaItemsByDate(now, mediaItems).map(section => {
|
const sections = groupMediaItemsByDate(now, mediaItems).map(section => {
|
||||||
const first = section.mediaItems[0];
|
const first = section.mediaItems[0];
|
||||||
const { message } = first;
|
const { message } = first;
|
||||||
const date = moment(message.received_at);
|
const date = moment(getMessageTimestamp(message));
|
||||||
const header =
|
const header =
|
||||||
section.type === 'yearMonth'
|
section.type === 'yearMonth'
|
||||||
? date.format(MONTH_FORMAT)
|
? date.format(MONTH_FORMAT)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import moment from 'moment';
|
||||||
import { compact, groupBy, sortBy } from 'lodash';
|
import { compact, groupBy, sortBy } from 'lodash';
|
||||||
|
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../LightboxGallery';
|
||||||
|
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
||||||
|
|
||||||
// import { missingCaseError } from '../../../util/missingCaseError';
|
// import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
|
|
||||||
|
@ -120,7 +121,7 @@ const withSection = (referenceDateTime: moment.Moment) => (
|
||||||
const thisMonth = moment(referenceDateTime).startOf('month');
|
const thisMonth = moment(referenceDateTime).startOf('month');
|
||||||
|
|
||||||
const { message } = mediaItem;
|
const { message } = mediaItem;
|
||||||
const mediaItemReceivedDate = moment.utc(message.received_at);
|
const mediaItemReceivedDate = moment.utc(getMessageTimestamp(message));
|
||||||
if (mediaItemReceivedDate.isAfter(today)) {
|
if (mediaItemReceivedDate.isAfter(today)) {
|
||||||
return {
|
return {
|
||||||
order: 0,
|
order: 0,
|
||||||
|
|
|
@ -9,4 +9,6 @@ export type Message = {
|
||||||
// Assuming this is for the API
|
// Assuming this is for the API
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
received_at: number;
|
received_at: number;
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
received_at_ms: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2452,7 +2452,8 @@ async function updateGroup({
|
||||||
return {
|
return {
|
||||||
...changeMessage,
|
...changeMessage,
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
received_at: finalReceivedAt,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: finalReceivedAt,
|
||||||
sent_at: syntheticSentAt,
|
sent_at: syntheticSentAt,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,9 +61,9 @@ export async function initialize(): Promise<bunyan> {
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
const logFile = path.join(logPath, 'log.log');
|
const logFile = path.join(logPath, 'main.log');
|
||||||
const loggerOptions: bunyan.LoggerOptions = {
|
const loggerOptions: bunyan.LoggerOptions = {
|
||||||
name: 'log',
|
name: 'main',
|
||||||
streams: [
|
streams: [
|
||||||
{
|
{
|
||||||
type: 'rotating-file',
|
type: 'rotating-file',
|
||||||
|
@ -83,31 +83,6 @@ export async function initialize(): Promise<bunyan> {
|
||||||
|
|
||||||
const logger = bunyan.createLogger(loggerOptions);
|
const logger = bunyan.createLogger(loggerOptions);
|
||||||
|
|
||||||
ipc.on('batch-log', (_first, batch: unknown) => {
|
|
||||||
if (!Array.isArray(batch)) {
|
|
||||||
logger.error(
|
|
||||||
'batch-log IPC event was called with a non-array; dropping logs'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
batch.forEach(item => {
|
|
||||||
if (isLogEntry(item)) {
|
|
||||||
const levelString = getLogLevelString(item.level);
|
|
||||||
logger[levelString](
|
|
||||||
{
|
|
||||||
time: item.time,
|
|
||||||
},
|
|
||||||
item.msg
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.error(
|
|
||||||
'batch-log IPC event was called with an invalid log entry; dropping entry'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.on('fetch-log', event => {
|
ipc.on('fetch-log', event => {
|
||||||
fetch(logPath).then(
|
fetch(logPath).then(
|
||||||
data => {
|
data => {
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
|
|
||||||
import { ipcRenderer as ipc } from 'electron';
|
import { ipcRenderer as ipc } from 'electron';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { levelFromName } from 'bunyan';
|
import * as path from 'path';
|
||||||
|
import * as bunyan from 'bunyan';
|
||||||
|
|
||||||
import { uploadDebugLogs } from './debuglogs';
|
import { uploadDebugLogs } from './debuglogs';
|
||||||
import { redactAll } from '../../js/modules/privacy';
|
import { redactAll } from '../../js/modules/privacy';
|
||||||
import { createBatcher } from '../util/batcher';
|
|
||||||
import {
|
import {
|
||||||
LogEntryType,
|
LogEntryType,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
@ -23,7 +23,7 @@ import * as log from './log';
|
||||||
import { reallyJsonStringify } from '../util/reallyJsonStringify';
|
import { reallyJsonStringify } from '../util/reallyJsonStringify';
|
||||||
|
|
||||||
// To make it easier to visually scan logs, we make all levels the same length
|
// To make it easier to visually scan logs, we make all levels the same length
|
||||||
const levelMaxLength: number = Object.keys(levelFromName).reduce(
|
const levelMaxLength: number = Object.keys(bunyan.levelFromName).reduce(
|
||||||
(maxLength, level) => Math.max(maxLength, level.length),
|
(maxLength, level) => Math.max(maxLength, level.length),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
@ -96,6 +96,30 @@ function fetch(): Promise<string> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let globalLogger: undefined | bunyan;
|
||||||
|
|
||||||
|
export function initialize(): void {
|
||||||
|
if (globalLogger) {
|
||||||
|
throw new Error('Already called initialize!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = ipc.sendSync('get-user-data-path');
|
||||||
|
const logFile = path.join(basePath, 'logs', 'app.log');
|
||||||
|
const loggerOptions: bunyan.LoggerOptions = {
|
||||||
|
name: 'app',
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
type: 'rotating-file',
|
||||||
|
path: logFile,
|
||||||
|
period: '1d',
|
||||||
|
count: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
globalLogger = bunyan.createLogger(loggerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
const publish = uploadDebugLogs;
|
const publish = uploadDebugLogs;
|
||||||
|
|
||||||
// A modern logging interface for the browser
|
// A modern logging interface for the browser
|
||||||
|
@ -103,14 +127,6 @@ const publish = uploadDebugLogs;
|
||||||
const env = window.getEnvironment();
|
const env = window.getEnvironment();
|
||||||
const IS_PRODUCTION = env === 'production';
|
const IS_PRODUCTION = env === 'production';
|
||||||
|
|
||||||
const ipcBatcher = createBatcher({
|
|
||||||
wait: 500,
|
|
||||||
maxSize: 500,
|
|
||||||
processBatch: (items: Array<LogEntryType>) => {
|
|
||||||
ipc.send('batch-log', items);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api
|
// The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api
|
||||||
function logAtLevel(level: LogLevel, ...args: ReadonlyArray<unknown>): void {
|
function logAtLevel(level: LogLevel, ...args: ReadonlyArray<unknown>): void {
|
||||||
if (!IS_PRODUCTION) {
|
if (!IS_PRODUCTION) {
|
||||||
|
@ -120,11 +136,16 @@ function logAtLevel(level: LogLevel, ...args: ReadonlyArray<unknown>): void {
|
||||||
console._log(prefix, now(), ...args);
|
console._log(prefix, now(), ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcBatcher.add({
|
const levelString = getLogLevelString(level);
|
||||||
level,
|
const msg = cleanArgs(args);
|
||||||
msg: cleanArgs(args),
|
const time = new Date().toISOString();
|
||||||
time: new Date().toISOString(),
|
|
||||||
});
|
if (!globalLogger) {
|
||||||
|
throw new Error('Logger has not been initialized yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalLogger[levelString]({ time }, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.setLogAtLevel(logAtLevel);
|
log.setLogAtLevel(logAtLevel);
|
||||||
|
|
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
|
@ -134,6 +134,7 @@ export type MessageAttributesType = {
|
||||||
groupV2Change?: GroupV2ChangeType;
|
groupV2Change?: GroupV2ChangeType;
|
||||||
// Required. Used to sort messages in the database for the conversation timeline.
|
// 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
|
// 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
|
// background, when we were still in IndexedDB, before attachments had gone to disk
|
||||||
// We set this so that the idle message upgrade process doesn't pick this message up
|
// We set this so that the idle message upgrade process doesn't pick this message up
|
||||||
|
|
|
@ -2282,7 +2282,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'chat-session-refreshed',
|
type: 'chat-session-refreshed',
|
||||||
sent_at: receivedAt,
|
sent_at: receivedAt,
|
||||||
received_at: receivedAt,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: receivedAt,
|
||||||
unread: 1,
|
unread: 1,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
// this type does not fully implement the interface it is expected to
|
// this type does not fully implement the interface it is expected to
|
||||||
|
@ -2315,7 +2316,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'keychange',
|
type: 'keychange',
|
||||||
sent_at: this.get('timestamp'),
|
sent_at: this.get('timestamp'),
|
||||||
received_at: timestamp,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: timestamp,
|
||||||
key_changed: keyChangedId,
|
key_changed: keyChangedId,
|
||||||
unread: 1,
|
unread: 1,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
|
@ -2373,7 +2375,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'verified-change',
|
type: 'verified-change',
|
||||||
sent_at: lastMessage,
|
sent_at: lastMessage,
|
||||||
received_at: timestamp,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: timestamp,
|
||||||
verifiedChanged: verifiedChangeId,
|
verifiedChanged: verifiedChangeId,
|
||||||
verified,
|
verified,
|
||||||
local: options.local,
|
local: options.local,
|
||||||
|
@ -2435,7 +2438,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'call-history',
|
type: 'call-history',
|
||||||
sent_at: timestamp,
|
sent_at: timestamp,
|
||||||
received_at: timestamp,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: timestamp,
|
||||||
unread,
|
unread,
|
||||||
callHistoryDetails: detailsToSave,
|
callHistoryDetails: detailsToSave,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
|
@ -2481,11 +2485,13 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
profileChange: unknown,
|
profileChange: unknown,
|
||||||
conversationId?: string
|
conversationId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
const message = ({
|
const message = ({
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'profile-change',
|
type: 'profile-change',
|
||||||
sent_at: Date.now(),
|
sent_at: now,
|
||||||
received_at: Date.now(),
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: now,
|
||||||
unread: true,
|
unread: true,
|
||||||
changedId: conversationId || this.id,
|
changedId: conversationId || this.id,
|
||||||
profileChange,
|
profileChange,
|
||||||
|
@ -2984,7 +2990,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId: this.get('id'),
|
conversationId: this.get('id'),
|
||||||
sent_at: timestamp,
|
sent_at: timestamp,
|
||||||
received_at: timestamp,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: timestamp,
|
||||||
recipients,
|
recipients,
|
||||||
deletedForEveryoneTimestamp: targetTimestamp,
|
deletedForEveryoneTimestamp: targetTimestamp,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
|
@ -3093,7 +3100,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
conversationId: this.get('id'),
|
conversationId: this.get('id'),
|
||||||
sent_at: timestamp,
|
sent_at: timestamp,
|
||||||
received_at: timestamp,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: timestamp,
|
||||||
recipients,
|
recipients,
|
||||||
reaction: outgoingReaction,
|
reaction: outgoingReaction,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
|
@ -3244,7 +3252,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
preview,
|
preview,
|
||||||
attachments,
|
attachments,
|
||||||
sent_at: now,
|
sent_at: now,
|
||||||
received_at: now,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: now,
|
||||||
expireTimer,
|
expireTimer,
|
||||||
recipients,
|
recipients,
|
||||||
sticker,
|
sticker,
|
||||||
|
@ -3866,7 +3875,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
// No type; 'incoming' messages are specially treated by conversation.markRead()
|
// No type; 'incoming' messages are specially treated by conversation.markRead()
|
||||||
sent_at: timestamp,
|
sent_at: timestamp,
|
||||||
received_at: timestamp,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: timestamp,
|
||||||
flags:
|
flags:
|
||||||
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
expirationTimerUpdate: {
|
expirationTimerUpdate: {
|
||||||
|
@ -3970,7 +3980,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
// No type; 'incoming' messages are specially treated by conversation.markRead()
|
// No type; 'incoming' messages are specially treated by conversation.markRead()
|
||||||
sent_at: timestamp,
|
sent_at: timestamp,
|
||||||
received_at: timestamp,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: timestamp,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
} as unknown) as MessageAttributesType);
|
} as unknown) as MessageAttributesType);
|
||||||
|
|
||||||
|
@ -4003,7 +4014,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
sent_at: now,
|
sent_at: now,
|
||||||
received_at: now,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: now,
|
||||||
destination: this.get('e164'),
|
destination: this.get('e164'),
|
||||||
destinationUuid: this.get('uuid'),
|
destinationUuid: this.get('uuid'),
|
||||||
recipients: this.getRecipients(),
|
recipients: this.getRecipients(),
|
||||||
|
@ -4059,7 +4071,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
sent_at: now,
|
sent_at: now,
|
||||||
received_at: now,
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: now,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
} as unknown) as MessageAttributesType);
|
} as unknown) as MessageAttributesType);
|
||||||
|
|
||||||
|
|
|
@ -198,6 +198,29 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSenderIdentifier(): string {
|
||||||
|
const sentAt = this.get('sent_at');
|
||||||
|
const source = this.get('source');
|
||||||
|
const sourceUuid = this.get('sourceUuid');
|
||||||
|
const sourceDevice = this.get('sourceDevice');
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const sourceId = window.ConversationController.ensureContactIds({
|
||||||
|
e164: source,
|
||||||
|
uuid: sourceUuid,
|
||||||
|
})!;
|
||||||
|
|
||||||
|
return `${sourceId}.${sourceDevice}-${sentAt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getReceivedAt(): number {
|
||||||
|
// We would like to get the received_at_ms ideally since received_at is
|
||||||
|
// now an incrementing counter for messages and not the actual time that
|
||||||
|
// the message was received. If this field doesn't exist on the message
|
||||||
|
// then we can trust received_at.
|
||||||
|
return Number(this.get('received_at_ms') || this.get('received_at'));
|
||||||
|
}
|
||||||
|
|
||||||
isNormalBubble(): boolean {
|
isNormalBubble(): boolean {
|
||||||
return (
|
return (
|
||||||
!this.isCallHistory() &&
|
!this.isCallHistory() &&
|
||||||
|
@ -381,7 +404,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sentAt: this.get('sent_at'),
|
sentAt: this.get('sent_at'),
|
||||||
receivedAt: this.get('received_at'),
|
receivedAt: this.getReceivedAt(),
|
||||||
message: {
|
message: {
|
||||||
...this.getPropsForMessage(),
|
...this.getPropsForMessage(),
|
||||||
disableMenu: true,
|
disableMenu: true,
|
||||||
|
@ -1904,9 +1927,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
window.Whisper.Notifications.removeBy({ messageId: this.id });
|
window.Whisper.Notifications.removeBy({ messageId: this.id });
|
||||||
|
|
||||||
if (!skipSave) {
|
if (!skipSave) {
|
||||||
await window.Signal.Data.saveMessage(this.attributes, {
|
window.Signal.Util.updateMessageBatcher.add(this.attributes);
|
||||||
Message: window.Whisper.Message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1955,9 +1976,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
const id = this.get('id');
|
const id = this.get('id');
|
||||||
if (id && !skipSave) {
|
if (id && !skipSave) {
|
||||||
await window.Signal.Data.saveMessage(this.attributes, {
|
window.Signal.Util.updateMessageBatcher.add(this.attributes);
|
||||||
Message: window.Whisper.Message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2822,6 +2841,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const inMemoryMessage = window.MessageController.findBySentAt(id);
|
||||||
|
|
||||||
|
let queryMessage;
|
||||||
|
|
||||||
|
if (inMemoryMessage) {
|
||||||
|
queryMessage = inMemoryMessage;
|
||||||
|
} else {
|
||||||
|
window.log.info('copyFromQuotedMessage: db lookup needed', id);
|
||||||
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
|
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
|
||||||
MessageCollection: window.Whisper.MessageCollection,
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
});
|
});
|
||||||
|
@ -2835,7 +2862,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
quote.referencedMessageNotFound = true;
|
quote.referencedMessageNotFound = true;
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
if (found.isTapToView()) {
|
|
||||||
|
queryMessage = window.MessageController.register(found.id, found);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryMessage.isTapToView()) {
|
||||||
quote.text = null;
|
quote.text = null;
|
||||||
quote.attachments = [
|
quote.attachments = [
|
||||||
{
|
{
|
||||||
|
@ -2846,7 +2877,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryMessage = window.MessageController.register(found.id, found);
|
|
||||||
quote.text = queryMessage.get('body');
|
quote.text = queryMessage.get('body');
|
||||||
if (firstAttachment) {
|
if (firstAttachment) {
|
||||||
firstAttachment.thumbnail = null;
|
firstAttachment.thumbnail = null;
|
||||||
|
@ -2946,9 +2976,20 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// First, check for duplicates. If we find one, stop processing here.
|
// First, check for duplicates. If we find one, stop processing here.
|
||||||
const existingMessage = await getMessageBySender(this.attributes, {
|
const inMemoryMessage = window.MessageController.findBySender(
|
||||||
|
this.getSenderIdentifier()
|
||||||
|
);
|
||||||
|
if (!inMemoryMessage) {
|
||||||
|
window.log.info(
|
||||||
|
'handleDataMessage: duplicate check db lookup needed',
|
||||||
|
this.getSenderIdentifier()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const existingMessage =
|
||||||
|
inMemoryMessage ||
|
||||||
|
(await getMessageBySender(this.attributes, {
|
||||||
Message: window.Whisper.Message,
|
Message: window.Whisper.Message,
|
||||||
});
|
}));
|
||||||
const isUpdate = Boolean(data && data.isRecipientUpdate);
|
const isUpdate = Boolean(data && data.isRecipientUpdate);
|
||||||
|
|
||||||
if (existingMessage && type === 'incoming') {
|
if (existingMessage && type === 'incoming') {
|
||||||
|
@ -3422,7 +3463,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
dataMessage.expireTimer,
|
dataMessage.expireTimer,
|
||||||
source,
|
source,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
message.get('received_at')!,
|
message.getReceivedAt()!,
|
||||||
{
|
{
|
||||||
fromGroupUpdate: message.isGroupUpdate(),
|
fromGroupUpdate: message.isGroupUpdate(),
|
||||||
}
|
}
|
||||||
|
@ -3437,7 +3478,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
undefined,
|
undefined,
|
||||||
source,
|
source,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
message.get('received_at')!
|
message.getReceivedAt()!
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3573,7 +3614,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
(this.getConversation()!.getAccepted() || message.isOutgoing()) &&
|
(this.getConversation()!.getAccepted() || message.isOutgoing()) &&
|
||||||
!shouldHoldOffDownload
|
!shouldHoldOffDownload
|
||||||
) {
|
) {
|
||||||
await message.queueAttachmentDownloads();
|
window.attachmentDownloadQueue = window.attachmentDownloadQueue || [];
|
||||||
|
window.attachmentDownloadQueue.unshift(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does this message have any pending, previously-received associated reactions?
|
// Does this message have any pending, previously-received associated reactions?
|
||||||
|
@ -3591,24 +3633,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await window.Signal.Data.saveMessage(message.attributes, {
|
window.log.info(
|
||||||
Message: window.Whisper.Message,
|
'handleDataMessage: Batching save for',
|
||||||
forceSave: true,
|
message.get('sent_at')
|
||||||
});
|
);
|
||||||
|
this.saveAndNotify(conversation, confirm);
|
||||||
conversation.trigger('newmessage', message);
|
|
||||||
|
|
||||||
if (message.get('unread')) {
|
|
||||||
await conversation.notify(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment the sent message count if this is an outgoing message
|
|
||||||
if (type === 'outgoing') {
|
|
||||||
conversation.incrementSentMessageCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.Whisper.events.trigger('incrementProgress');
|
|
||||||
confirm();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorForLog = error && error.stack ? error.stack : error;
|
const errorForLog = error && error.stack ? error.stack : error;
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
@ -3622,6 +3651,29 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveAndNotify(
|
||||||
|
conversation: ConversationModel,
|
||||||
|
confirm: () => void
|
||||||
|
): Promise<void> {
|
||||||
|
await window.Signal.Util.saveNewMessageBatcher.add(this.attributes);
|
||||||
|
|
||||||
|
window.log.info('Message saved', this.get('sent_at'));
|
||||||
|
|
||||||
|
conversation.trigger('newmessage', this);
|
||||||
|
|
||||||
|
if (this.get('unread')) {
|
||||||
|
await conversation.notify(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the sent message count if this is an outgoing message
|
||||||
|
if (this.get('type') === 'outgoing') {
|
||||||
|
conversation.incrementSentMessageCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Whisper.events.trigger('incrementProgress');
|
||||||
|
confirm();
|
||||||
|
}
|
||||||
|
|
||||||
async handleReaction(
|
async handleReaction(
|
||||||
reaction: typeof window.WhatIsThis,
|
reaction: typeof window.WhatIsThis,
|
||||||
shouldPersist = true
|
shouldPersist = true
|
||||||
|
|
463
ts/sql/Client.ts
463
ts/sql/Client.ts
File diff suppressed because it is too large
Load diff
|
@ -259,7 +259,12 @@ export type ServerInterface = DataInterface & {
|
||||||
configDir: string;
|
configDir: string;
|
||||||
key: string;
|
key: string;
|
||||||
messages: LocaleMessagesType;
|
messages: LocaleMessagesType;
|
||||||
}) => Promise<boolean>;
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
initializeRenderer: (options: {
|
||||||
|
configDir: string;
|
||||||
|
key: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
removeKnownAttachments: (
|
removeKnownAttachments: (
|
||||||
allAttachments: Array<string>
|
allAttachments: Array<string>
|
||||||
|
@ -393,7 +398,6 @@ export type ClientInterface = DataInterface & {
|
||||||
// Client-side only, and test-only
|
// Client-side only, and test-only
|
||||||
|
|
||||||
_removeConversations: (ids: Array<string>) => Promise<void>;
|
_removeConversations: (ids: Array<string>) => Promise<void>;
|
||||||
_jobs: { [id: string]: ClientJobType };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClientJobType = {
|
export type ClientJobType = {
|
||||||
|
|
141
ts/sql/Queueing.ts
Normal file
141
ts/sql/Queueing.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
// 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 prevous 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);
|
||||||
|
}
|
|
@ -14,7 +14,6 @@ import mkdirp from 'mkdirp';
|
||||||
import rimraf from 'rimraf';
|
import rimraf from 'rimraf';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import sql from '@journeyapps/sqlcipher';
|
import sql from '@journeyapps/sqlcipher';
|
||||||
import { app, clipboard, dialog } from 'electron';
|
|
||||||
|
|
||||||
import pify from 'pify';
|
import pify from 'pify';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
@ -31,8 +30,6 @@ import {
|
||||||
pick,
|
pick,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
|
||||||
import { redactAll } from '../../js/modules/privacy';
|
|
||||||
import { remove as removeUserConfig } from '../../app/user_config';
|
|
||||||
import { combineNames } from '../util/combineNames';
|
import { combineNames } from '../util/combineNames';
|
||||||
|
|
||||||
import { GroupV2MemberType } from '../model-types.d';
|
import { GroupV2MemberType } from '../model-types.d';
|
||||||
|
@ -54,6 +51,7 @@ import {
|
||||||
StickerType,
|
StickerType,
|
||||||
UnprocessedType,
|
UnprocessedType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
|
import { applyQueueing } from './Queueing';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// We want to extend `Function`'s properties, so we need to use an interface.
|
// We want to extend `Function`'s properties, so we need to use an interface.
|
||||||
|
@ -195,13 +193,14 @@ const dataInterface: ServerInterface = {
|
||||||
// Server-only
|
// Server-only
|
||||||
|
|
||||||
initialize,
|
initialize,
|
||||||
|
initializeRenderer,
|
||||||
|
|
||||||
removeKnownAttachments,
|
removeKnownAttachments,
|
||||||
removeKnownStickers,
|
removeKnownStickers,
|
||||||
removeKnownDraftAttachments,
|
removeKnownDraftAttachments,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default dataInterface;
|
export default applyQueueing(dataInterface);
|
||||||
|
|
||||||
function objectToJSON(data: any) {
|
function objectToJSON(data: any) {
|
||||||
return JSON.stringify(data);
|
return JSON.stringify(data);
|
||||||
|
@ -210,6 +209,14 @@ function jsonToObject(json: string): any {
|
||||||
return JSON.parse(json);
|
return JSON.parse(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRenderer() {
|
||||||
|
if (typeof process === 'undefined' || !process) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.type === 'renderer';
|
||||||
|
}
|
||||||
|
|
||||||
async function openDatabase(filePath: string): Promise<sql.Database> {
|
async function openDatabase(filePath: string): Promise<sql.Database> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let instance: sql.Database | undefined;
|
let instance: sql.Database | undefined;
|
||||||
|
@ -1702,6 +1709,7 @@ async function updateSchema(instance: PromisifiedSQLDatabase) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let globalInstance: PromisifiedSQLDatabase | undefined;
|
let globalInstance: PromisifiedSQLDatabase | undefined;
|
||||||
|
let globalInstanceRenderer: PromisifiedSQLDatabase | undefined;
|
||||||
let databaseFilePath: string | undefined;
|
let databaseFilePath: string | undefined;
|
||||||
let indexedDBPath: string | undefined;
|
let indexedDBPath: string | undefined;
|
||||||
|
|
||||||
|
@ -1788,37 +1796,57 @@ async function initialize({
|
||||||
await getMessageCount();
|
await getMessageCount();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Database startup error:', error.stack);
|
console.log('Database startup error:', error.stack);
|
||||||
const buttonIndex = dialog.showMessageBoxSync({
|
|
||||||
buttons: [
|
|
||||||
messages.copyErrorAndQuit.message,
|
|
||||||
messages.deleteAndRestart.message,
|
|
||||||
],
|
|
||||||
defaultId: 0,
|
|
||||||
detail: redactAll(error.stack),
|
|
||||||
message: messages.databaseError.message,
|
|
||||||
noLink: true,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (buttonIndex === 0) {
|
|
||||||
clipboard.writeText(
|
|
||||||
`Database startup error:\n\n${redactAll(error.stack)}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (promisified) {
|
if (promisified) {
|
||||||
await promisified.close();
|
await promisified.close();
|
||||||
}
|
}
|
||||||
await removeDB();
|
throw error;
|
||||||
removeUserConfig();
|
}
|
||||||
app.relaunch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.exit(1);
|
async function initializeRenderer({
|
||||||
|
configDir,
|
||||||
return false;
|
key,
|
||||||
|
}: {
|
||||||
|
configDir: string;
|
||||||
|
key: string;
|
||||||
|
}) {
|
||||||
|
if (!isRenderer()) {
|
||||||
|
throw new Error('Cannot call from main process.');
|
||||||
|
}
|
||||||
|
if (globalInstanceRenderer) {
|
||||||
|
throw new Error('Cannot initialize more than once!');
|
||||||
|
}
|
||||||
|
if (!isString(configDir)) {
|
||||||
|
throw new Error('initialize: configDir is required!');
|
||||||
|
}
|
||||||
|
if (!isString(key)) {
|
||||||
|
throw new Error('initialize: key is required!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (!indexedDBPath) {
|
||||||
|
indexedDBPath = join(configDir, 'IndexedDB');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbDir = join(configDir, 'sql');
|
||||||
|
|
||||||
|
if (!databaseFilePath) {
|
||||||
|
databaseFilePath = join(dbDir, 'db.sqlite');
|
||||||
|
}
|
||||||
|
|
||||||
|
let promisified: PromisifiedSQLDatabase | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
promisified = await openAndSetUpSQLCipher(databaseFilePath, { key });
|
||||||
|
|
||||||
|
// At this point we can allow general access to the database
|
||||||
|
globalInstanceRenderer = promisified;
|
||||||
|
|
||||||
|
// test database
|
||||||
|
await getMessageCount();
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error('Database startup error:', error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function close() {
|
async function close() {
|
||||||
|
@ -1857,6 +1885,13 @@ async function removeIndexedDBFiles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInstance(): PromisifiedSQLDatabase {
|
function getInstance(): PromisifiedSQLDatabase {
|
||||||
|
if (isRenderer()) {
|
||||||
|
if (!globalInstanceRenderer) {
|
||||||
|
throw new Error('getInstance: globalInstanceRenderer not set!');
|
||||||
|
}
|
||||||
|
return globalInstanceRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
if (!globalInstance) {
|
if (!globalInstance) {
|
||||||
throw new Error('getInstance: globalInstance not set!');
|
throw new Error('getInstance: globalInstance not set!');
|
||||||
}
|
}
|
||||||
|
@ -2285,6 +2320,7 @@ async function updateConversation(data: ConversationType) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateConversations(array: Array<ConversationType>) {
|
async function updateConversations(array: Array<ConversationType>) {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
await db.run('BEGIN TRANSACTION;');
|
await db.run('BEGIN TRANSACTION;');
|
||||||
|
|
16
ts/sql/initialize.ts
Normal file
16
ts/sql/initialize.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { ipcRenderer as ipc } from 'electron';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import pify from 'pify';
|
||||||
|
import sql from './Server';
|
||||||
|
|
||||||
|
const getRealPath = pify(fs.realpath);
|
||||||
|
|
||||||
|
export async function initialize(): Promise<void> {
|
||||||
|
const configDir = await getRealPath(ipc.sendSync('get-user-data-path'));
|
||||||
|
const key = ipc.sendSync('user-config-key');
|
||||||
|
|
||||||
|
await sql.initializeRenderer({ configDir, key });
|
||||||
|
}
|
|
@ -23,7 +23,10 @@ describe('Message', () => {
|
||||||
|
|
||||||
function createMessage(attrs: { [key: string]: unknown }) {
|
function createMessage(attrs: { [key: string]: unknown }) {
|
||||||
const messages = new window.Whisper.MessageCollection();
|
const messages = new window.Whisper.MessageCollection();
|
||||||
return messages.add(attrs);
|
return messages.add({
|
||||||
|
received_at: Date.now(),
|
||||||
|
...attrs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
|
|
@ -17,6 +17,7 @@ const toMediaItem = (date: Date): MediaItemType => ({
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: date.getTime(),
|
received_at: date.getTime(),
|
||||||
|
received_at_ms: date.getTime(),
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -57,6 +58,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523534400000,
|
received_at: 1523534400000,
|
||||||
|
received_at_ms: 1523534400000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -71,6 +73,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523491260000,
|
received_at: 1523491260000,
|
||||||
|
received_at_ms: 1523491260000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -90,6 +93,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523491140000,
|
received_at: 1523491140000,
|
||||||
|
received_at_ms: 1523491140000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -109,6 +113,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523232060000,
|
received_at: 1523232060000,
|
||||||
|
received_at_ms: 1523232060000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -128,6 +133,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523231940000,
|
received_at: 1523231940000,
|
||||||
|
received_at_ms: 1523231940000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -142,6 +148,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1522540860000,
|
received_at: 1522540860000,
|
||||||
|
received_at_ms: 1522540860000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -163,6 +170,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1522540740000,
|
received_at: 1522540740000,
|
||||||
|
received_at_ms: 1522540740000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -177,6 +185,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1519912800000,
|
received_at: 1519912800000,
|
||||||
|
received_at_ms: 1519912800000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -198,6 +207,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1298937540000,
|
received_at: 1298937540000,
|
||||||
|
received_at_ms: 1298937540000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
@ -212,6 +222,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
message: {
|
message: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1296554400000,
|
received_at: 1296554400000,
|
||||||
|
received_at_ms: 1296554400000,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
|
|
3
ts/textsecure.d.ts
vendored
3
ts/textsecure.d.ts
vendored
|
@ -23,6 +23,7 @@ export type UnprocessedType = {
|
||||||
decrypted?: string;
|
decrypted?: string;
|
||||||
envelope?: string;
|
envelope?: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
serverTimestamp?: number;
|
serverTimestamp?: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
sourceDevice?: number;
|
sourceDevice?: number;
|
||||||
|
@ -795,6 +796,8 @@ export declare class EnvelopeClass {
|
||||||
|
|
||||||
// Note: these additional properties are added in the course of processing
|
// Note: these additional properties are added in the course of processing
|
||||||
id: string;
|
id: string;
|
||||||
|
receivedAtCounter: number;
|
||||||
|
receivedAtDate: number;
|
||||||
unidentifiedDeliveryReceived?: boolean;
|
unidentifiedDeliveryReceived?: boolean;
|
||||||
messageAgeSec?: number;
|
messageAgeSec?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,7 +203,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
this.cacheAddBatcher = createBatcher<CacheAddItemType>({
|
this.cacheAddBatcher = createBatcher<CacheAddItemType>({
|
||||||
wait: 200,
|
wait: 200,
|
||||||
maxSize: 30,
|
maxSize: 30,
|
||||||
processBatch: this.cacheAndQueueBatch.bind(this),
|
processBatch: this.cacheAndHandleBatch.bind(this),
|
||||||
});
|
});
|
||||||
this.cacheUpdateBatcher = createBatcher<CacheUpdateItemType>({
|
this.cacheUpdateBatcher = createBatcher<CacheUpdateItemType>({
|
||||||
wait: 500,
|
wait: 500,
|
||||||
|
@ -237,7 +237,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We always process our cache before processing a new websocket message
|
// We always process our cache before processing a new websocket message
|
||||||
this.pendingQueue.add(async () => this.queueAllCached());
|
this.pendingQueue.add(async () => this.handleAllCached());
|
||||||
|
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
if (this.hasConnected) {
|
if (this.hasConnected) {
|
||||||
|
@ -428,13 +428,16 @@ class MessageReceiverInner extends EventTarget {
|
||||||
? envelope.serverTimestamp.toNumber()
|
? envelope.serverTimestamp.toNumber()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
envelope.receivedAtCounter = window.Signal.Util.incrementMessageCounter();
|
||||||
|
envelope.receivedAtDate = Date.now();
|
||||||
|
|
||||||
// Calculate the message age (time on server).
|
// Calculate the message age (time on server).
|
||||||
envelope.messageAgeSec = this.calculateMessageAge(
|
envelope.messageAgeSec = this.calculateMessageAge(
|
||||||
headers,
|
headers,
|
||||||
envelope.serverTimestamp
|
envelope.serverTimestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cacheAndQueue(envelope, plaintext, request);
|
this.cacheAndHandle(envelope, plaintext, request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
request.respond(500, 'Bad encrypted websocket message');
|
request.respond(500, 'Bad encrypted websocket message');
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
@ -553,16 +556,17 @@ class MessageReceiverInner extends EventTarget {
|
||||||
this.dispatchEvent(ev);
|
this.dispatchEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueAllCached() {
|
async handleAllCached() {
|
||||||
const items = await this.getAllFromCache();
|
const items = await this.getAllFromCache();
|
||||||
const max = items.length;
|
const max = items.length;
|
||||||
for (let i = 0; i < max; i += 1) {
|
for (let i = 0; i < max; i += 1) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await this.queueCached(items[i]);
|
await this.handleCachedEnvelope(items[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueCached(item: UnprocessedType) {
|
async handleCachedEnvelope(item: UnprocessedType) {
|
||||||
|
window.log.info('MessageReceiver.handleCachedEnvelope', item.id);
|
||||||
try {
|
try {
|
||||||
let envelopePlaintext: ArrayBuffer;
|
let envelopePlaintext: ArrayBuffer;
|
||||||
|
|
||||||
|
@ -576,7 +580,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'MessageReceiver.queueCached: item.envelope was malformed'
|
'MessageReceiver.handleCachedEnvelope: item.envelope was malformed'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -584,6 +588,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
envelopePlaintext
|
envelopePlaintext
|
||||||
);
|
);
|
||||||
envelope.id = item.id;
|
envelope.id = item.id;
|
||||||
|
envelope.receivedAtCounter = item.timestamp;
|
||||||
|
envelope.receivedAtDate = Date.now();
|
||||||
envelope.source = envelope.source || item.source;
|
envelope.source = envelope.source || item.source;
|
||||||
envelope.sourceUuid = envelope.sourceUuid || item.sourceUuid;
|
envelope.sourceUuid = envelope.sourceUuid || item.sourceUuid;
|
||||||
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
|
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
|
||||||
|
@ -605,13 +611,13 @@ class MessageReceiverInner extends EventTarget {
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Cached decrypted value was not a string!');
|
throw new Error('Cached decrypted value was not a string!');
|
||||||
}
|
}
|
||||||
this.queueDecryptedEnvelope(envelope, payloadPlaintext);
|
this.handleDecryptedEnvelope(envelope, payloadPlaintext);
|
||||||
} else {
|
} else {
|
||||||
this.queueEnvelope(envelope);
|
this.handleEnvelope(envelope);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'queueCached error handling item',
|
'handleCachedEnvelope error handling item',
|
||||||
item.id,
|
item.id,
|
||||||
'removing it. Error:',
|
'removing it. Error:',
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
|
@ -622,7 +628,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
await window.textsecure.storage.unprocessed.remove(id);
|
await window.textsecure.storage.unprocessed.remove(id);
|
||||||
} catch (deleteError) {
|
} catch (deleteError) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'queueCached error deleting item',
|
'handleCachedEnvelope error deleting item',
|
||||||
item.id,
|
item.id,
|
||||||
'Error:',
|
'Error:',
|
||||||
deleteError && deleteError.stack ? deleteError.stack : deleteError
|
deleteError && deleteError.stack ? deleteError.stack : deleteError
|
||||||
|
@ -656,7 +662,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
if (this.isEmptied) {
|
if (this.isEmptied) {
|
||||||
this.clearRetryTimeout();
|
this.clearRetryTimeout();
|
||||||
this.retryCachedTimeout = setTimeout(() => {
|
this.retryCachedTimeout = setTimeout(() => {
|
||||||
this.pendingQueue.add(async () => this.queueAllCached());
|
this.pendingQueue.add(async () => this.handleAllCached());
|
||||||
}, RETRY_TIMEOUT);
|
}, RETRY_TIMEOUT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -705,7 +711,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cacheAndQueueBatch(items: Array<CacheAddItemType>) {
|
async cacheAndHandleBatch(items: Array<CacheAddItemType>) {
|
||||||
|
window.log.info('MessageReceiver.cacheAndHandleBatch', items.length);
|
||||||
const dataArray = items.map(item => item.data);
|
const dataArray = items.map(item => item.data);
|
||||||
try {
|
try {
|
||||||
await window.textsecure.storage.unprocessed.batchAdd(dataArray);
|
await window.textsecure.storage.unprocessed.batchAdd(dataArray);
|
||||||
|
@ -714,16 +721,16 @@ class MessageReceiverInner extends EventTarget {
|
||||||
item.request.respond(200, 'OK');
|
item.request.respond(200, 'OK');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'cacheAndQueueBatch: Failed to send 200 to server; still queuing envelope'
|
'cacheAndHandleBatch: Failed to send 200 to server; still queuing envelope'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.queueEnvelope(item.envelope);
|
this.handleEnvelope(item.envelope);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.maybeScheduleRetryTimeout();
|
this.maybeScheduleRetryTimeout();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'cacheAndQueue error trying to add messages to cache:',
|
'cacheAndHandleBatch error trying to add messages to cache:',
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -733,7 +740,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheAndQueue(
|
cacheAndHandle(
|
||||||
envelope: EnvelopeClass,
|
envelope: EnvelopeClass,
|
||||||
plaintext: ArrayBuffer,
|
plaintext: ArrayBuffer,
|
||||||
request: IncomingWebSocketRequest
|
request: IncomingWebSocketRequest
|
||||||
|
@ -743,7 +750,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
id,
|
id,
|
||||||
version: 2,
|
version: 2,
|
||||||
envelope: MessageReceiverInner.arrayBufferToStringBase64(plaintext),
|
envelope: MessageReceiverInner.arrayBufferToStringBase64(plaintext),
|
||||||
timestamp: Date.now(),
|
timestamp: envelope.receivedAtCounter,
|
||||||
attempts: 1,
|
attempts: 1,
|
||||||
};
|
};
|
||||||
this.cacheAddBatcher.add({
|
this.cacheAddBatcher.add({
|
||||||
|
@ -754,6 +761,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
async cacheUpdateBatch(items: Array<Partial<UnprocessedType>>) {
|
async cacheUpdateBatch(items: Array<Partial<UnprocessedType>>) {
|
||||||
|
window.log.info('MessageReceiver.cacheUpdateBatch', items.length);
|
||||||
await window.textsecure.storage.unprocessed.addDecryptedDataToList(items);
|
await window.textsecure.storage.unprocessed.addDecryptedDataToList(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -778,56 +786,6 @@ class MessageReceiverInner extends EventTarget {
|
||||||
this.cacheRemoveBatcher.add(id);
|
this.cacheRemoveBatcher.add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueDecryptedEnvelope(
|
|
||||||
envelope: EnvelopeClass,
|
|
||||||
plaintext: ArrayBuffer
|
|
||||||
) {
|
|
||||||
const id = this.getEnvelopeId(envelope);
|
|
||||||
window.log.info('queueing decrypted envelope', id);
|
|
||||||
|
|
||||||
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
|
|
||||||
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
|
|
||||||
task,
|
|
||||||
`queueEncryptedEnvelope ${id}`
|
|
||||||
);
|
|
||||||
const promise = this.addToQueue(taskWithTimeout);
|
|
||||||
|
|
||||||
return promise.catch(error => {
|
|
||||||
window.log.error(
|
|
||||||
`queueDecryptedEnvelope error handling envelope ${id}:`,
|
|
||||||
error && error.extra ? JSON.stringify(error.extra) : '',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async queueEnvelope(envelope: EnvelopeClass) {
|
|
||||||
const id = this.getEnvelopeId(envelope);
|
|
||||||
window.log.info('queueing envelope', id);
|
|
||||||
|
|
||||||
const task = this.handleEnvelope.bind(this, envelope);
|
|
||||||
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
|
|
||||||
task,
|
|
||||||
`queueEnvelope ${id}`
|
|
||||||
);
|
|
||||||
const promise = this.addToQueue(taskWithTimeout);
|
|
||||||
|
|
||||||
return promise.catch(error => {
|
|
||||||
const args = [
|
|
||||||
'queueEnvelope error handling envelope',
|
|
||||||
this.getEnvelopeId(envelope),
|
|
||||||
':',
|
|
||||||
error && error.extra ? JSON.stringify(error.extra) : '',
|
|
||||||
error && error.stack ? error.stack : error,
|
|
||||||
];
|
|
||||||
if (error.warn) {
|
|
||||||
window.log.warn(...args);
|
|
||||||
} else {
|
|
||||||
window.log.error(...args);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same as handleEnvelope, just without the decryption step. Necessary for handling
|
// Same as handleEnvelope, just without the decryption step. Necessary for handling
|
||||||
// messages which were successfully decrypted, but application logic didn't finish
|
// messages which were successfully decrypted, but application logic didn't finish
|
||||||
// processing.
|
// processing.
|
||||||
|
@ -835,6 +793,10 @@ class MessageReceiverInner extends EventTarget {
|
||||||
envelope: EnvelopeClass,
|
envelope: EnvelopeClass,
|
||||||
plaintext: ArrayBuffer
|
plaintext: ArrayBuffer
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const id = this.getEnvelopeId(envelope);
|
||||||
|
window.log.info('MessageReceiver.handleDecryptedEnvelope', id);
|
||||||
|
|
||||||
|
try {
|
||||||
if (this.stoppingProcessing) {
|
if (this.stoppingProcessing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -854,9 +816,20 @@ class MessageReceiverInner extends EventTarget {
|
||||||
|
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
throw new Error('Received message with no content and no legacyMessage');
|
throw new Error('Received message with no content and no legacyMessage');
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
`handleDecryptedEnvelope error handling envelope ${id}:`,
|
||||||
|
error && error.extra ? JSON.stringify(error.extra) : '',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleEnvelope(envelope: EnvelopeClass) {
|
async handleEnvelope(envelope: EnvelopeClass) {
|
||||||
|
const id = this.getEnvelopeId(envelope);
|
||||||
|
window.log.info('MessageReceiver.handleEnvelope', id);
|
||||||
|
|
||||||
|
try {
|
||||||
if (this.stoppingProcessing) {
|
if (this.stoppingProcessing) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -871,8 +844,26 @@ class MessageReceiverInner extends EventTarget {
|
||||||
if (envelope.legacyMessage) {
|
if (envelope.legacyMessage) {
|
||||||
return this.handleLegacyMessage(envelope);
|
return this.handleLegacyMessage(envelope);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
|
|
||||||
throw new Error('Received message with no content and no legacyMessage');
|
throw new Error('Received message with no content and no legacyMessage');
|
||||||
|
} catch (error) {
|
||||||
|
const args = [
|
||||||
|
'handleEnvelope error handling envelope',
|
||||||
|
this.getEnvelopeId(envelope),
|
||||||
|
':',
|
||||||
|
error && error.extra ? JSON.stringify(error.extra) : '',
|
||||||
|
error && error.stack ? error.stack : error,
|
||||||
|
];
|
||||||
|
if (error.warn) {
|
||||||
|
window.log.warn(...args);
|
||||||
|
} else {
|
||||||
|
window.log.error(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
|
@ -1257,6 +1248,10 @@ class MessageReceiverInner extends EventTarget {
|
||||||
envelope: EnvelopeClass,
|
envelope: EnvelopeClass,
|
||||||
sentContainer: SyncMessageClass.Sent
|
sentContainer: SyncMessageClass.Sent
|
||||||
) {
|
) {
|
||||||
|
window.log.info(
|
||||||
|
'MessageReceiver.handleSentMessage',
|
||||||
|
this.getEnvelopeId(envelope)
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
destination,
|
destination,
|
||||||
destinationUuid,
|
destinationUuid,
|
||||||
|
@ -1324,6 +1319,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
unidentifiedStatus,
|
unidentifiedStatus,
|
||||||
message,
|
message,
|
||||||
isRecipientUpdate,
|
isRecipientUpdate,
|
||||||
|
receivedAtCounter: envelope.receivedAtCounter,
|
||||||
|
receivedAtDate: envelope.receivedAtDate,
|
||||||
};
|
};
|
||||||
if (expirationStartTimestamp) {
|
if (expirationStartTimestamp) {
|
||||||
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
|
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
|
||||||
|
@ -1334,7 +1331,10 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDataMessage(envelope: EnvelopeClass, msg: DataMessageClass) {
|
async handleDataMessage(envelope: EnvelopeClass, msg: DataMessageClass) {
|
||||||
window.log.info('data message from', this.getEnvelopeId(envelope));
|
window.log.info(
|
||||||
|
'MessageReceiver.handleDataMessage',
|
||||||
|
this.getEnvelopeId(envelope)
|
||||||
|
);
|
||||||
let p: Promise<any> = Promise.resolve();
|
let p: Promise<any> = Promise.resolve();
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
const destination = envelope.sourceUuid || envelope.source;
|
const destination = envelope.sourceUuid || envelope.source;
|
||||||
|
@ -1412,6 +1412,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
serverTimestamp: envelope.serverTimestamp,
|
serverTimestamp: envelope.serverTimestamp,
|
||||||
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
|
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
|
||||||
message,
|
message,
|
||||||
|
receivedAtCounter: envelope.receivedAtCounter,
|
||||||
|
receivedAtDate: envelope.receivedAtDate,
|
||||||
};
|
};
|
||||||
return this.dispatchAndWait(ev);
|
return this.dispatchAndWait(ev);
|
||||||
})
|
})
|
||||||
|
@ -1419,6 +1421,10 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLegacyMessage(envelope: EnvelopeClass) {
|
async handleLegacyMessage(envelope: EnvelopeClass) {
|
||||||
|
window.log.info(
|
||||||
|
'MessageReceiver.handleLegacyMessage',
|
||||||
|
this.getEnvelopeId(envelope)
|
||||||
|
);
|
||||||
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => {
|
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => {
|
||||||
if (!plaintext) {
|
if (!plaintext) {
|
||||||
window.log.warn('handleLegacyMessage: plaintext was falsey');
|
window.log.warn('handleLegacyMessage: plaintext was falsey');
|
||||||
|
@ -1437,6 +1443,10 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleContentMessage(envelope: EnvelopeClass) {
|
async handleContentMessage(envelope: EnvelopeClass) {
|
||||||
|
window.log.info(
|
||||||
|
'MessageReceiver.handleContentMessage',
|
||||||
|
this.getEnvelopeId(envelope)
|
||||||
|
);
|
||||||
return this.decrypt(envelope, envelope.content).then(plaintext => {
|
return this.decrypt(envelope, envelope.content).then(plaintext => {
|
||||||
if (!plaintext) {
|
if (!plaintext) {
|
||||||
window.log.warn('handleContentMessage: plaintext was falsey');
|
window.log.warn('handleContentMessage: plaintext was falsey');
|
||||||
|
@ -1579,7 +1589,10 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNullMessage(envelope: EnvelopeClass) {
|
handleNullMessage(envelope: EnvelopeClass) {
|
||||||
window.log.info('null message from', this.getEnvelopeId(envelope));
|
window.log.info(
|
||||||
|
'MessageReceiver.handleNullMessage',
|
||||||
|
this.getEnvelopeId(envelope)
|
||||||
|
);
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1778,7 +1791,6 @@ class MessageReceiverInner extends EventTarget {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (syncMessage.read && syncMessage.read.length) {
|
if (syncMessage.read && syncMessage.read.length) {
|
||||||
window.log.info('read messages from', this.getEnvelopeId(envelope));
|
|
||||||
return this.handleRead(envelope, syncMessage.read);
|
return this.handleRead(envelope, syncMessage.read);
|
||||||
}
|
}
|
||||||
if (syncMessage.verified) {
|
if (syncMessage.verified) {
|
||||||
|
@ -1952,6 +1964,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
envelope: EnvelopeClass,
|
envelope: EnvelopeClass,
|
||||||
read: Array<SyncMessageClass.Read>
|
read: Array<SyncMessageClass.Read>
|
||||||
) {
|
) {
|
||||||
|
window.log.info('MessageReceiver.handleRead', this.getEnvelopeId(envelope));
|
||||||
const results = [];
|
const results = [];
|
||||||
for (let i = 0; i < read.length; i += 1) {
|
for (let i = 0; i < read.length; i += 1) {
|
||||||
const ev = new Event('readSync');
|
const ev = new Event('readSync');
|
||||||
|
|
8
ts/util/getMessageTimestamp.ts
Normal file
8
ts/util/getMessageTimestamp.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { Message } from '../components/conversation/media-gallery/types/Message';
|
||||||
|
|
||||||
|
export function getMessageTimestamp(message: Message): number {
|
||||||
|
return message.received_at_ms || message.received_at;
|
||||||
|
}
|
23
ts/util/incrementMessageCounter.ts
Normal file
23
ts/util/incrementMessageCounter.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
export function incrementMessageCounter(): number {
|
||||||
|
if (!window.receivedAtCounter) {
|
||||||
|
window.receivedAtCounter =
|
||||||
|
Number(localStorage.getItem('lastReceivedAtCounter')) || Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.receivedAtCounter += 1;
|
||||||
|
debouncedUpdateLastReceivedAt();
|
||||||
|
|
||||||
|
return window.receivedAtCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedUpdateLastReceivedAt = debounce(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'lastReceivedAtCounter',
|
||||||
|
String(window.receivedAtCounter)
|
||||||
|
);
|
||||||
|
}, 500);
|
|
@ -14,8 +14,10 @@ import { getStringForProfileChange } from './getStringForProfileChange';
|
||||||
import { getTextWithMentions } from './getTextWithMentions';
|
import { getTextWithMentions } from './getTextWithMentions';
|
||||||
import { getUserAgent } from './getUserAgent';
|
import { getUserAgent } from './getUserAgent';
|
||||||
import { hasExpired } from './hasExpired';
|
import { hasExpired } from './hasExpired';
|
||||||
|
import { incrementMessageCounter } from './incrementMessageCounter';
|
||||||
import { isFileDangerous } from './isFileDangerous';
|
import { isFileDangerous } from './isFileDangerous';
|
||||||
import { makeLookup } from './makeLookup';
|
import { makeLookup } from './makeLookup';
|
||||||
|
import { saveNewMessageBatcher, updateMessageBatcher } from './messageBatcher';
|
||||||
import { missingCaseError } from './missingCaseError';
|
import { missingCaseError } from './missingCaseError';
|
||||||
import { parseRemoteClientExpiration } from './parseRemoteClientExpiration';
|
import { parseRemoteClientExpiration } from './parseRemoteClientExpiration';
|
||||||
import { sleep } from './sleep';
|
import { sleep } from './sleep';
|
||||||
|
@ -29,6 +31,8 @@ import {
|
||||||
import * as zkgroup from './zkgroup';
|
import * as zkgroup from './zkgroup';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
GoogleChrome,
|
||||||
|
Registration,
|
||||||
arrayBufferToObjectURL,
|
arrayBufferToObjectURL,
|
||||||
combineNames,
|
combineNames,
|
||||||
createBatcher,
|
createBatcher,
|
||||||
|
@ -40,18 +44,19 @@ export {
|
||||||
getStringForProfileChange,
|
getStringForProfileChange,
|
||||||
getTextWithMentions,
|
getTextWithMentions,
|
||||||
getUserAgent,
|
getUserAgent,
|
||||||
GoogleChrome,
|
|
||||||
hasExpired,
|
hasExpired,
|
||||||
|
incrementMessageCounter,
|
||||||
isFileDangerous,
|
isFileDangerous,
|
||||||
longRunningTaskWrapper,
|
longRunningTaskWrapper,
|
||||||
makeLookup,
|
makeLookup,
|
||||||
mapToSupportLocale,
|
mapToSupportLocale,
|
||||||
missingCaseError,
|
missingCaseError,
|
||||||
parseRemoteClientExpiration,
|
parseRemoteClientExpiration,
|
||||||
Registration,
|
saveNewMessageBatcher,
|
||||||
sessionRecordToProtobuf,
|
sessionRecordToProtobuf,
|
||||||
sessionStructureToArrayBuffer,
|
sessionStructureToArrayBuffer,
|
||||||
sleep,
|
sleep,
|
||||||
toWebSafeBase64,
|
toWebSafeBase64,
|
||||||
|
updateMessageBatcher,
|
||||||
zkgroup,
|
zkgroup,
|
||||||
};
|
};
|
||||||
|
|
|
@ -15022,7 +15022,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.js",
|
"path": "ts/components/conversation/media-gallery/MediaGallery.js",
|
||||||
"line": " this.focusRef = react_1.default.createRef();",
|
"line": " this.focusRef = react_1.default.createRef();",
|
||||||
"lineNumber": 31,
|
"lineNumber": 32,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-01T22:46:33.013Z",
|
"updated": "2019-11-01T22:46:33.013Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
|
@ -15031,7 +15031,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
|
"path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
|
||||||
"line": " public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||||
"lineNumber": 71,
|
"lineNumber": 72,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-01T22:46:33.013Z",
|
"updated": "2019-11-01T22:46:33.013Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
|
|
24
ts/util/messageBatcher.ts
Normal file
24
ts/util/messageBatcher.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { MessageAttributesType } from '../model-types.d';
|
||||||
|
import { createBatcher } from './batcher';
|
||||||
|
import { createWaitBatcher } from './waitBatcher';
|
||||||
|
|
||||||
|
export const updateMessageBatcher = createBatcher<MessageAttributesType>({
|
||||||
|
wait: 500,
|
||||||
|
maxSize: 50,
|
||||||
|
processBatch: async (messages: Array<MessageAttributesType>) => {
|
||||||
|
window.log.info('updateMessageBatcher', messages.length);
|
||||||
|
await window.Signal.Data.saveMessages(messages, {});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const saveNewMessageBatcher = createWaitBatcher<MessageAttributesType>({
|
||||||
|
wait: 500,
|
||||||
|
maxSize: 30,
|
||||||
|
processBatch: async (messages: Array<MessageAttributesType>) => {
|
||||||
|
window.log.info('saveNewMessageBatcher', messages.length);
|
||||||
|
await window.Signal.Data.saveMessages(messages, { forceSave: true });
|
||||||
|
},
|
||||||
|
});
|
7
ts/window.d.ts
vendored
7
ts/window.d.ts
vendored
|
@ -137,10 +137,12 @@ declare global {
|
||||||
|
|
||||||
WhatIsThis: WhatIsThis;
|
WhatIsThis: WhatIsThis;
|
||||||
|
|
||||||
|
attachmentDownloadQueue: Array<MessageModel>;
|
||||||
baseAttachmentsPath: string;
|
baseAttachmentsPath: string;
|
||||||
baseStickersPath: string;
|
baseStickersPath: string;
|
||||||
baseTempPath: string;
|
baseTempPath: string;
|
||||||
dcodeIO: DCodeIOType;
|
dcodeIO: DCodeIOType;
|
||||||
|
receivedAtCounter: number;
|
||||||
enterKeyboardMode: () => void;
|
enterKeyboardMode: () => void;
|
||||||
enterMouseMode: () => void;
|
enterMouseMode: () => void;
|
||||||
getAccountManager: () => AccountManager | undefined;
|
getAccountManager: () => AccountManager | undefined;
|
||||||
|
@ -246,6 +248,9 @@ declare global {
|
||||||
titleBarDoubleClick: () => void;
|
titleBarDoubleClick: () => void;
|
||||||
unregisterForActive: (handler: () => void) => void;
|
unregisterForActive: (handler: () => void) => void;
|
||||||
updateTrayIcon: (count: number) => void;
|
updateTrayIcon: (count: number) => void;
|
||||||
|
sqlInitializer: {
|
||||||
|
initialize: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
Backbone: typeof Backbone;
|
Backbone: typeof Backbone;
|
||||||
Signal: {
|
Signal: {
|
||||||
|
@ -561,6 +566,8 @@ export type DCodeIOType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type MessageControllerType = {
|
type MessageControllerType = {
|
||||||
|
findBySender: (sender: string) => MessageModel | null;
|
||||||
|
findBySentAt: (sentAt: number) => MessageModel | null;
|
||||||
register: (id: string, model: MessageModel) => MessageModel;
|
register: (id: string, model: MessageModel) => MessageModel;
|
||||||
unregister: (id: string) => void;
|
unregister: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue