Format all source code using Prettier
This commit is contained in:
parent
b4dee3f30b
commit
1dd87ad197
149 changed files with 17847 additions and 15439 deletions
|
@ -20,21 +20,25 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
|
|||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
loadImage(fileOrBlobOrURL, (canvasOrError) => {
|
||||
if (canvasOrError.type === 'error') {
|
||||
const error = new Error('autoOrientImage: Failed to process image');
|
||||
error.cause = canvasOrError;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
loadImage(
|
||||
fileOrBlobOrURL,
|
||||
canvasOrError => {
|
||||
if (canvasOrError.type === 'error') {
|
||||
const error = new Error('autoOrientImage: Failed to process image');
|
||||
error.cause = canvasOrError;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasOrError;
|
||||
const dataURL = canvas.toDataURL(
|
||||
optionsWithDefaults.type,
|
||||
optionsWithDefaults.quality
|
||||
);
|
||||
const canvas = canvasOrError;
|
||||
const dataURL = canvas.toDataURL(
|
||||
optionsWithDefaults.type,
|
||||
optionsWithDefaults.quality
|
||||
);
|
||||
|
||||
resolve(dataURL);
|
||||
}, optionsWithDefaults);
|
||||
resolve(dataURL);
|
||||
},
|
||||
optionsWithDefaults
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -23,12 +23,7 @@ const electronRemote = require('electron').remote;
|
|||
const Attachment = require('./types/attachment');
|
||||
const crypto = require('./crypto');
|
||||
|
||||
|
||||
const {
|
||||
dialog,
|
||||
BrowserWindow,
|
||||
} = electronRemote;
|
||||
|
||||
const { dialog, BrowserWindow } = electronRemote;
|
||||
|
||||
module.exports = {
|
||||
getDirectoryForExport,
|
||||
|
@ -44,7 +39,6 @@ module.exports = {
|
|||
_getConversationLoggingName,
|
||||
};
|
||||
|
||||
|
||||
function stringify(object) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in object) {
|
||||
|
@ -69,10 +63,12 @@ function unstringify(object) {
|
|||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in object) {
|
||||
const val = object[key];
|
||||
if (val &&
|
||||
val.type === 'ArrayBuffer' &&
|
||||
val.encoding === 'base64' &&
|
||||
typeof val.data === 'string') {
|
||||
if (
|
||||
val &&
|
||||
val.type === 'ArrayBuffer' &&
|
||||
val.encoding === 'base64' &&
|
||||
typeof val.data === 'string'
|
||||
) {
|
||||
object[key] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer();
|
||||
} else if (val instanceof Object) {
|
||||
object[key] = unstringify(object[key]);
|
||||
|
@ -86,19 +82,22 @@ function createOutputStream(writer) {
|
|||
return {
|
||||
write(string) {
|
||||
// eslint-disable-next-line more/no-then
|
||||
wait = wait.then(() => new Promise((resolve) => {
|
||||
if (writer.write(string)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
wait = wait.then(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
if (writer.write(string)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// If write() returns true, we don't need to wait for the drain event
|
||||
// https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable
|
||||
writer.once('drain', resolve);
|
||||
// If write() returns true, we don't need to wait for the drain event
|
||||
// https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable
|
||||
writer.once('drain', resolve);
|
||||
|
||||
// We don't register for the 'error' event here, only in close(). Otherwise,
|
||||
// we'll get "Possible EventEmitter memory leak detected" warnings.
|
||||
}));
|
||||
// We don't register for the 'error' event here, only in close(). Otherwise,
|
||||
// we'll get "Possible EventEmitter memory leak detected" warnings.
|
||||
})
|
||||
);
|
||||
return wait;
|
||||
},
|
||||
async close() {
|
||||
|
@ -141,7 +140,7 @@ function exportContactsAndGroups(db, fileWriter) {
|
|||
|
||||
stream.write('{');
|
||||
|
||||
_.each(storeNames, (storeName) => {
|
||||
_.each(storeNames, storeName => {
|
||||
// Both the readwrite permission and the multi-store transaction are required to
|
||||
// keep this function working. They serve to serialize all of these transactions,
|
||||
// one per store to be exported.
|
||||
|
@ -167,7 +166,7 @@ function exportContactsAndGroups(db, fileWriter) {
|
|||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async (event) => {
|
||||
request.onsuccess = async event => {
|
||||
if (count === 0) {
|
||||
console.log('cursor opened');
|
||||
stream.write(`"${storeName}": [`);
|
||||
|
@ -180,10 +179,7 @@ function exportContactsAndGroups(db, fileWriter) {
|
|||
}
|
||||
|
||||
// Preventing base64'd images from reaching the disk, making db.json too big
|
||||
const item = _.omit(
|
||||
cursor.value,
|
||||
['avatar', 'profileAvatar']
|
||||
);
|
||||
const item = _.omit(cursor.value, ['avatar', 'profileAvatar']);
|
||||
|
||||
const jsonString = JSON.stringify(stringify(item));
|
||||
stream.write(jsonString);
|
||||
|
@ -235,10 +231,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
|
|||
groupLookup: {},
|
||||
});
|
||||
|
||||
const {
|
||||
conversationLookup,
|
||||
groupLookup,
|
||||
} = options;
|
||||
const { conversationLookup, groupLookup } = options;
|
||||
const result = {
|
||||
fullImport: true,
|
||||
};
|
||||
|
@ -269,7 +262,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
|
|||
console.log('Importing to these stores:', storeNames.join(', '));
|
||||
|
||||
let finished = false;
|
||||
const finish = (via) => {
|
||||
const finish = via => {
|
||||
console.log('non-messages import done via', via);
|
||||
if (finished) {
|
||||
resolve(result);
|
||||
|
@ -287,7 +280,7 @@ function importFromJsonString(db, jsonString, targetPath, options) {
|
|||
};
|
||||
transaction.oncomplete = finish.bind(null, 'transaction complete');
|
||||
|
||||
_.each(storeNames, (storeName) => {
|
||||
_.each(storeNames, storeName => {
|
||||
console.log('Importing items for store', storeName);
|
||||
|
||||
if (!importObject[storeName].length) {
|
||||
|
@ -316,14 +309,14 @@ function importFromJsonString(db, jsonString, targetPath, options) {
|
|||
}
|
||||
};
|
||||
|
||||
_.each(importObject[storeName], (toAdd) => {
|
||||
_.each(importObject[storeName], toAdd => {
|
||||
toAdd = unstringify(toAdd);
|
||||
|
||||
const haveConversationAlready =
|
||||
storeName === 'conversations' &&
|
||||
conversationLookup[getConversationKey(toAdd)];
|
||||
storeName === 'conversations' &&
|
||||
conversationLookup[getConversationKey(toAdd)];
|
||||
const haveGroupAlready =
|
||||
storeName === 'groups' && groupLookup[getGroupKey(toAdd)];
|
||||
storeName === 'groups' && groupLookup[getGroupKey(toAdd)];
|
||||
|
||||
if (haveConversationAlready || haveGroupAlready) {
|
||||
skipCount += 1;
|
||||
|
@ -365,7 +358,7 @@ function createDirectory(parent, name) {
|
|||
return;
|
||||
}
|
||||
|
||||
fs.mkdir(targetDir, (error) => {
|
||||
fs.mkdir(targetDir, error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
|
@ -377,7 +370,7 @@ function createDirectory(parent, name) {
|
|||
}
|
||||
|
||||
function createFileAndWriter(parent, name) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
const sanitized = _sanitizeFileName(name);
|
||||
const targetPath = path.join(parent, sanitized);
|
||||
const options = {
|
||||
|
@ -430,7 +423,6 @@ function _trimFileName(filename) {
|
|||
return `${name.join('.').slice(0, 24)}.${extension}`;
|
||||
}
|
||||
|
||||
|
||||
function _getExportAttachmentFileName(message, index, attachment) {
|
||||
if (attachment.fileName) {
|
||||
return _trimFileName(attachment.fileName);
|
||||
|
@ -440,7 +432,9 @@ function _getExportAttachmentFileName(message, index, attachment) {
|
|||
|
||||
if (attachment.contentType) {
|
||||
const components = attachment.contentType.split('/');
|
||||
name += `.${components.length > 1 ? components[1] : attachment.contentType}`;
|
||||
name += `.${
|
||||
components.length > 1 ? components[1] : attachment.contentType
|
||||
}`;
|
||||
}
|
||||
|
||||
return name;
|
||||
|
@ -477,14 +471,11 @@ async function readAttachment(dir, attachment, name, options) {
|
|||
}
|
||||
|
||||
async function writeThumbnail(attachment, options) {
|
||||
const {
|
||||
dir,
|
||||
const { dir, message, index, key, newKey } = options;
|
||||
const filename = `${_getAnonymousAttachmentFileName(
|
||||
message,
|
||||
index,
|
||||
key,
|
||||
newKey,
|
||||
} = options;
|
||||
const filename = `${_getAnonymousAttachmentFileName(message, index)}-thumbnail`;
|
||||
index
|
||||
)}-thumbnail`;
|
||||
const target = path.join(dir, filename);
|
||||
const { thumbnail } = attachment;
|
||||
|
||||
|
@ -504,26 +495,28 @@ async function writeThumbnails(rawQuotedAttachments, options) {
|
|||
const { name } = options;
|
||||
|
||||
const { loadAttachmentData } = Signal.Migrations;
|
||||
const promises = rawQuotedAttachments.map(async (attachment) => {
|
||||
const promises = rawQuotedAttachments.map(async attachment => {
|
||||
if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
attachment,
|
||||
{ thumbnail: await loadAttachmentData(attachment.thumbnail) }
|
||||
);
|
||||
return Object.assign({}, attachment, {
|
||||
thumbnail: await loadAttachmentData(attachment.thumbnail),
|
||||
});
|
||||
});
|
||||
|
||||
const attachments = await Promise.all(promises);
|
||||
try {
|
||||
await Promise.all(_.map(
|
||||
attachments,
|
||||
(attachment, index) => writeThumbnail(attachment, Object.assign({}, options, {
|
||||
index,
|
||||
}))
|
||||
));
|
||||
await Promise.all(
|
||||
_.map(attachments, (attachment, index) =>
|
||||
writeThumbnail(
|
||||
attachment,
|
||||
Object.assign({}, options, {
|
||||
index,
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'writeThumbnails: error exporting conversation',
|
||||
|
@ -536,13 +529,7 @@ async function writeThumbnails(rawQuotedAttachments, options) {
|
|||
}
|
||||
|
||||
async function writeAttachment(attachment, options) {
|
||||
const {
|
||||
dir,
|
||||
message,
|
||||
index,
|
||||
key,
|
||||
newKey,
|
||||
} = options;
|
||||
const { dir, message, index, key, newKey } = options;
|
||||
const filename = _getAnonymousAttachmentFileName(message, index);
|
||||
const target = path.join(dir, filename);
|
||||
if (!Attachment.hasData(attachment)) {
|
||||
|
@ -562,11 +549,13 @@ async function writeAttachments(rawAttachments, options) {
|
|||
|
||||
const { loadAttachmentData } = Signal.Migrations;
|
||||
const attachments = await Promise.all(rawAttachments.map(loadAttachmentData));
|
||||
const promises = _.map(
|
||||
attachments,
|
||||
(attachment, index) => writeAttachment(attachment, Object.assign({}, options, {
|
||||
index,
|
||||
}))
|
||||
const promises = _.map(attachments, (attachment, index) =>
|
||||
writeAttachment(
|
||||
attachment,
|
||||
Object.assign({}, options, {
|
||||
index,
|
||||
})
|
||||
)
|
||||
);
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
|
@ -582,12 +571,7 @@ async function writeAttachments(rawAttachments, options) {
|
|||
}
|
||||
|
||||
async function writeEncryptedAttachment(target, data, options = {}) {
|
||||
const {
|
||||
key,
|
||||
newKey,
|
||||
filename,
|
||||
dir,
|
||||
} = options;
|
||||
const { key, newKey, filename, dir } = options;
|
||||
|
||||
if (fs.existsSync(target)) {
|
||||
if (newKey) {
|
||||
|
@ -613,13 +597,7 @@ function _sanitizeFileName(filename) {
|
|||
|
||||
async function exportConversation(db, conversation, options) {
|
||||
options = options || {};
|
||||
const {
|
||||
name,
|
||||
dir,
|
||||
attachmentsDir,
|
||||
key,
|
||||
newKey,
|
||||
} = options;
|
||||
const { name, dir, attachmentsDir, key, newKey } = options;
|
||||
if (!name) {
|
||||
throw new Error('Need a name!');
|
||||
}
|
||||
|
@ -670,7 +648,7 @@ async function exportConversation(db, conversation, options) {
|
|||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async (event) => {
|
||||
request.onsuccess = async event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
const message = cursor.value;
|
||||
|
@ -688,13 +666,12 @@ async function exportConversation(db, conversation, options) {
|
|||
|
||||
// eliminate attachment data from the JSON, since it will go to disk
|
||||
// Note: this is for legacy messages only, which stored attachment data in the db
|
||||
message.attachments = _.map(
|
||||
attachments,
|
||||
attachment => _.omit(attachment, ['data'])
|
||||
message.attachments = _.map(attachments, attachment =>
|
||||
_.omit(attachment, ['data'])
|
||||
);
|
||||
// completely drop any attachments in messages cached in error objects
|
||||
// TODO: move to lodash. Sadly, a number of the method signatures have changed!
|
||||
message.errors = _.map(message.errors, (error) => {
|
||||
message.errors = _.map(message.errors, error => {
|
||||
if (error && error.args) {
|
||||
error.args = [];
|
||||
}
|
||||
|
@ -709,13 +686,14 @@ async function exportConversation(db, conversation, options) {
|
|||
|
||||
console.log({ backupMessage: message });
|
||||
if (attachments && attachments.length > 0) {
|
||||
const exportAttachments = () => writeAttachments(attachments, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
const exportAttachments = () =>
|
||||
writeAttachments(attachments, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
promiseChain = promiseChain.then(exportAttachments);
|
||||
|
@ -723,13 +701,14 @@ async function exportConversation(db, conversation, options) {
|
|||
|
||||
const quoteThumbnails = message.quote && message.quote.attachments;
|
||||
if (quoteThumbnails && quoteThumbnails.length > 0) {
|
||||
const exportQuoteThumbnails = () => writeThumbnails(quoteThumbnails, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
const exportQuoteThumbnails = () =>
|
||||
writeThumbnails(quoteThumbnails, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
promiseChain = promiseChain.then(exportQuoteThumbnails);
|
||||
|
@ -739,11 +718,7 @@ async function exportConversation(db, conversation, options) {
|
|||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
await Promise.all([
|
||||
stream.write(']}'),
|
||||
promiseChain,
|
||||
stream.close(),
|
||||
]);
|
||||
await Promise.all([stream.write(']}'), promiseChain, stream.close()]);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'exportConversation: error exporting conversation',
|
||||
|
@ -791,12 +766,7 @@ function _getConversationLoggingName(conversation) {
|
|||
|
||||
function exportConversations(db, options) {
|
||||
options = options || {};
|
||||
const {
|
||||
messagesDir,
|
||||
attachmentsDir,
|
||||
key,
|
||||
newKey,
|
||||
} = options;
|
||||
const { messagesDir, attachmentsDir, key, newKey } = options;
|
||||
|
||||
if (!messagesDir) {
|
||||
return Promise.reject(new Error('Need a messages directory!'));
|
||||
|
@ -828,7 +798,7 @@ function exportConversations(db, options) {
|
|||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async (event) => {
|
||||
request.onsuccess = async event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor && cursor.value) {
|
||||
const conversation = cursor.value;
|
||||
|
@ -873,7 +843,7 @@ function getDirectory(options) {
|
|||
buttonLabel: options.buttonLabel,
|
||||
};
|
||||
|
||||
dialog.showOpenDialog(browserWindow, dialogOptions, (directory) => {
|
||||
dialog.showOpenDialog(browserWindow, dialogOptions, directory => {
|
||||
if (!directory || !directory[0]) {
|
||||
const error = new Error('Error choosing directory');
|
||||
error.name = 'ChooseError';
|
||||
|
@ -940,7 +910,7 @@ async function saveAllMessages(db, rawMessages) {
|
|||
|
||||
return new Promise((resolve, reject) => {
|
||||
let finished = false;
|
||||
const finish = (via) => {
|
||||
const finish = via => {
|
||||
console.log('messages done saving via', via);
|
||||
if (finished) {
|
||||
resolve();
|
||||
|
@ -962,7 +932,7 @@ async function saveAllMessages(db, rawMessages) {
|
|||
const { conversationId } = messages[0];
|
||||
let count = 0;
|
||||
|
||||
_.forEach(messages, (message) => {
|
||||
_.forEach(messages, message => {
|
||||
const request = store.put(message, message.id);
|
||||
request.onsuccess = () => {
|
||||
count += 1;
|
||||
|
@ -997,11 +967,7 @@ async function importConversation(db, dir, options) {
|
|||
options = options || {};
|
||||
_.defaults(options, { messageLookup: {} });
|
||||
|
||||
const {
|
||||
messageLookup,
|
||||
attachmentsDir,
|
||||
key,
|
||||
} = options;
|
||||
const { messageLookup, attachmentsDir, key } = options;
|
||||
|
||||
let conversationId = 'unknown';
|
||||
let total = 0;
|
||||
|
@ -1018,11 +984,13 @@ async function importConversation(db, dir, options) {
|
|||
|
||||
const json = JSON.parse(contents);
|
||||
if (json.messages && json.messages.length) {
|
||||
conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice(-3)}`;
|
||||
conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice(
|
||||
-3
|
||||
)}`;
|
||||
}
|
||||
total = json.messages.length;
|
||||
|
||||
const messages = _.filter(json.messages, (message) => {
|
||||
const messages = _.filter(json.messages, message => {
|
||||
message = unstringify(message);
|
||||
|
||||
if (messageLookup[getMessageKey(message)]) {
|
||||
|
@ -1031,7 +999,9 @@ async function importConversation(db, dir, options) {
|
|||
}
|
||||
|
||||
const hasAttachments = message.attachments && message.attachments.length;
|
||||
const hasQuotedAttachments = message.quote && message.quote.attachments &&
|
||||
const hasQuotedAttachments =
|
||||
message.quote &&
|
||||
message.quote.attachments &&
|
||||
message.quote.attachments.length > 0;
|
||||
|
||||
if (hasAttachments || hasQuotedAttachments) {
|
||||
|
@ -1039,8 +1009,8 @@ async function importConversation(db, dir, options) {
|
|||
const getName = attachmentsDir
|
||||
? _getAnonymousAttachmentFileName
|
||||
: _getExportAttachmentFileName;
|
||||
const parentDir = attachmentsDir ||
|
||||
path.join(dir, message.received_at.toString());
|
||||
const parentDir =
|
||||
attachmentsDir || path.join(dir, message.received_at.toString());
|
||||
|
||||
await loadAttachments(parentDir, getName, {
|
||||
message,
|
||||
|
@ -1075,12 +1045,13 @@ async function importConversations(db, dir, options) {
|
|||
const contents = await getDirContents(dir);
|
||||
let promiseChain = Promise.resolve();
|
||||
|
||||
_.forEach(contents, (conversationDir) => {
|
||||
_.forEach(contents, conversationDir => {
|
||||
if (!fs.statSync(conversationDir).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadConversation = () => importConversation(db, conversationDir, options);
|
||||
const loadConversation = () =>
|
||||
importConversation(db, conversationDir, options);
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
promiseChain = promiseChain.then(loadConversation);
|
||||
|
@ -1142,7 +1113,7 @@ function assembleLookup(db, storeName, keyFunction) {
|
|||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = (event) => {
|
||||
request.onsuccess = event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor && cursor.value) {
|
||||
lookup[keyFunction(cursor.value)] = true;
|
||||
|
@ -1175,7 +1146,7 @@ function createZip(zipDir, targetDir) {
|
|||
resolve(target);
|
||||
});
|
||||
|
||||
archive.on('warning', (error) => {
|
||||
archive.on('warning', error => {
|
||||
console.log(`Archive generation warning: ${error.stack}`);
|
||||
});
|
||||
archive.on('error', reject);
|
||||
|
@ -1247,10 +1218,13 @@ async function exportToDirectory(directory, options) {
|
|||
const attachmentsDir = await createDirectory(directory, 'attachments');
|
||||
|
||||
await exportContactAndGroupsToFile(db, stagingDir);
|
||||
await exportConversations(db, Object.assign({}, options, {
|
||||
messagesDir: stagingDir,
|
||||
attachmentsDir,
|
||||
}));
|
||||
await exportConversations(
|
||||
db,
|
||||
Object.assign({}, options, {
|
||||
messagesDir: stagingDir,
|
||||
attachmentsDir,
|
||||
})
|
||||
);
|
||||
|
||||
const zip = await createZip(encryptionDir, stagingDir);
|
||||
await encryptFile(zip, path.join(directory, 'messages.zip'), options);
|
||||
|
@ -1302,7 +1276,9 @@ async function importFromDirectory(directory, options) {
|
|||
if (fs.existsSync(zipPath)) {
|
||||
// we're in the world of an encrypted, zipped backup
|
||||
if (!options.key) {
|
||||
throw new Error('Importing an encrypted backup; decryption key is required!');
|
||||
throw new Error(
|
||||
'Importing an encrypted backup; decryption key is required!'
|
||||
);
|
||||
}
|
||||
|
||||
let stagingDir;
|
||||
|
|
|
@ -19,8 +19,15 @@ async function encryptSymmetric(key, plaintext) {
|
|||
const cipherKey = await _hmac_SHA256(key, nonce);
|
||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
||||
|
||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(cipherKey, iv, plaintext);
|
||||
const mac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH);
|
||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(
|
||||
cipherKey,
|
||||
iv,
|
||||
plaintext
|
||||
);
|
||||
const mac = _getFirstBytes(
|
||||
await _hmac_SHA256(macKey, cipherText),
|
||||
MAC_LENGTH
|
||||
);
|
||||
|
||||
return _concatData([nonce, cipherText, mac]);
|
||||
}
|
||||
|
@ -39,9 +46,14 @@ async function decryptSymmetric(key, data) {
|
|||
const cipherKey = await _hmac_SHA256(key, nonce);
|
||||
const macKey = await _hmac_SHA256(key, cipherKey);
|
||||
|
||||
const ourMac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH);
|
||||
const ourMac = _getFirstBytes(
|
||||
await _hmac_SHA256(macKey, cipherText),
|
||||
MAC_LENGTH
|
||||
);
|
||||
if (!constantTimeEqual(theirMac, ourMac)) {
|
||||
throw new Error('decryptSymmetric: Failed to decrypt; MAC verification failed');
|
||||
throw new Error(
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
}
|
||||
|
||||
return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText);
|
||||
|
@ -61,7 +73,6 @@ function constantTimeEqual(left, right) {
|
|||
return result === 0;
|
||||
}
|
||||
|
||||
|
||||
async function _hmac_SHA256(key, data) {
|
||||
const extractable = false;
|
||||
const cryptoKey = await window.crypto.subtle.importKey(
|
||||
|
@ -72,7 +83,11 @@ async function _hmac_SHA256(key, data) {
|
|||
['sign']
|
||||
);
|
||||
|
||||
return window.crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, cryptoKey, data);
|
||||
return window.crypto.subtle.sign(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
cryptoKey,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
async function _encrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
||||
|
@ -101,7 +116,6 @@ async function _decrypt_aes256_CBC_PKCSPadding(key, iv, data) {
|
|||
return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data);
|
||||
}
|
||||
|
||||
|
||||
function _getRandomBytes(n) {
|
||||
const bytes = new Uint8Array(n);
|
||||
window.crypto.getRandomValues(bytes);
|
||||
|
|
|
@ -6,14 +6,12 @@
|
|||
|
||||
const { isObject, isNumber } = require('lodash');
|
||||
|
||||
|
||||
exports.open = (name, version, { onUpgradeNeeded } = {}) => {
|
||||
const request = indexedDB.open(name, version);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onblocked = () =>
|
||||
reject(new Error('Database blocked'));
|
||||
request.onblocked = () => reject(new Error('Database blocked'));
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
request.onupgradeneeded = event => {
|
||||
const hasRequestedSpecificVersion = isNumber(version);
|
||||
if (!hasRequestedSpecificVersion) {
|
||||
return;
|
||||
|
@ -26,14 +24,17 @@ exports.open = (name, version, { onUpgradeNeeded } = {}) => {
|
|||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Database upgrade required:' +
|
||||
` oldVersion: ${oldVersion}, newVersion: ${newVersion}`));
|
||||
reject(
|
||||
new Error(
|
||||
'Database upgrade required:' +
|
||||
` oldVersion: ${oldVersion}, newVersion: ${newVersion}`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
request.onsuccess = event => {
|
||||
const connection = event.target.result;
|
||||
resolve(connection);
|
||||
};
|
||||
|
@ -47,7 +48,7 @@ exports.completeTransaction = transaction =>
|
|||
transaction.addEventListener('complete', () => resolve());
|
||||
});
|
||||
|
||||
exports.getVersion = async (name) => {
|
||||
exports.getVersion = async name => {
|
||||
const connection = await exports.open(name);
|
||||
const { version } = connection;
|
||||
connection.close();
|
||||
|
@ -61,9 +62,7 @@ exports.getCount = async ({ store } = {}) => {
|
|||
|
||||
const request = store.count();
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onsuccess = event =>
|
||||
resolve(event.target.result);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
request.onsuccess = event => resolve(event.target.result);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -18,7 +18,6 @@ const Message = require('./types/message');
|
|||
const { deferredToPromise } = require('./deferred_to_promise');
|
||||
const { sleep } = require('./sleep');
|
||||
|
||||
|
||||
// See: https://en.wikipedia.org/wiki/Fictitious_telephone_number#North_American_Numbering_Plan
|
||||
const SENDER_ID = '+12126647665';
|
||||
|
||||
|
@ -27,8 +26,10 @@ exports.createConversation = async ({
|
|||
numMessages,
|
||||
WhisperMessage,
|
||||
} = {}) => {
|
||||
if (!isObject(ConversationController) ||
|
||||
!isFunction(ConversationController.getOrCreateAndWait)) {
|
||||
if (
|
||||
!isObject(ConversationController) ||
|
||||
!isFunction(ConversationController.getOrCreateAndWait)
|
||||
) {
|
||||
throw new TypeError("'ConversationController' is required");
|
||||
}
|
||||
|
||||
|
@ -40,8 +41,10 @@ exports.createConversation = async ({
|
|||
throw new TypeError("'WhisperMessage' is required");
|
||||
}
|
||||
|
||||
const conversation =
|
||||
await ConversationController.getOrCreateAndWait(SENDER_ID, 'private');
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
SENDER_ID,
|
||||
'private'
|
||||
);
|
||||
conversation.set({
|
||||
active_at: Date.now(),
|
||||
unread: numMessages,
|
||||
|
@ -50,13 +53,15 @@ exports.createConversation = async ({
|
|||
|
||||
const conversationId = conversation.get('id');
|
||||
|
||||
await Promise.all(range(0, numMessages).map(async (index) => {
|
||||
await sleep(index * 100);
|
||||
console.log(`Create message ${index + 1}`);
|
||||
const messageAttributes = await createRandomMessage({ conversationId });
|
||||
const message = new WhisperMessage(messageAttributes);
|
||||
return deferredToPromise(message.save());
|
||||
}));
|
||||
await Promise.all(
|
||||
range(0, numMessages).map(async index => {
|
||||
await sleep(index * 100);
|
||||
console.log(`Create message ${index + 1}`);
|
||||
const messageAttributes = await createRandomMessage({ conversationId });
|
||||
const message = new WhisperMessage(messageAttributes);
|
||||
return deferredToPromise(message.save());
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const SAMPLE_MESSAGES = [
|
||||
|
@ -88,7 +93,8 @@ const createRandomMessage = async ({ conversationId } = {}) => {
|
|||
|
||||
const hasAttachment = Math.random() <= ATTACHMENT_SAMPLE_RATE;
|
||||
const attachments = hasAttachment
|
||||
? [await createRandomInMemoryAttachment()] : [];
|
||||
? [await createRandomInMemoryAttachment()]
|
||||
: [];
|
||||
const type = sample(['incoming', 'outgoing']);
|
||||
const commonProperties = {
|
||||
attachments,
|
||||
|
@ -145,7 +151,7 @@ const createFileEntry = fileName => ({
|
|||
fileName,
|
||||
contentType: fileNameToContentType(fileName),
|
||||
});
|
||||
const fileNameToContentType = (fileName) => {
|
||||
const fileNameToContentType = fileName => {
|
||||
const fileExtension = path.extname(fileName).toLowerCase();
|
||||
switch (fileExtension) {
|
||||
case '.gif':
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
const FormData = require('form-data');
|
||||
const got = require('got');
|
||||
|
||||
|
||||
const BASE_URL = 'https://debuglogs.org';
|
||||
|
||||
// Workaround: Submitting `FormData` using native `FormData::submit` procedure
|
||||
|
@ -12,7 +11,7 @@ const BASE_URL = 'https://debuglogs.org';
|
|||
// https://github.com/sindresorhus/got/pull/466
|
||||
const submitFormData = (form, url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
form.submit(url, (error) => {
|
||||
form.submit(url, error => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
@ -22,7 +21,7 @@ const submitFormData = (form, url) =>
|
|||
});
|
||||
|
||||
// upload :: String -> Promise URL
|
||||
exports.upload = async (content) => {
|
||||
exports.upload = async content => {
|
||||
const signedForm = await got.get(BASE_URL, { json: true });
|
||||
const { fields, url } = signedForm.body;
|
||||
|
||||
|
|
|
@ -2,11 +2,10 @@ const addUnhandledErrorHandler = require('electron-unhandled');
|
|||
|
||||
const Errors = require('./types/errors');
|
||||
|
||||
|
||||
// addHandler :: Unit -> Unit
|
||||
exports.addHandler = () => {
|
||||
addUnhandledErrorHandler({
|
||||
logger: (error) => {
|
||||
logger: error => {
|
||||
console.error(
|
||||
'Uncaught error or unhandled promise rejection:',
|
||||
Errors.toLogFormat(error)
|
||||
|
|
|
@ -11,7 +11,9 @@ exports.setup = (locale, messages) => {
|
|||
function getMessage(key, substitutions) {
|
||||
const entry = messages[key];
|
||||
if (!entry) {
|
||||
console.error(`i18n: Attempted to get translation for nonexistent key '${key}'`);
|
||||
console.error(
|
||||
`i18n: Attempted to get translation for nonexistent key '${key}'`
|
||||
);
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
|
||||
const POLL_INTERVAL_MS = 5 * 1000;
|
||||
const IDLE_THRESHOLD_MS = 20;
|
||||
|
||||
|
@ -35,14 +34,17 @@ class IdleDetector extends EventEmitter {
|
|||
|
||||
_scheduleNextCallback() {
|
||||
this._clearScheduledCallbacks();
|
||||
this.handle = window.requestIdleCallback((deadline) => {
|
||||
this.handle = window.requestIdleCallback(deadline => {
|
||||
const { didTimeout } = deadline;
|
||||
const timeRemaining = deadline.timeRemaining();
|
||||
const isIdle = timeRemaining >= IDLE_THRESHOLD_MS;
|
||||
if (isIdle || didTimeout) {
|
||||
this.emit('idle', { timestamp: Date.now(), didTimeout, timeRemaining });
|
||||
}
|
||||
this.timeoutId = setTimeout(() => this._scheduleNextCallback(), POLL_INTERVAL_MS);
|
||||
this.timeoutId = setTimeout(
|
||||
() => this._scheduleNextCallback(),
|
||||
POLL_INTERVAL_MS
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ function createLink(url, text, attrs = {}) {
|
|||
const html = [];
|
||||
html.push('<a ');
|
||||
html.push(`href="${url}"`);
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
Object.keys(attrs).forEach(key => {
|
||||
html.push(` ${key}="${attrs[key]}"`);
|
||||
});
|
||||
html.push('>');
|
||||
|
@ -23,7 +23,7 @@ module.exports = (text, attrs = {}) => {
|
|||
const result = [];
|
||||
let last = 0;
|
||||
|
||||
matchData.forEach((match) => {
|
||||
matchData.forEach(match => {
|
||||
if (last < match.index) {
|
||||
result.push(text.slice(last, match.index));
|
||||
}
|
||||
|
|
|
@ -6,20 +6,13 @@
|
|||
|
||||
/* global IDBKeyRange */
|
||||
|
||||
const {
|
||||
isFunction,
|
||||
isNumber,
|
||||
isObject,
|
||||
isString,
|
||||
last,
|
||||
} = require('lodash');
|
||||
const { isFunction, isNumber, isObject, isString, last } = require('lodash');
|
||||
|
||||
const database = require('./database');
|
||||
const Message = require('./types/message');
|
||||
const settings = require('./settings');
|
||||
const { deferredToPromise } = require('./deferred_to_promise');
|
||||
|
||||
|
||||
const MESSAGES_STORE_NAME = 'messages';
|
||||
|
||||
exports.processNext = async ({
|
||||
|
@ -29,12 +22,16 @@ exports.processNext = async ({
|
|||
upgradeMessageSchema,
|
||||
} = {}) => {
|
||||
if (!isFunction(BackboneMessage)) {
|
||||
throw new TypeError("'BackboneMessage' (Whisper.Message) constructor is required");
|
||||
throw new TypeError(
|
||||
"'BackboneMessage' (Whisper.Message) constructor is required"
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFunction(BackboneMessageCollection)) {
|
||||
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required');
|
||||
throw new TypeError(
|
||||
"'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required'
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNumber(numMessagesPerBatch)) {
|
||||
|
@ -48,16 +45,18 @@ exports.processNext = async ({
|
|||
const startTime = Date.now();
|
||||
|
||||
const fetchStartTime = Date.now();
|
||||
const messagesRequiringSchemaUpgrade =
|
||||
await _fetchMessagesRequiringSchemaUpgrade({
|
||||
const messagesRequiringSchemaUpgrade = await _fetchMessagesRequiringSchemaUpgrade(
|
||||
{
|
||||
BackboneMessageCollection,
|
||||
count: numMessagesPerBatch,
|
||||
});
|
||||
}
|
||||
);
|
||||
const fetchDuration = Date.now() - fetchStartTime;
|
||||
|
||||
const upgradeStartTime = Date.now();
|
||||
const upgradedMessages =
|
||||
await Promise.all(messagesRequiringSchemaUpgrade.map(upgradeMessageSchema));
|
||||
const upgradedMessages = await Promise.all(
|
||||
messagesRequiringSchemaUpgrade.map(upgradeMessageSchema)
|
||||
);
|
||||
const upgradeDuration = Date.now() - upgradeStartTime;
|
||||
|
||||
const saveStartTime = Date.now();
|
||||
|
@ -109,8 +108,10 @@ exports.dangerouslyProcessAllWithoutIndex = async ({
|
|||
minDatabaseVersion,
|
||||
});
|
||||
if (!isValidDatabaseVersion) {
|
||||
throw new Error(`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`);
|
||||
throw new Error(
|
||||
`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: Even if we make this async using `then`, requesting `count` on an
|
||||
|
@ -132,10 +133,13 @@ exports.dangerouslyProcessAllWithoutIndex = async ({
|
|||
break;
|
||||
}
|
||||
numCumulativeMessagesProcessed += status.numMessagesProcessed;
|
||||
console.log('Upgrade message schema:', Object.assign({}, status, {
|
||||
numTotalMessages,
|
||||
numCumulativeMessagesProcessed,
|
||||
}));
|
||||
console.log(
|
||||
'Upgrade message schema:',
|
||||
Object.assign({}, status, {
|
||||
numTotalMessages,
|
||||
numCumulativeMessagesProcessed,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Close database connection');
|
||||
|
@ -181,8 +185,10 @@ const _getConnection = async ({ databaseName, minDatabaseVersion }) => {
|
|||
const databaseVersion = connection.version;
|
||||
const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion;
|
||||
if (!isValidDatabaseVersion) {
|
||||
throw new Error(`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`);
|
||||
throw new Error(
|
||||
`Expected database version (${databaseVersion})` +
|
||||
` to be at least ${minDatabaseVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
return connection;
|
||||
|
@ -205,29 +211,33 @@ const _processBatch = async ({
|
|||
throw new TypeError("'numMessagesPerBatch' is required");
|
||||
}
|
||||
|
||||
const isAttachmentMigrationComplete =
|
||||
await settings.isAttachmentMigrationComplete(connection);
|
||||
const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete(
|
||||
connection
|
||||
);
|
||||
if (isAttachmentMigrationComplete) {
|
||||
return {
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
|
||||
const lastProcessedIndex =
|
||||
await settings.getAttachmentMigrationLastProcessedIndex(connection);
|
||||
const lastProcessedIndex = await settings.getAttachmentMigrationLastProcessedIndex(
|
||||
connection
|
||||
);
|
||||
|
||||
const fetchUnprocessedMessagesStartTime = Date.now();
|
||||
const unprocessedMessages =
|
||||
await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({
|
||||
const unprocessedMessages = await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex(
|
||||
{
|
||||
connection,
|
||||
count: numMessagesPerBatch,
|
||||
lastIndex: lastProcessedIndex,
|
||||
});
|
||||
}
|
||||
);
|
||||
const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime;
|
||||
|
||||
const upgradeStartTime = Date.now();
|
||||
const upgradedMessages =
|
||||
await Promise.all(unprocessedMessages.map(upgradeMessageSchema));
|
||||
const upgradedMessages = await Promise.all(
|
||||
unprocessedMessages.map(upgradeMessageSchema)
|
||||
);
|
||||
const upgradeDuration = Date.now() - upgradeStartTime;
|
||||
|
||||
const saveMessagesStartTime = Date.now();
|
||||
|
@ -266,12 +276,12 @@ const _processBatch = async ({
|
|||
};
|
||||
};
|
||||
|
||||
const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => {
|
||||
const _saveMessageBackbone = ({ BackboneMessage } = {}) => message => {
|
||||
const backboneMessage = new BackboneMessage(message);
|
||||
return deferredToPromise(backboneMessage.save());
|
||||
};
|
||||
|
||||
const _saveMessage = ({ transaction } = {}) => (message) => {
|
||||
const _saveMessage = ({ transaction } = {}) => message => {
|
||||
if (!isObject(transaction)) {
|
||||
throw new TypeError("'transaction' is required");
|
||||
}
|
||||
|
@ -279,83 +289,91 @@ const _saveMessage = ({ transaction } = {}) => (message) => {
|
|||
const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
|
||||
const request = messagesStore.put(message, message.id);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () =>
|
||||
resolve();
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = event => reject(event.target.error);
|
||||
});
|
||||
};
|
||||
|
||||
const _fetchMessagesRequiringSchemaUpgrade =
|
||||
async ({ BackboneMessageCollection, count } = {}) => {
|
||||
if (!isFunction(BackboneMessageCollection)) {
|
||||
throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required');
|
||||
}
|
||||
const _fetchMessagesRequiringSchemaUpgrade = async ({
|
||||
BackboneMessageCollection,
|
||||
count,
|
||||
} = {}) => {
|
||||
if (!isFunction(BackboneMessageCollection)) {
|
||||
throw new TypeError(
|
||||
"'BackboneMessageCollection' (Whisper.MessageCollection)" +
|
||||
' constructor is required'
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNumber(count)) {
|
||||
throw new TypeError("'count' is required");
|
||||
}
|
||||
if (!isNumber(count)) {
|
||||
throw new TypeError("'count' is required");
|
||||
}
|
||||
|
||||
const collection = new BackboneMessageCollection();
|
||||
return new Promise(resolve => collection.fetch({
|
||||
limit: count,
|
||||
index: {
|
||||
name: 'schemaVersion',
|
||||
upper: Message.CURRENT_SCHEMA_VERSION,
|
||||
excludeUpper: true,
|
||||
order: 'desc',
|
||||
},
|
||||
}).always(() => {
|
||||
const models = collection.models || [];
|
||||
const messages = models.map(model => model.toJSON());
|
||||
resolve(messages);
|
||||
}));
|
||||
};
|
||||
const collection = new BackboneMessageCollection();
|
||||
return new Promise(resolve =>
|
||||
collection
|
||||
.fetch({
|
||||
limit: count,
|
||||
index: {
|
||||
name: 'schemaVersion',
|
||||
upper: Message.CURRENT_SCHEMA_VERSION,
|
||||
excludeUpper: true,
|
||||
order: 'desc',
|
||||
},
|
||||
})
|
||||
.always(() => {
|
||||
const models = collection.models || [];
|
||||
const messages = models.map(model => model.toJSON());
|
||||
resolve(messages);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// NOTE: Named ‘dangerous’ because it is not as efficient as using our
|
||||
// `messages` `schemaVersion` index:
|
||||
const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex =
|
||||
({ connection, count, lastIndex } = {}) => {
|
||||
if (!isObject(connection)) {
|
||||
throw new TypeError("'connection' is required");
|
||||
}
|
||||
const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = ({
|
||||
connection,
|
||||
count,
|
||||
lastIndex,
|
||||
} = {}) => {
|
||||
if (!isObject(connection)) {
|
||||
throw new TypeError("'connection' is required");
|
||||
}
|
||||
|
||||
if (!isNumber(count)) {
|
||||
throw new TypeError("'count' is required");
|
||||
}
|
||||
if (!isNumber(count)) {
|
||||
throw new TypeError("'count' is required");
|
||||
}
|
||||
|
||||
if (lastIndex && !isString(lastIndex)) {
|
||||
throw new TypeError("'lastIndex' must be a string");
|
||||
}
|
||||
if (lastIndex && !isString(lastIndex)) {
|
||||
throw new TypeError("'lastIndex' must be a string");
|
||||
}
|
||||
|
||||
const hasLastIndex = Boolean(lastIndex);
|
||||
const hasLastIndex = Boolean(lastIndex);
|
||||
|
||||
const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly');
|
||||
const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
|
||||
const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly');
|
||||
const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME);
|
||||
|
||||
const excludeLowerBound = true;
|
||||
const range = hasLastIndex
|
||||
? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound)
|
||||
: undefined;
|
||||
return new Promise((resolve, reject) => {
|
||||
const items = [];
|
||||
const request = messagesStore.openCursor(range);
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
const hasMoreData = Boolean(cursor);
|
||||
if (!hasMoreData || items.length === count) {
|
||||
resolve(items);
|
||||
return;
|
||||
}
|
||||
const item = cursor.value;
|
||||
items.push(item);
|
||||
cursor.continue();
|
||||
};
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
});
|
||||
};
|
||||
const excludeLowerBound = true;
|
||||
const range = hasLastIndex
|
||||
? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound)
|
||||
: undefined;
|
||||
return new Promise((resolve, reject) => {
|
||||
const items = [];
|
||||
const request = messagesStore.openCursor(range);
|
||||
request.onsuccess = event => {
|
||||
const cursor = event.target.result;
|
||||
const hasMoreData = Boolean(cursor);
|
||||
if (!hasMoreData || items.length === count) {
|
||||
resolve(items);
|
||||
return;
|
||||
}
|
||||
const item = cursor.value;
|
||||
items.push(item);
|
||||
cursor.continue();
|
||||
};
|
||||
request.onerror = event => reject(event.target.error);
|
||||
});
|
||||
};
|
||||
|
||||
const _getNumMessages = async ({ connection } = {}) => {
|
||||
if (!isObject(connection)) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
exports.run = (transaction) => {
|
||||
exports.run = transaction => {
|
||||
const messagesStore = transaction.objectStore('messages');
|
||||
|
||||
console.log("Create message attachment metadata index: 'hasAttachments'");
|
||||
|
@ -8,12 +8,10 @@ exports.run = (transaction) => {
|
|||
{ unique: false }
|
||||
);
|
||||
|
||||
['hasVisualMediaAttachments', 'hasFileAttachments'].forEach((name) => {
|
||||
['hasVisualMediaAttachments', 'hasFileAttachments'].forEach(name => {
|
||||
console.log(`Create message attachment metadata index: '${name}'`);
|
||||
messagesStore.createIndex(
|
||||
name,
|
||||
['conversationId', 'received_at', name],
|
||||
{ unique: false }
|
||||
);
|
||||
messagesStore.createIndex(name, ['conversationId', 'received_at', name], {
|
||||
unique: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
const Migrations0DatabaseWithAttachmentData =
|
||||
require('./migrations_0_database_with_attachment_data');
|
||||
const Migrations1DatabaseWithoutAttachmentData =
|
||||
require('./migrations_1_database_without_attachment_data');
|
||||
|
||||
const Migrations0DatabaseWithAttachmentData = require('./migrations_0_database_with_attachment_data');
|
||||
const Migrations1DatabaseWithoutAttachmentData = require('./migrations_1_database_without_attachment_data');
|
||||
|
||||
exports.getPlaceholderMigrations = () => {
|
||||
const last0MigrationVersion =
|
||||
Migrations0DatabaseWithAttachmentData.getLatestVersion();
|
||||
const last1MigrationVersion =
|
||||
Migrations1DatabaseWithoutAttachmentData.getLatestVersion();
|
||||
const last0MigrationVersion = Migrations0DatabaseWithAttachmentData.getLatestVersion();
|
||||
const last1MigrationVersion = Migrations1DatabaseWithoutAttachmentData.getLatestVersion();
|
||||
|
||||
const lastMigrationVersion = last1MigrationVersion || last0MigrationVersion;
|
||||
|
||||
return [{
|
||||
version: lastMigrationVersion,
|
||||
migrate() {
|
||||
throw new Error('Unexpected invocation of placeholder migration!' +
|
||||
'\n\nMigrations must explicitly be run upon application startup instead' +
|
||||
' of implicitly via Backbone IndexedDB adapter at any time.');
|
||||
return [
|
||||
{
|
||||
version: lastMigrationVersion,
|
||||
migrate() {
|
||||
throw new Error(
|
||||
'Unexpected invocation of placeholder migration!' +
|
||||
'\n\nMigrations must explicitly be run upon application startup instead' +
|
||||
' of implicitly via Backbone IndexedDB adapter at any time.'
|
||||
);
|
||||
},
|
||||
},
|
||||
}];
|
||||
];
|
||||
};
|
||||
|
|
|
@ -3,7 +3,6 @@ const { isString, last } = require('lodash');
|
|||
const { runMigrations } = require('./run_migrations');
|
||||
const Migration18 = require('./18');
|
||||
|
||||
|
||||
// IMPORTANT: The migrations below are run on a database that may be very large
|
||||
// due to attachments being directly stored inside the database. Please avoid
|
||||
// any expensive operations, e.g. modifying all messages / attachments, etc., as
|
||||
|
@ -20,7 +19,9 @@ const migrations = [
|
|||
unique: false,
|
||||
});
|
||||
messages.createIndex('receipt', 'sent_at', { unique: false });
|
||||
messages.createIndex('unread', ['conversationId', 'unread'], { unique: false });
|
||||
messages.createIndex('unread', ['conversationId', 'unread'], {
|
||||
unique: false,
|
||||
});
|
||||
messages.createIndex('expires_at', 'expires_at', { unique: false });
|
||||
|
||||
const conversations = transaction.db.createObjectStore('conversations');
|
||||
|
@ -59,7 +60,7 @@ const migrations = [
|
|||
const identityKeys = transaction.objectStore('identityKeys');
|
||||
const request = identityKeys.openCursor();
|
||||
const promises = [];
|
||||
request.onsuccess = (event) => {
|
||||
request.onsuccess = event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
const attributes = cursor.value;
|
||||
|
@ -67,14 +68,16 @@ const migrations = [
|
|||
attributes.firstUse = false;
|
||||
attributes.nonblockingApproval = false;
|
||||
attributes.verified = 0;
|
||||
promises.push(new Promise(((resolve, reject) => {
|
||||
const putRequest = identityKeys.put(attributes, attributes.id);
|
||||
putRequest.onsuccess = resolve;
|
||||
putRequest.onerror = (e) => {
|
||||
console.log(e);
|
||||
reject(e);
|
||||
};
|
||||
})));
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
const putRequest = identityKeys.put(attributes, attributes.id);
|
||||
putRequest.onsuccess = resolve;
|
||||
putRequest.onerror = e => {
|
||||
console.log(e);
|
||||
reject(e);
|
||||
};
|
||||
})
|
||||
);
|
||||
cursor.continue();
|
||||
} else {
|
||||
// no more results
|
||||
|
@ -84,7 +87,7 @@ const migrations = [
|
|||
});
|
||||
}
|
||||
};
|
||||
request.onerror = (event) => {
|
||||
request.onerror = event => {
|
||||
console.log(event);
|
||||
};
|
||||
},
|
||||
|
@ -129,7 +132,9 @@ const migrations = [
|
|||
|
||||
const messagesStore = transaction.objectStore('messages');
|
||||
console.log('Create index from attachment schema version to attachment');
|
||||
messagesStore.createIndex('schemaVersion', 'schemaVersion', { unique: false });
|
||||
messagesStore.createIndex('schemaVersion', 'schemaVersion', {
|
||||
unique: false,
|
||||
});
|
||||
|
||||
const duration = Date.now() - start;
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ const db = require('../database');
|
|||
const settings = require('../settings');
|
||||
const { runMigrations } = require('./run_migrations');
|
||||
|
||||
|
||||
// IMPORTANT: Add new migrations that need to traverse entire database, e.g.
|
||||
// messages store, below. Whenever we need this, we need to force attachment
|
||||
// migration on startup:
|
||||
|
@ -20,7 +19,9 @@ const migrations = [
|
|||
exports.run = async ({ Backbone, database } = {}) => {
|
||||
const { canRun } = await exports.getStatus({ database });
|
||||
if (!canRun) {
|
||||
throw new Error('Cannot run migrations on database without attachment data');
|
||||
throw new Error(
|
||||
'Cannot run migrations on database without attachment data'
|
||||
);
|
||||
}
|
||||
|
||||
await runMigrations({ Backbone, database });
|
||||
|
@ -28,8 +29,9 @@ exports.run = async ({ Backbone, database } = {}) => {
|
|||
|
||||
exports.getStatus = async ({ database } = {}) => {
|
||||
const connection = await db.open(database.id, database.version);
|
||||
const isAttachmentMigrationComplete =
|
||||
await settings.isAttachmentMigrationComplete(connection);
|
||||
const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete(
|
||||
connection
|
||||
);
|
||||
const hasMigrations = migrations.length > 0;
|
||||
|
||||
const canRun = isAttachmentMigrationComplete && hasMigrations;
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
/* eslint-env browser */
|
||||
|
||||
const {
|
||||
head,
|
||||
isFunction,
|
||||
isObject,
|
||||
isString,
|
||||
last,
|
||||
} = require('lodash');
|
||||
|
||||
const { head, isFunction, isObject, isString, last } = require('lodash');
|
||||
|
||||
const db = require('../database');
|
||||
const { deferredToPromise } = require('../deferred_to_promise');
|
||||
|
||||
|
||||
const closeDatabaseConnection = ({ Backbone } = {}) =>
|
||||
deferredToPromise(Backbone.sync('closeall'));
|
||||
|
||||
exports.runMigrations = async ({ Backbone, database } = {}) => {
|
||||
if (!isObject(Backbone) || !isObject(Backbone.Collection) ||
|
||||
!isFunction(Backbone.Collection.extend)) {
|
||||
if (
|
||||
!isObject(Backbone) ||
|
||||
!isObject(Backbone.Collection) ||
|
||||
!isFunction(Backbone.Collection.extend)
|
||||
) {
|
||||
throw new TypeError("'Backbone' is required");
|
||||
}
|
||||
|
||||
if (!isObject(database) || !isString(database.id) ||
|
||||
!Array.isArray(database.migrations)) {
|
||||
if (
|
||||
!isObject(database) ||
|
||||
!isString(database.id) ||
|
||||
!Array.isArray(database.migrations)
|
||||
) {
|
||||
throw new TypeError("'database' is required");
|
||||
}
|
||||
|
||||
|
@ -56,7 +54,7 @@ exports.runMigrations = async ({ Backbone, database } = {}) => {
|
|||
await closeDatabaseConnection({ Backbone });
|
||||
};
|
||||
|
||||
const getMigrationVersions = (database) => {
|
||||
const getMigrationVersions = database => {
|
||||
if (!isObject(database) || !Array.isArray(database.migrations)) {
|
||||
throw new TypeError("'database' is required");
|
||||
}
|
||||
|
@ -64,8 +62,12 @@ const getMigrationVersions = (database) => {
|
|||
const firstMigration = head(database.migrations);
|
||||
const lastMigration = last(database.migrations);
|
||||
|
||||
const firstVersion = firstMigration ? parseInt(firstMigration.version, 10) : null;
|
||||
const lastVersion = lastMigration ? parseInt(lastMigration.version, 10) : null;
|
||||
const firstVersion = firstMigration
|
||||
? parseInt(firstMigration.version, 10)
|
||||
: null;
|
||||
const lastVersion = lastMigration
|
||||
? parseInt(lastMigration.version, 10)
|
||||
: null;
|
||||
|
||||
return { firstVersion, lastVersion };
|
||||
};
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
/* eslint-env node */
|
||||
|
||||
exports.isMacOS = () =>
|
||||
process.platform === 'darwin';
|
||||
exports.isMacOS = () => process.platform === 'darwin';
|
||||
|
||||
exports.isLinux = () =>
|
||||
process.platform === 'linux';
|
||||
exports.isLinux = () => process.platform === 'linux';
|
||||
|
||||
exports.isWindows = () =>
|
||||
process.platform === 'win32';
|
||||
exports.isWindows = () => process.platform === 'win32';
|
||||
|
|
|
@ -6,22 +6,20 @@ const path = require('path');
|
|||
const { compose } = require('lodash/fp');
|
||||
const { escapeRegExp } = require('lodash');
|
||||
|
||||
|
||||
const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
|
||||
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
|
||||
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
||||
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
||||
|
||||
|
||||
// _redactPath :: Path -> String -> String
|
||||
exports._redactPath = (filePath) => {
|
||||
exports._redactPath = filePath => {
|
||||
if (!is.string(filePath)) {
|
||||
throw new TypeError("'filePath' must be a string");
|
||||
}
|
||||
|
||||
const filePathPattern = exports._pathToRegExp(filePath);
|
||||
|
||||
return (text) => {
|
||||
return text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
@ -35,7 +33,7 @@ exports._redactPath = (filePath) => {
|
|||
};
|
||||
|
||||
// _pathToRegExp :: Path -> Maybe RegExp
|
||||
exports._pathToRegExp = (filePath) => {
|
||||
exports._pathToRegExp = filePath => {
|
||||
try {
|
||||
const pathWithNormalizedSlashes = filePath.replace(/\//g, '\\');
|
||||
const pathWithEscapedSlashes = filePath.replace(/\\/g, '\\\\');
|
||||
|
@ -47,7 +45,9 @@ exports._pathToRegExp = (filePath) => {
|
|||
pathWithNormalizedSlashes,
|
||||
pathWithEscapedSlashes,
|
||||
urlEncodedPath,
|
||||
].map(escapeRegExp).join('|');
|
||||
]
|
||||
.map(escapeRegExp)
|
||||
.join('|');
|
||||
return new RegExp(patternString, 'g');
|
||||
} catch (error) {
|
||||
return null;
|
||||
|
@ -56,7 +56,7 @@ exports._pathToRegExp = (filePath) => {
|
|||
|
||||
// Public API
|
||||
// redactPhoneNumbers :: String -> String
|
||||
exports.redactPhoneNumbers = (text) => {
|
||||
exports.redactPhoneNumbers = text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ exports.redactPhoneNumbers = (text) => {
|
|||
};
|
||||
|
||||
// redactGroupIds :: String -> String
|
||||
exports.redactGroupIds = (text) => {
|
||||
exports.redactGroupIds = text => {
|
||||
if (!is.string(text)) {
|
||||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const { isObject, isString } = require('lodash');
|
||||
|
||||
|
||||
const ITEMS_STORE_NAME = 'items';
|
||||
const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
|
||||
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';
|
||||
|
@ -37,8 +36,7 @@ exports._getItem = (connection, key) => {
|
|||
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
|
||||
const request = itemsStore.get(key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = event =>
|
||||
resolve(event.target.result ? event.target.result.value : null);
|
||||
|
@ -58,11 +56,9 @@ exports._setItem = (connection, key, value) => {
|
|||
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
|
||||
const request = itemsStore.put({ id: key, value }, key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = () =>
|
||||
resolve();
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -79,10 +75,8 @@ exports._deleteItem = (connection, key) => {
|
|||
const itemsStore = transaction.objectStore(ITEMS_STORE_NAME);
|
||||
const request = itemsStore.delete(key);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = event =>
|
||||
reject(event.target.error);
|
||||
request.onerror = event => reject(event.target.error);
|
||||
|
||||
request.onsuccess = () =>
|
||||
resolve();
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* global setTimeout */
|
||||
|
||||
exports.sleep = ms =>
|
||||
new Promise(resolve => setTimeout(resolve, ms));
|
||||
exports.sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
|
|
@ -3,7 +3,6 @@ const is = require('@sindresorhus/is');
|
|||
const Errors = require('./types/errors');
|
||||
const Settings = require('./settings');
|
||||
|
||||
|
||||
exports.syncReadReceiptConfiguration = async ({
|
||||
deviceId,
|
||||
sendRequestConfigurationSyncMessage,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
exports.stringToArrayBuffer = (string) => {
|
||||
exports.stringToArrayBuffer = string => {
|
||||
if (typeof string !== 'string') {
|
||||
throw new TypeError("'string' must be a string");
|
||||
}
|
||||
|
|
|
@ -2,9 +2,15 @@ const is = require('@sindresorhus/is');
|
|||
|
||||
const AttachmentTS = require('../../../ts/types/Attachment');
|
||||
const MIME = require('../../../ts/types/MIME');
|
||||
const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util');
|
||||
const {
|
||||
arrayBufferToBlob,
|
||||
blobToArrayBuffer,
|
||||
dataURLToBlob,
|
||||
} = require('blob-util');
|
||||
const { autoOrientImage } = require('../auto_orient_image');
|
||||
const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_system');
|
||||
const {
|
||||
migrateDataToFileSystem,
|
||||
} = require('./attachment/migrate_data_to_file_system');
|
||||
|
||||
// // Incoming message attachment fields
|
||||
// {
|
||||
|
@ -30,7 +36,7 @@ const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_s
|
|||
// Returns true if `rawAttachment` is a valid attachment based on our current schema.
|
||||
// Over time, we can expand this definition to become more narrow, e.g. require certain
|
||||
// fields, etc.
|
||||
exports.isValid = (rawAttachment) => {
|
||||
exports.isValid = rawAttachment => {
|
||||
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
|
||||
// deserialized by protobuf:
|
||||
if (!rawAttachment) {
|
||||
|
@ -41,12 +47,15 @@ exports.isValid = (rawAttachment) => {
|
|||
};
|
||||
|
||||
// Upgrade steps
|
||||
exports.autoOrientJPEG = async (attachment) => {
|
||||
exports.autoOrientJPEG = async attachment => {
|
||||
if (!MIME.isJPEG(attachment.contentType)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const dataBlob = await arrayBufferToBlob(attachment.data, attachment.contentType);
|
||||
const dataBlob = await arrayBufferToBlob(
|
||||
attachment.data,
|
||||
attachment.contentType
|
||||
);
|
||||
const newDataBlob = await dataURLToBlob(await autoOrientImage(dataBlob));
|
||||
const newDataArrayBuffer = await blobToArrayBuffer(newDataBlob);
|
||||
|
||||
|
@ -76,7 +85,7 @@ const INVALID_CHARACTERS_PATTERN = new RegExp(
|
|||
// NOTE: Expose synchronous version to do property-based testing using `testcheck`,
|
||||
// which currently doesn’t support async testing:
|
||||
// https://github.com/leebyron/testcheck-js/issues/45
|
||||
exports._replaceUnicodeOrderOverridesSync = (attachment) => {
|
||||
exports._replaceUnicodeOrderOverridesSync = attachment => {
|
||||
if (!is.string(attachment.fileName)) {
|
||||
return attachment;
|
||||
}
|
||||
|
@ -95,9 +104,12 @@ exports._replaceUnicodeOrderOverridesSync = (attachment) => {
|
|||
exports.replaceUnicodeOrderOverrides = async attachment =>
|
||||
exports._replaceUnicodeOrderOverridesSync(attachment);
|
||||
|
||||
exports.removeSchemaVersion = (attachment) => {
|
||||
exports.removeSchemaVersion = attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
console.log('Attachment.removeSchemaVersion: Invalid input attachment:', attachment);
|
||||
console.log(
|
||||
'Attachment.removeSchemaVersion: Invalid input attachment:',
|
||||
attachment
|
||||
);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
|
@ -115,12 +127,12 @@ exports.hasData = attachment =>
|
|||
// loadData :: (RelativePath -> IO (Promise ArrayBuffer))
|
||||
// Attachment ->
|
||||
// IO (Promise Attachment)
|
||||
exports.loadData = (readAttachmentData) => {
|
||||
exports.loadData = readAttachmentData => {
|
||||
if (!is.function(readAttachmentData)) {
|
||||
throw new TypeError("'readAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (attachment) => {
|
||||
return async attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
throw new TypeError("'attachment' is not valid");
|
||||
}
|
||||
|
@ -142,12 +154,12 @@ exports.loadData = (readAttachmentData) => {
|
|||
// deleteData :: (RelativePath -> IO Unit)
|
||||
// Attachment ->
|
||||
// IO Unit
|
||||
exports.deleteData = (deleteAttachmentData) => {
|
||||
exports.deleteData = deleteAttachmentData => {
|
||||
if (!is.function(deleteAttachmentData)) {
|
||||
throw new TypeError("'deleteAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (attachment) => {
|
||||
return async attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
throw new TypeError("'attachment' is not valid");
|
||||
}
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
const {
|
||||
isArrayBuffer,
|
||||
isFunction,
|
||||
isUndefined,
|
||||
omit,
|
||||
} = require('lodash');
|
||||
|
||||
const { isArrayBuffer, isFunction, isUndefined, omit } = require('lodash');
|
||||
|
||||
// type Context :: {
|
||||
// writeNewAttachmentData :: ArrayBuffer -> Promise (IO Path)
|
||||
|
@ -13,7 +7,10 @@ const {
|
|||
// migrateDataToFileSystem :: Attachment ->
|
||||
// Context ->
|
||||
// Promise Attachment
|
||||
exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData } = {}) => {
|
||||
exports.migrateDataToFileSystem = async (
|
||||
attachment,
|
||||
{ writeNewAttachmentData } = {}
|
||||
) => {
|
||||
if (!isFunction(writeNewAttachmentData)) {
|
||||
throw new TypeError("'writeNewAttachmentData' must be a function");
|
||||
}
|
||||
|
@ -28,15 +25,16 @@ exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData }
|
|||
|
||||
const isValidData = isArrayBuffer(data);
|
||||
if (!isValidData) {
|
||||
throw new TypeError('Expected `attachment.data` to be an array buffer;' +
|
||||
` got: ${typeof attachment.data}`);
|
||||
throw new TypeError(
|
||||
'Expected `attachment.data` to be an array buffer;' +
|
||||
` got: ${typeof attachment.data}`
|
||||
);
|
||||
}
|
||||
|
||||
const path = await writeNewAttachmentData(data);
|
||||
|
||||
const attachmentWithoutData = omit(
|
||||
Object.assign({}, attachment, { path }),
|
||||
['data']
|
||||
);
|
||||
const attachmentWithoutData = omit(Object.assign({}, attachment, { path }), [
|
||||
'data',
|
||||
]);
|
||||
return attachmentWithoutData;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// toLogFormat :: Error -> String
|
||||
exports.toLogFormat = (error) => {
|
||||
exports.toLogFormat = error => {
|
||||
if (!error) {
|
||||
return error;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ const { isFunction, isString, omit } = require('lodash');
|
|||
const Attachment = require('./attachment');
|
||||
const Errors = require('./errors');
|
||||
const SchemaVersion = require('./schema_version');
|
||||
const { initializeAttachmentMetadata } =
|
||||
require('../../../ts/types/message/initializeAttachmentMetadata');
|
||||
|
||||
const {
|
||||
initializeAttachmentMetadata,
|
||||
} = require('../../../ts/types/message/initializeAttachmentMetadata');
|
||||
|
||||
const GROUP = 'group';
|
||||
const PRIVATE = 'private';
|
||||
|
@ -37,19 +37,17 @@ const INITIAL_SCHEMA_VERSION = 0;
|
|||
// how we do database migrations:
|
||||
exports.CURRENT_SCHEMA_VERSION = 5;
|
||||
|
||||
|
||||
// Public API
|
||||
exports.GROUP = GROUP;
|
||||
exports.PRIVATE = PRIVATE;
|
||||
|
||||
// Placeholder until we have stronger preconditions:
|
||||
exports.isValid = () =>
|
||||
true;
|
||||
exports.isValid = () => true;
|
||||
|
||||
// Schema
|
||||
exports.initializeSchemaVersion = (message) => {
|
||||
const isInitialized = SchemaVersion.isValid(message.schemaVersion) &&
|
||||
message.schemaVersion >= 1;
|
||||
exports.initializeSchemaVersion = message => {
|
||||
const isInitialized =
|
||||
SchemaVersion.isValid(message.schemaVersion) && message.schemaVersion >= 1;
|
||||
if (isInitialized) {
|
||||
return message;
|
||||
}
|
||||
|
@ -59,27 +57,23 @@ exports.initializeSchemaVersion = (message) => {
|
|||
: 0;
|
||||
const hasAttachments = numAttachments > 0;
|
||||
if (!hasAttachments) {
|
||||
return Object.assign(
|
||||
{},
|
||||
message,
|
||||
{ schemaVersion: INITIAL_SCHEMA_VERSION }
|
||||
);
|
||||
return Object.assign({}, message, {
|
||||
schemaVersion: INITIAL_SCHEMA_VERSION,
|
||||
});
|
||||
}
|
||||
|
||||
// All attachments should have the same schema version, so we just pick
|
||||
// the first one:
|
||||
const firstAttachment = message.attachments[0];
|
||||
const inheritedSchemaVersion = SchemaVersion.isValid(firstAttachment.schemaVersion)
|
||||
const inheritedSchemaVersion = SchemaVersion.isValid(
|
||||
firstAttachment.schemaVersion
|
||||
)
|
||||
? firstAttachment.schemaVersion
|
||||
: INITIAL_SCHEMA_VERSION;
|
||||
const messageWithInitialSchema = Object.assign(
|
||||
{},
|
||||
message,
|
||||
{
|
||||
schemaVersion: inheritedSchemaVersion,
|
||||
attachments: message.attachments.map(Attachment.removeSchemaVersion),
|
||||
}
|
||||
);
|
||||
const messageWithInitialSchema = Object.assign({}, message, {
|
||||
schemaVersion: inheritedSchemaVersion,
|
||||
attachments: message.attachments.map(Attachment.removeSchemaVersion),
|
||||
});
|
||||
|
||||
return messageWithInitialSchema;
|
||||
};
|
||||
|
@ -98,7 +92,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
|
|||
|
||||
return async (message, context) => {
|
||||
if (!exports.isValid(message)) {
|
||||
console.log('Message._withSchemaVersion: Invalid input message:', message);
|
||||
console.log(
|
||||
'Message._withSchemaVersion: Invalid input message:',
|
||||
message
|
||||
);
|
||||
return message;
|
||||
}
|
||||
|
||||
|
@ -138,15 +135,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
|
|||
return message;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
upgradedMessage,
|
||||
{ schemaVersion }
|
||||
);
|
||||
return Object.assign({}, upgradedMessage, { schemaVersion });
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// Public API
|
||||
// _mapAttachments :: (Attachment -> Promise Attachment) ->
|
||||
// (Message, Context) ->
|
||||
|
@ -154,19 +146,24 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
|
|||
exports._mapAttachments = upgradeAttachment => async (message, context) => {
|
||||
const upgradeWithContext = attachment =>
|
||||
upgradeAttachment(attachment, context);
|
||||
const attachments = await Promise.all(message.attachments.map(upgradeWithContext));
|
||||
const attachments = await Promise.all(
|
||||
message.attachments.map(upgradeWithContext)
|
||||
);
|
||||
return Object.assign({}, message, { attachments });
|
||||
};
|
||||
|
||||
// _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) ->
|
||||
// (Message, Context) ->
|
||||
// Promise Message
|
||||
exports._mapQuotedAttachments = upgradeAttachment => async (message, context) => {
|
||||
exports._mapQuotedAttachments = upgradeAttachment => async (
|
||||
message,
|
||||
context
|
||||
) => {
|
||||
if (!message.quote) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const upgradeWithContext = async (attachment) => {
|
||||
const upgradeWithContext = async attachment => {
|
||||
const { thumbnail } = attachment;
|
||||
if (!thumbnail) {
|
||||
return attachment;
|
||||
|
@ -185,7 +182,9 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) =>
|
|||
|
||||
const quotedAttachments = (message.quote && message.quote.attachments) || [];
|
||||
|
||||
const attachments = await Promise.all(quotedAttachments.map(upgradeWithContext));
|
||||
const attachments = await Promise.all(
|
||||
quotedAttachments.map(upgradeWithContext)
|
||||
);
|
||||
return Object.assign({}, message, {
|
||||
quote: Object.assign({}, message.quote, {
|
||||
attachments,
|
||||
|
@ -193,8 +192,7 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) =>
|
|||
});
|
||||
};
|
||||
|
||||
const toVersion0 = async message =>
|
||||
exports.initializeSchemaVersion(message);
|
||||
const toVersion0 = async message => exports.initializeSchemaVersion(message);
|
||||
|
||||
const toVersion1 = exports._withSchemaVersion(
|
||||
1,
|
||||
|
@ -241,25 +239,28 @@ exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
|
|||
return message;
|
||||
};
|
||||
|
||||
exports.createAttachmentLoader = (loadAttachmentData) => {
|
||||
exports.createAttachmentLoader = loadAttachmentData => {
|
||||
if (!isFunction(loadAttachmentData)) {
|
||||
throw new TypeError('`loadAttachmentData` is required');
|
||||
}
|
||||
|
||||
return async message => (Object.assign({}, message, {
|
||||
attachments: await Promise.all(message.attachments.map(loadAttachmentData)),
|
||||
}));
|
||||
return async message =>
|
||||
Object.assign({}, message, {
|
||||
attachments: await Promise.all(
|
||||
message.attachments.map(loadAttachmentData)
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// createAttachmentDataWriter :: (RelativePath -> IO Unit)
|
||||
// Message ->
|
||||
// IO (Promise Message)
|
||||
exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
|
||||
exports.createAttachmentDataWriter = writeExistingAttachmentData => {
|
||||
if (!isFunction(writeExistingAttachmentData)) {
|
||||
throw new TypeError("'writeExistingAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (rawMessage) => {
|
||||
return async rawMessage => {
|
||||
if (!exports.isValid(rawMessage)) {
|
||||
throw new TypeError("'rawMessage' is not valid");
|
||||
}
|
||||
|
@ -282,17 +283,21 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
|
|||
return message;
|
||||
}
|
||||
|
||||
(attachments || []).forEach((attachment) => {
|
||||
(attachments || []).forEach(attachment => {
|
||||
if (!Attachment.hasData(attachment)) {
|
||||
throw new TypeError("'attachment.data' is required during message import");
|
||||
throw new TypeError(
|
||||
"'attachment.data' is required during message import"
|
||||
);
|
||||
}
|
||||
|
||||
if (!isString(attachment.path)) {
|
||||
throw new TypeError("'attachment.path' is required during message import");
|
||||
throw new TypeError(
|
||||
"'attachment.path' is required during message import"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const writeThumbnails = exports._mapQuotedAttachments(async (thumbnail) => {
|
||||
const writeThumbnails = exports._mapQuotedAttachments(async thumbnail => {
|
||||
const { data, path } = thumbnail;
|
||||
|
||||
// we want to be bulletproof to thumbnails without data
|
||||
|
@ -315,10 +320,12 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => {
|
|||
{},
|
||||
await writeThumbnails(message),
|
||||
{
|
||||
attachments: await Promise.all((attachments || []).map(async (attachment) => {
|
||||
await writeExistingAttachmentData(attachment);
|
||||
return omit(attachment, ['data']);
|
||||
})),
|
||||
attachments: await Promise.all(
|
||||
(attachments || []).map(async attachment => {
|
||||
await writeExistingAttachmentData(attachment);
|
||||
return omit(attachment, ['data']);
|
||||
})
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
const { isNumber } = require('lodash');
|
||||
|
||||
|
||||
exports.isValid = value =>
|
||||
isNumber(value) && value >= 0;
|
||||
exports.isValid = value => isNumber(value) && value >= 0;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
const OS = require('../os');
|
||||
|
||||
exports.isAudioNotificationSupported = () =>
|
||||
!OS.isLinux();
|
||||
exports.isAudioNotificationSupported = () => !OS.isLinux();
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
/* global i18n: false */
|
||||
|
||||
|
||||
const OPTIMIZATION_MESSAGE_DISPLAY_THRESHOLD = 1000; // milliseconds
|
||||
|
||||
const setMessage = () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue