2018-07-27 01:13:56 +00:00
|
|
|
/* global window, setTimeout */
|
|
|
|
|
|
|
|
const electron = require('electron');
|
|
|
|
const { forEach, isFunction, isObject } = require('lodash');
|
2018-07-25 22:02:27 +00:00
|
|
|
|
|
|
|
const { deferredToPromise } = require('./deferred_to_promise');
|
|
|
|
const MessageType = require('./types/message');
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
const { ipcRenderer } = electron;
|
|
|
|
|
|
|
|
// We listen to a lot of events on ipcRenderer, often on the same channel. This prevents
|
|
|
|
// any warnings that might be sent to the console in that case.
|
|
|
|
ipcRenderer.setMaxListeners(0);
|
|
|
|
|
|
|
|
// calls to search for when finding functions to convert:
|
2018-07-25 22:02:27 +00:00
|
|
|
// .fetch(
|
|
|
|
// .save(
|
|
|
|
// .destroy(
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
const SQL_CHANNEL_KEY = 'sql-channel';
|
|
|
|
const ERASE_SQL_KEY = 'erase-sql-key';
|
|
|
|
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
|
|
|
|
|
|
|
const _jobs = Object.create(null);
|
|
|
|
const _DEBUG = false;
|
|
|
|
let _jobCounter = 0;
|
|
|
|
|
|
|
|
const channels = {};
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
_jobs,
|
|
|
|
_cleanData,
|
|
|
|
|
|
|
|
close,
|
|
|
|
removeDB,
|
|
|
|
|
2018-08-06 23:18:58 +00:00
|
|
|
getMessageCount,
|
2018-07-27 01:13:56 +00:00
|
|
|
saveMessage,
|
2018-08-01 19:38:48 +00:00
|
|
|
saveLegacyMessage,
|
2018-07-27 01:13:56 +00:00
|
|
|
saveMessages,
|
|
|
|
removeMessage,
|
2018-08-01 02:51:17 +00:00
|
|
|
_removeMessages,
|
2018-07-27 01:13:56 +00:00
|
|
|
getUnreadByConversation,
|
|
|
|
|
|
|
|
removeAllMessagesInConversation,
|
|
|
|
|
|
|
|
getMessageBySender,
|
|
|
|
getMessageById,
|
|
|
|
getAllMessageIds,
|
|
|
|
getMessagesBySentAt,
|
|
|
|
getExpiredMessages,
|
2018-08-07 19:33:56 +00:00
|
|
|
getOutgoingWithoutExpiresAt,
|
2018-07-27 01:13:56 +00:00
|
|
|
getNextExpiringMessage,
|
|
|
|
getMessagesByConversation,
|
|
|
|
|
|
|
|
getAllUnprocessed,
|
|
|
|
getUnprocessedById,
|
|
|
|
saveUnprocessed,
|
|
|
|
saveUnprocesseds,
|
|
|
|
updateUnprocessed,
|
|
|
|
removeUnprocessed,
|
|
|
|
removeAllUnprocessed,
|
|
|
|
|
|
|
|
removeAll,
|
|
|
|
removeOtherData,
|
|
|
|
|
|
|
|
// Returning plain JSON
|
|
|
|
getMessagesNeedingUpgrade,
|
|
|
|
getLegacyMessagesNeedingUpgrade,
|
|
|
|
getMessagesWithVisualMediaAttachments,
|
|
|
|
getMessagesWithFileAttachments,
|
|
|
|
};
|
|
|
|
|
|
|
|
// When IPC arguments are prepared for the cross-process send, they are JSON.stringified.
|
|
|
|
// We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates).
|
|
|
|
function _cleanData(data) {
|
|
|
|
const keys = Object.keys(data);
|
|
|
|
for (let index = 0, max = keys.length; index < max; index += 1) {
|
|
|
|
const key = keys[index];
|
|
|
|
const value = data[key];
|
|
|
|
|
|
|
|
if (value === null || value === undefined) {
|
|
|
|
// eslint-disable-next-line no-continue
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isFunction(value.toNumber)) {
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
data[key] = value.toNumber();
|
|
|
|
} else if (Array.isArray(value)) {
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
data[key] = value.map(item => _cleanData(item));
|
|
|
|
} else if (isObject(value)) {
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
data[key] = _cleanData(value);
|
|
|
|
} else if (
|
|
|
|
typeof value !== 'string' &&
|
|
|
|
typeof value !== 'number' &&
|
|
|
|
typeof value !== 'boolean'
|
|
|
|
) {
|
|
|
|
window.log.info(`_cleanData: key ${key} had type ${typeof value}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _makeJob(fnName) {
|
|
|
|
_jobCounter += 1;
|
|
|
|
const id = _jobCounter;
|
|
|
|
|
|
|
|
_jobs[id] = {
|
|
|
|
fnName,
|
|
|
|
};
|
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _updateJob(id, data) {
|
|
|
|
const { resolve, reject } = data;
|
|
|
|
|
|
|
|
_jobs[id] = {
|
|
|
|
..._jobs[id],
|
|
|
|
...data,
|
|
|
|
resolve: value => {
|
|
|
|
_removeJob(id);
|
|
|
|
return resolve(value);
|
|
|
|
},
|
|
|
|
reject: error => {
|
|
|
|
_removeJob(id);
|
|
|
|
return reject(error);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function _removeJob(id) {
|
|
|
|
if (_DEBUG) {
|
|
|
|
_jobs[id].complete = true;
|
|
|
|
} else {
|
|
|
|
delete _jobs[id];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function _getJob(id) {
|
|
|
|
return _jobs[id];
|
|
|
|
}
|
|
|
|
|
|
|
|
ipcRenderer.on(
|
|
|
|
`${SQL_CHANNEL_KEY}-done`,
|
|
|
|
(event, jobId, errorForDisplay, result) => {
|
|
|
|
const job = _getJob(jobId);
|
|
|
|
if (!job) {
|
|
|
|
throw new Error(
|
|
|
|
`Received job reply to job ${jobId}, but did not have it in our registry!`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { resolve, reject, fnName } = job;
|
|
|
|
|
|
|
|
if (errorForDisplay) {
|
|
|
|
return reject(
|
|
|
|
new Error(`Error calling channel ${fnName}: ${errorForDisplay}`)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return resolve(result);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
function makeChannel(fnName) {
|
|
|
|
channels[fnName] = (...args) => {
|
|
|
|
const jobId = _makeJob(fnName);
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
ipcRenderer.send(SQL_CHANNEL_KEY, jobId, fnName, ...args);
|
|
|
|
|
|
|
|
_updateJob(jobId, {
|
|
|
|
resolve,
|
|
|
|
reject,
|
|
|
|
args: _DEBUG ? args : null,
|
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(
|
2018-08-06 18:12:37 +00:00
|
|
|
() => reject(new Error(`Request to ${fnName} timed out`)),
|
|
|
|
10000
|
2018-07-27 01:13:56 +00:00
|
|
|
);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
forEach(module.exports, fn => {
|
|
|
|
if (isFunction(fn)) {
|
|
|
|
makeChannel(fn.name);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Note: will need to restart the app after calling this, to set up afresh
|
|
|
|
async function close() {
|
|
|
|
await channels.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: will need to restart the app after calling this, to set up afresh
|
|
|
|
async function removeDB() {
|
|
|
|
await channels.removeDB();
|
|
|
|
}
|
|
|
|
|
2018-08-06 23:18:58 +00:00
|
|
|
async function getMessageCount() {
|
|
|
|
return channels.getMessageCount();
|
|
|
|
}
|
|
|
|
|
2018-08-03 04:12:27 +00:00
|
|
|
async function saveMessage(data, { forceSave, Message } = {}) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const id = await channels.saveMessage(_cleanData(data), { forceSave });
|
2018-08-03 04:12:27 +00:00
|
|
|
Message.refreshExpirationTimer();
|
2018-07-27 01:13:56 +00:00
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2018-08-01 19:38:48 +00:00
|
|
|
async function saveLegacyMessage(data, { Message }) {
|
|
|
|
const message = new Message(data);
|
|
|
|
await deferredToPromise(message.save());
|
|
|
|
return message.id;
|
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function saveMessages(arrayOfMessages, { forceSave } = {}) {
|
|
|
|
await channels.saveMessages(_cleanData(arrayOfMessages), { forceSave });
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function removeMessage(id, { Message }) {
|
|
|
|
const message = await getMessageById(id, { Message });
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2018-07-25 22:02:27 +00:00
|
|
|
// Note: It's important to have a fully database-hydrated model to delete here because
|
|
|
|
// it needs to delete all associated on-disk files along with the database delete.
|
|
|
|
if (message) {
|
2018-07-27 01:13:56 +00:00
|
|
|
await channels.removeMessage(id);
|
|
|
|
const model = new Message(message);
|
|
|
|
await model.cleanup();
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-01 02:51:17 +00:00
|
|
|
// Note: this method will not clean up external files, just delete from SQL
|
|
|
|
async function _removeMessages(ids) {
|
|
|
|
await channels.removeMessage(ids);
|
|
|
|
}
|
|
|
|
|
2018-07-25 22:02:27 +00:00
|
|
|
async function getMessageById(id, { Message }) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const message = await channels.getMessageById(id);
|
|
|
|
return new Message(message);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getAllMessageIds() {
|
|
|
|
const ids = await channels.getAllMessageIds();
|
|
|
|
return ids;
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessageBySender(
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
{ source, sourceDevice, sent_at },
|
|
|
|
{ Message }
|
|
|
|
) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const messages = await channels.getMessageBySender({
|
|
|
|
source,
|
|
|
|
sourceDevice,
|
|
|
|
sent_at,
|
|
|
|
});
|
|
|
|
if (!messages || !messages.length) {
|
2018-07-25 22:02:27 +00:00
|
|
|
return null;
|
|
|
|
}
|
2018-07-27 01:13:56 +00:00
|
|
|
|
|
|
|
return new Message(messages[0]);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getUnreadByConversation(conversationId, { MessageCollection }) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const messages = await channels.getUnreadByConversation(conversationId);
|
|
|
|
return new MessageCollection(messages);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessagesByConversation(
|
|
|
|
conversationId,
|
|
|
|
{ limit = 100, receivedAt = Number.MAX_VALUE, MessageCollection }
|
|
|
|
) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const messages = await channels.getMessagesByConversation(conversationId, {
|
2018-07-25 22:02:27 +00:00
|
|
|
limit,
|
2018-07-27 01:13:56 +00:00
|
|
|
receivedAt,
|
|
|
|
});
|
2018-07-25 22:02:27 +00:00
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
return new MessageCollection(messages);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function removeAllMessagesInConversation(
|
|
|
|
conversationId,
|
|
|
|
{ MessageCollection }
|
|
|
|
) {
|
2018-07-27 01:13:56 +00:00
|
|
|
let messages;
|
2018-07-25 22:02:27 +00:00
|
|
|
do {
|
|
|
|
// Yes, we really want the await in the loop. We're deleting 100 at a
|
|
|
|
// time so we don't use too much memory.
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
2018-07-27 01:13:56 +00:00
|
|
|
messages = await getMessagesByConversation(conversationId, {
|
|
|
|
limit: 100,
|
|
|
|
MessageCollection,
|
|
|
|
});
|
2018-07-25 22:02:27 +00:00
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
if (!messages.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ids = messages.map(message => message.id);
|
2018-07-25 22:02:27 +00:00
|
|
|
|
|
|
|
// Note: It's very important that these models are fully hydrated because
|
|
|
|
// we need to delete all associated on-disk files along with the database delete.
|
2018-07-27 01:13:56 +00:00
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
await Promise.all(messages.map(message => message.cleanup()));
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
await channels.removeMessage(ids);
|
|
|
|
} while (messages.length > 0);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessagesBySentAt(sentAt, { MessageCollection }) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const messages = await channels.getMessagesBySentAt(sentAt);
|
|
|
|
return new MessageCollection(messages);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getExpiredMessages({ MessageCollection }) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const messages = await channels.getExpiredMessages();
|
|
|
|
return new MessageCollection(messages);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-08-07 19:33:56 +00:00
|
|
|
async function getOutgoingWithoutExpiresAt({ MessageCollection }) {
|
|
|
|
const messages = await channels.getOutgoingWithoutExpiresAt();
|
|
|
|
return new MessageCollection(messages);
|
|
|
|
}
|
|
|
|
|
2018-07-25 22:02:27 +00:00
|
|
|
async function getNextExpiringMessage({ MessageCollection }) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const messages = await channels.getNextExpiringMessage();
|
|
|
|
return new MessageCollection(messages);
|
|
|
|
}
|
2018-07-25 22:02:27 +00:00
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getAllUnprocessed() {
|
|
|
|
return channels.getAllUnprocessed();
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getUnprocessedById(id, { Unprocessed }) {
|
|
|
|
const unprocessed = await channels.getUnprocessedById(id);
|
|
|
|
return new Unprocessed(unprocessed);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function saveUnprocessed(data, { forceSave } = {}) {
|
|
|
|
const id = await channels.saveUnprocessed(_cleanData(data), { forceSave });
|
|
|
|
return id;
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function saveUnprocesseds(arrayOfUnprocessed, { forceSave } = {}) {
|
|
|
|
await channels.saveUnprocesseds(_cleanData(arrayOfUnprocessed), {
|
|
|
|
forceSave,
|
2018-07-25 22:02:27 +00:00
|
|
|
});
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
2018-07-25 22:02:27 +00:00
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function updateUnprocessed(id, updates) {
|
|
|
|
const existing = await channels.getUnprocessedById(id);
|
|
|
|
if (!existing) {
|
|
|
|
throw new Error(`Unprocessed id ${id} does not exist in the database!`);
|
|
|
|
}
|
|
|
|
const toSave = {
|
|
|
|
...existing,
|
|
|
|
...updates,
|
|
|
|
};
|
2018-07-25 22:02:27 +00:00
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
await saveUnprocessed(toSave);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function removeUnprocessed(id) {
|
|
|
|
await channels.removeUnprocessed(id);
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function removeAllUnprocessed() {
|
2018-07-27 01:13:56 +00:00
|
|
|
await channels.removeAllUnprocessed();
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function removeAll() {
|
2018-07-27 01:13:56 +00:00
|
|
|
await channels.removeAll();
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
// Note: will need to restart the app after calling this, to set up afresh
|
|
|
|
async function removeOtherData() {
|
|
|
|
await Promise.all([
|
|
|
|
callChannel(ERASE_SQL_KEY),
|
|
|
|
callChannel(ERASE_ATTACHMENTS_KEY),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function callChannel(name) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
ipcRenderer.send(name);
|
|
|
|
ipcRenderer.once(`${name}-done`, (event, error) => {
|
|
|
|
if (error) {
|
|
|
|
return reject(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
return resolve();
|
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
() => reject(new Error(`callChannel call to ${name} timed out`)),
|
|
|
|
5000
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Functions below here return JSON
|
|
|
|
|
|
|
|
async function getLegacyMessagesNeedingUpgrade(
|
2018-07-27 02:19:34 +00:00
|
|
|
limit,
|
|
|
|
{ MessageCollection, maxVersion = MessageType.CURRENT_SCHEMA_VERSION }
|
|
|
|
) {
|
2018-07-25 22:02:27 +00:00
|
|
|
const messages = new MessageCollection();
|
|
|
|
|
|
|
|
await deferredToPromise(
|
|
|
|
messages.fetch({
|
|
|
|
limit,
|
|
|
|
index: {
|
|
|
|
name: 'schemaVersion',
|
2018-07-27 02:19:34 +00:00
|
|
|
upper: maxVersion,
|
2018-07-25 22:02:27 +00:00
|
|
|
excludeUpper: true,
|
|
|
|
order: 'desc',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
const models = messages.models || [];
|
|
|
|
return models.map(model => model.toJSON());
|
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getMessagesNeedingUpgrade(
|
|
|
|
limit,
|
|
|
|
{ maxVersion = MessageType.CURRENT_SCHEMA_VERSION }
|
2018-07-25 22:02:27 +00:00
|
|
|
) {
|
2018-07-27 01:13:56 +00:00
|
|
|
const messages = await channels.getMessagesNeedingUpgrade(limit, {
|
|
|
|
maxVersion,
|
|
|
|
});
|
2018-07-25 22:02:27 +00:00
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
return messages;
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getMessagesWithVisualMediaAttachments(
|
2018-07-25 22:02:27 +00:00
|
|
|
conversationId,
|
2018-07-27 01:13:56 +00:00
|
|
|
{ limit }
|
2018-07-25 22:02:27 +00:00
|
|
|
) {
|
2018-07-27 01:13:56 +00:00
|
|
|
return channels.getMessagesWithVisualMediaAttachments(conversationId, {
|
|
|
|
limit,
|
|
|
|
});
|
2018-07-25 22:02:27 +00:00
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getMessagesWithFileAttachments(conversationId, { limit }) {
|
|
|
|
return channels.getMessagesWithFileAttachments(conversationId, {
|
|
|
|
limit,
|
|
|
|
});
|
|
|
|
}
|