2018-07-27 01:13:56 +00:00
|
|
|
const path = require('path');
|
|
|
|
const mkdirp = require('mkdirp');
|
|
|
|
const rimraf = require('rimraf');
|
|
|
|
const sql = require('@journeyapps/sqlcipher');
|
2019-02-20 01:32:44 +00:00
|
|
|
const { app, dialog, clipboard } = require('electron');
|
|
|
|
const { redactAll } = require('../js/modules/privacy');
|
|
|
|
const { remove: removeUserConfig } = require('./user_config');
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
const pify = require('pify');
|
|
|
|
const uuidv4 = require('uuid/v4');
|
2019-02-20 01:32:44 +00:00
|
|
|
const { map, isObject, isString, fromPairs, forEach, last } = require('lodash');
|
2018-07-27 01:13:56 +00:00
|
|
|
|
|
|
|
// To get long stack traces
|
|
|
|
// https://github.com/mapbox/node-sqlite3/wiki/API#sqlite3verbose
|
|
|
|
sql.verbose();
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
initialize,
|
|
|
|
close,
|
|
|
|
removeDB,
|
2018-11-01 23:40:19 +00:00
|
|
|
removeIndexedDBFiles,
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
createOrUpdateIdentityKey,
|
|
|
|
getIdentityKeyById,
|
|
|
|
bulkAddIdentityKeys,
|
|
|
|
removeIdentityKeyById,
|
|
|
|
removeAllIdentityKeys,
|
2019-02-04 23:54:37 +00:00
|
|
|
getAllIdentityKeys,
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
createOrUpdatePreKey,
|
|
|
|
getPreKeyById,
|
|
|
|
bulkAddPreKeys,
|
|
|
|
removePreKeyById,
|
|
|
|
removeAllPreKeys,
|
2019-02-04 23:54:37 +00:00
|
|
|
getAllPreKeys,
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
createOrUpdateSignedPreKey,
|
|
|
|
getSignedPreKeyById,
|
|
|
|
getAllSignedPreKeys,
|
|
|
|
bulkAddSignedPreKeys,
|
|
|
|
removeSignedPreKeyById,
|
|
|
|
removeAllSignedPreKeys,
|
|
|
|
|
|
|
|
createOrUpdateItem,
|
|
|
|
getItemById,
|
|
|
|
getAllItems,
|
|
|
|
bulkAddItems,
|
|
|
|
removeItemById,
|
|
|
|
removeAllItems,
|
|
|
|
|
|
|
|
createOrUpdateSession,
|
|
|
|
getSessionById,
|
|
|
|
getSessionsByNumber,
|
|
|
|
bulkAddSessions,
|
|
|
|
removeSessionById,
|
|
|
|
removeSessionsByNumber,
|
|
|
|
removeAllSessions,
|
2019-02-04 23:54:37 +00:00
|
|
|
getAllSessions,
|
2018-10-18 01:01:21 +00:00
|
|
|
|
2018-09-21 01:47:19 +00:00
|
|
|
getConversationCount,
|
|
|
|
saveConversation,
|
|
|
|
saveConversations,
|
|
|
|
getConversationById,
|
|
|
|
updateConversation,
|
|
|
|
removeConversation,
|
|
|
|
getAllConversations,
|
|
|
|
getAllConversationIds,
|
|
|
|
getAllPrivateConversations,
|
|
|
|
getAllGroupsInvolvingId,
|
2019-01-14 21:47:19 +00:00
|
|
|
|
2018-09-21 01:47:19 +00:00
|
|
|
searchConversations,
|
2019-01-14 21:47:19 +00:00
|
|
|
searchMessages,
|
|
|
|
searchMessagesInConversation,
|
2018-09-21 01:47:19 +00:00
|
|
|
|
2018-08-06 23:18:58 +00:00
|
|
|
getMessageCount,
|
2018-07-27 01:13:56 +00:00
|
|
|
saveMessage,
|
|
|
|
saveMessages,
|
|
|
|
removeMessage,
|
|
|
|
getUnreadByConversation,
|
|
|
|
getMessageBySender,
|
|
|
|
getMessageById,
|
2018-09-21 01:47:19 +00:00
|
|
|
getAllMessages,
|
2018-07-27 01:13:56 +00:00
|
|
|
getAllMessageIds,
|
|
|
|
getMessagesBySentAt,
|
|
|
|
getExpiredMessages,
|
2018-08-07 19:33:56 +00:00
|
|
|
getOutgoingWithoutExpiresAt,
|
2018-07-27 01:13:56 +00:00
|
|
|
getNextExpiringMessage,
|
|
|
|
getMessagesByConversation,
|
|
|
|
|
2018-09-28 22:51:26 +00:00
|
|
|
getUnprocessedCount,
|
2018-07-27 01:13:56 +00:00
|
|
|
getAllUnprocessed,
|
|
|
|
saveUnprocessed,
|
2019-02-05 01:23:50 +00:00
|
|
|
updateUnprocessedAttempts,
|
|
|
|
updateUnprocessedWithData,
|
2018-07-27 01:13:56 +00:00
|
|
|
getUnprocessedById,
|
|
|
|
saveUnprocesseds,
|
|
|
|
removeUnprocessed,
|
|
|
|
removeAllUnprocessed,
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
getNextAttachmentDownloadJobs,
|
|
|
|
saveAttachmentDownloadJob,
|
|
|
|
setAttachmentDownloadJobPending,
|
|
|
|
resetAttachmentDownloadPending,
|
|
|
|
removeAttachmentDownloadJob,
|
|
|
|
removeAllAttachmentDownloadJobs,
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
removeAll,
|
2018-10-18 01:01:21 +00:00
|
|
|
removeAllConfiguration,
|
2018-07-27 01:13:56 +00:00
|
|
|
|
|
|
|
getMessagesNeedingUpgrade,
|
|
|
|
getMessagesWithVisualMediaAttachments,
|
|
|
|
getMessagesWithFileAttachments,
|
2018-08-06 23:18:58 +00:00
|
|
|
|
|
|
|
removeKnownAttachments,
|
2018-07-27 01:13:56 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
function generateUUID() {
|
|
|
|
return uuidv4();
|
|
|
|
}
|
|
|
|
|
|
|
|
function objectToJSON(data) {
|
|
|
|
return JSON.stringify(data);
|
|
|
|
}
|
|
|
|
function jsonToObject(json) {
|
|
|
|
return JSON.parse(json);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function openDatabase(filePath) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const instance = new sql.Database(filePath, error => {
|
|
|
|
if (error) {
|
|
|
|
return reject(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
return resolve(instance);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function promisify(rawInstance) {
|
|
|
|
/* eslint-disable no-param-reassign */
|
|
|
|
rawInstance.close = pify(rawInstance.close.bind(rawInstance));
|
|
|
|
rawInstance.run = pify(rawInstance.run.bind(rawInstance));
|
|
|
|
rawInstance.get = pify(rawInstance.get.bind(rawInstance));
|
|
|
|
rawInstance.all = pify(rawInstance.all.bind(rawInstance));
|
|
|
|
rawInstance.each = pify(rawInstance.each.bind(rawInstance));
|
|
|
|
rawInstance.exec = pify(rawInstance.exec.bind(rawInstance));
|
|
|
|
rawInstance.prepare = pify(rawInstance.prepare.bind(rawInstance));
|
|
|
|
/* eslint-enable */
|
|
|
|
|
|
|
|
return rawInstance;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getSQLiteVersion(instance) {
|
|
|
|
const row = await instance.get('select sqlite_version() AS sqlite_version');
|
|
|
|
return row.sqlite_version;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getSchemaVersion(instance) {
|
|
|
|
const row = await instance.get('PRAGMA schema_version;');
|
|
|
|
return row.schema_version;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getSQLCipherVersion(instance) {
|
|
|
|
const row = await instance.get('PRAGMA cipher_version;');
|
|
|
|
try {
|
|
|
|
return row.cipher_version;
|
|
|
|
} catch (e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const INVALID_KEY = /[^0-9A-Fa-f]/;
|
|
|
|
async function setupSQLCipher(instance, { key }) {
|
|
|
|
const match = INVALID_KEY.exec(key);
|
|
|
|
if (match) {
|
|
|
|
throw new Error(`setupSQLCipher: key '${key}' is not valid`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
|
|
|
|
await instance.run(`PRAGMA key = "x'${key}'";`);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function updateToSchemaVersion1(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('updateToSchemaVersion1: starting...');
|
|
|
|
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE messages(
|
2018-08-07 19:33:56 +00:00
|
|
|
id STRING PRIMARY KEY ASC,
|
|
|
|
json TEXT,
|
|
|
|
|
|
|
|
unread INTEGER,
|
|
|
|
expires_at INTEGER,
|
|
|
|
sent_at INTEGER,
|
|
|
|
schemaVersion INTEGER,
|
|
|
|
conversationId STRING,
|
|
|
|
received_at INTEGER,
|
|
|
|
source STRING,
|
|
|
|
sourceDevice STRING,
|
|
|
|
hasAttachments INTEGER,
|
|
|
|
hasFileAttachments INTEGER,
|
|
|
|
hasVisualMediaAttachments INTEGER
|
|
|
|
);`
|
2018-07-27 01:13:56 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX messages_unread ON messages (
|
|
|
|
unread
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX messages_expires_at ON messages (
|
|
|
|
expires_at
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX messages_receipt ON messages (
|
|
|
|
sent_at
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX messages_schemaVersion ON messages (
|
|
|
|
schemaVersion
|
|
|
|
);`);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX messages_conversation ON messages (
|
|
|
|
conversationId,
|
|
|
|
received_at
|
|
|
|
);`);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX messages_duplicate_check ON messages (
|
|
|
|
source,
|
|
|
|
sourceDevice,
|
|
|
|
sent_at
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX messages_hasAttachments ON messages (
|
|
|
|
conversationId,
|
|
|
|
hasAttachments,
|
|
|
|
received_at
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX messages_hasFileAttachments ON messages (
|
|
|
|
conversationId,
|
|
|
|
hasFileAttachments,
|
|
|
|
received_at
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX messages_hasVisualMediaAttachments ON messages (
|
|
|
|
conversationId,
|
|
|
|
hasVisualMediaAttachments,
|
|
|
|
received_at
|
|
|
|
);`);
|
|
|
|
|
|
|
|
await instance.run(`CREATE TABLE unprocessed(
|
|
|
|
id STRING,
|
|
|
|
timestamp INTEGER,
|
|
|
|
json TEXT
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX unprocessed_id ON unprocessed (
|
|
|
|
id
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX unprocessed_timestamp ON unprocessed (
|
|
|
|
timestamp
|
|
|
|
);`);
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 1;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
|
|
|
|
console.log('updateToSchemaVersion1: success!');
|
|
|
|
}
|
|
|
|
|
2018-08-07 19:33:56 +00:00
|
|
|
async function updateToSchemaVersion2(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('updateToSchemaVersion2: starting...');
|
|
|
|
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
await instance.run(
|
|
|
|
`ALTER TABLE messages
|
|
|
|
ADD COLUMN expireTimer INTEGER;`
|
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run(
|
|
|
|
`ALTER TABLE messages
|
|
|
|
ADD COLUMN expirationStartTimestamp INTEGER;`
|
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run(
|
|
|
|
`ALTER TABLE messages
|
|
|
|
ADD COLUMN type STRING;`
|
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX messages_expiring ON messages (
|
|
|
|
expireTimer,
|
|
|
|
expirationStartTimestamp,
|
|
|
|
expires_at
|
|
|
|
);`);
|
|
|
|
|
|
|
|
await instance.run(
|
|
|
|
`UPDATE messages SET
|
|
|
|
expirationStartTimestamp = json_extract(json, '$.expirationStartTimestamp'),
|
|
|
|
expireTimer = json_extract(json, '$.expireTimer'),
|
|
|
|
type = json_extract(json, '$.type');`
|
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 2;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
|
|
|
|
console.log('updateToSchemaVersion2: success!');
|
|
|
|
}
|
|
|
|
|
2018-08-09 01:32:10 +00:00
|
|
|
async function updateToSchemaVersion3(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 3) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('updateToSchemaVersion3: starting...');
|
|
|
|
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
await instance.run('DROP INDEX messages_expiring;');
|
|
|
|
await instance.run('DROP INDEX messages_unread;');
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX messages_without_timer ON messages (
|
|
|
|
expireTimer,
|
|
|
|
expires_at,
|
|
|
|
type
|
|
|
|
) WHERE expires_at IS NULL AND expireTimer IS NOT NULL;`);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX messages_unread ON messages (
|
|
|
|
conversationId,
|
|
|
|
unread
|
|
|
|
) WHERE unread IS NOT NULL;`);
|
|
|
|
|
|
|
|
await instance.run('ANALYZE;');
|
|
|
|
await instance.run('PRAGMA schema_version = 3;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
|
|
|
|
console.log('updateToSchemaVersion3: success!');
|
|
|
|
}
|
|
|
|
|
2018-09-21 01:47:19 +00:00
|
|
|
async function updateToSchemaVersion4(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 4) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('updateToSchemaVersion4: starting...');
|
|
|
|
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE conversations(
|
|
|
|
id STRING PRIMARY KEY ASC,
|
|
|
|
json TEXT,
|
|
|
|
|
|
|
|
active_at INTEGER,
|
|
|
|
type STRING,
|
|
|
|
members TEXT,
|
|
|
|
name TEXT,
|
|
|
|
profileName TEXT
|
|
|
|
);`
|
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX conversations_active ON conversations (
|
|
|
|
active_at
|
|
|
|
) WHERE active_at IS NOT NULL;`);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX conversations_type ON conversations (
|
|
|
|
type
|
|
|
|
) WHERE type IS NOT NULL;`);
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 4;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
|
|
|
|
console.log('updateToSchemaVersion4: success!');
|
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
async function updateToSchemaVersion6(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 6) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log('updateToSchemaVersion6: starting...');
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
// key-value, ids are strings, one extra column
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE sessions(
|
|
|
|
id STRING PRIMARY KEY ASC,
|
|
|
|
number STRING,
|
|
|
|
json TEXT
|
|
|
|
);`
|
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX sessions_number ON sessions (
|
|
|
|
number
|
|
|
|
) WHERE number IS NOT NULL;`);
|
|
|
|
|
|
|
|
// key-value, ids are strings
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE groups(
|
|
|
|
id STRING PRIMARY KEY ASC,
|
|
|
|
json TEXT
|
|
|
|
);`
|
|
|
|
);
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE identityKeys(
|
|
|
|
id STRING PRIMARY KEY ASC,
|
|
|
|
json TEXT
|
|
|
|
);`
|
|
|
|
);
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE items(
|
|
|
|
id STRING PRIMARY KEY ASC,
|
|
|
|
json TEXT
|
|
|
|
);`
|
|
|
|
);
|
|
|
|
|
|
|
|
// key-value, ids are integers
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE preKeys(
|
|
|
|
id INTEGER PRIMARY KEY ASC,
|
|
|
|
json TEXT
|
|
|
|
);`
|
|
|
|
);
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE signedPreKeys(
|
|
|
|
id INTEGER PRIMARY KEY ASC,
|
|
|
|
json TEXT
|
|
|
|
);`
|
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 6;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
console.log('updateToSchemaVersion6: success!');
|
|
|
|
}
|
|
|
|
|
2018-11-20 01:38:55 +00:00
|
|
|
async function updateToSchemaVersion7(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 7) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log('updateToSchemaVersion7: starting...');
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
// SQLite has been coercing our STRINGs into numbers, so we force it with TEXT
|
|
|
|
// We create a new table then copy the data into it, since we can't modify columns
|
|
|
|
|
|
|
|
await instance.run('DROP INDEX sessions_number;');
|
|
|
|
await instance.run('ALTER TABLE sessions RENAME TO sessions_old;');
|
|
|
|
|
|
|
|
await instance.run(
|
|
|
|
`CREATE TABLE sessions(
|
|
|
|
id TEXT PRIMARY KEY,
|
|
|
|
number TEXT,
|
|
|
|
json TEXT
|
|
|
|
);`
|
|
|
|
);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX sessions_number ON sessions (
|
|
|
|
number
|
|
|
|
) WHERE number IS NOT NULL;`);
|
|
|
|
|
|
|
|
await instance.run(`INSERT INTO sessions(id, number, json)
|
|
|
|
SELECT "+" || id, number, json FROM sessions_old;
|
|
|
|
`);
|
|
|
|
|
|
|
|
await instance.run('DROP TABLE sessions_old;');
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 7;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
console.log('updateToSchemaVersion7: success!');
|
|
|
|
}
|
|
|
|
|
2019-01-14 21:47:19 +00:00
|
|
|
async function updateToSchemaVersion8(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 8) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log('updateToSchemaVersion8: starting...');
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
// First, we pull a new body field out of the message table's json blob
|
|
|
|
await instance.run(
|
|
|
|
`ALTER TABLE messages
|
|
|
|
ADD COLUMN body TEXT;`
|
|
|
|
);
|
|
|
|
await instance.run("UPDATE messages SET body = json_extract(json, '$.body')");
|
|
|
|
|
|
|
|
// Then we create our full-text search table and populate it
|
|
|
|
await instance.run(`
|
|
|
|
CREATE VIRTUAL TABLE messages_fts
|
|
|
|
USING fts5(id UNINDEXED, body);
|
|
|
|
`);
|
|
|
|
await instance.run(`
|
|
|
|
INSERT INTO messages_fts(id, body)
|
|
|
|
SELECT id, body FROM messages;
|
|
|
|
`);
|
|
|
|
|
|
|
|
// Then we set up triggers to keep the full-text search table up to date
|
|
|
|
await instance.run(`
|
|
|
|
CREATE TRIGGER messages_on_insert AFTER INSERT ON messages BEGIN
|
|
|
|
INSERT INTO messages_fts (
|
|
|
|
id,
|
|
|
|
body
|
|
|
|
) VALUES (
|
|
|
|
new.id,
|
|
|
|
new.body
|
|
|
|
);
|
|
|
|
END;
|
|
|
|
`);
|
|
|
|
await instance.run(`
|
|
|
|
CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN
|
|
|
|
DELETE FROM messages_fts WHERE id = old.id;
|
|
|
|
END;
|
|
|
|
`);
|
|
|
|
await instance.run(`
|
|
|
|
CREATE TRIGGER messages_on_update AFTER UPDATE ON messages BEGIN
|
|
|
|
DELETE FROM messages_fts WHERE id = old.id;
|
|
|
|
INSERT INTO messages_fts(
|
|
|
|
id,
|
|
|
|
body
|
|
|
|
) VALUES (
|
|
|
|
new.id,
|
|
|
|
new.body
|
|
|
|
);
|
|
|
|
END;
|
|
|
|
`);
|
|
|
|
|
|
|
|
// For formatting search results:
|
|
|
|
// https://sqlite.org/fts5.html#the_highlight_function
|
|
|
|
// https://sqlite.org/fts5.html#the_snippet_function
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 8;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
console.log('updateToSchemaVersion8: success!');
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
async function updateToSchemaVersion9(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 9) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log('updateToSchemaVersion9: starting...');
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
await instance.run(`CREATE TABLE attachment_downloads(
|
|
|
|
id STRING primary key,
|
|
|
|
timestamp INTEGER,
|
|
|
|
pending INTEGER,
|
|
|
|
json TEXT
|
|
|
|
);`);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX attachment_downloads_timestamp
|
|
|
|
ON attachment_downloads (
|
|
|
|
timestamp
|
|
|
|
) WHERE pending = 0;`);
|
|
|
|
await instance.run(`CREATE INDEX attachment_downloads_pending
|
|
|
|
ON attachment_downloads (
|
|
|
|
pending
|
|
|
|
) WHERE pending != 0;`);
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 9;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
console.log('updateToSchemaVersion9: success!');
|
|
|
|
}
|
|
|
|
|
2019-02-05 01:23:50 +00:00
|
|
|
async function updateToSchemaVersion10(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 10) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log('updateToSchemaVersion10: starting...');
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
await instance.run('DROP INDEX unprocessed_id;');
|
|
|
|
await instance.run('DROP INDEX unprocessed_timestamp;');
|
|
|
|
await instance.run('ALTER TABLE unprocessed RENAME TO unprocessed_old;');
|
|
|
|
|
|
|
|
await instance.run(`CREATE TABLE unprocessed(
|
|
|
|
id STRING,
|
|
|
|
timestamp INTEGER,
|
|
|
|
version INTEGER,
|
|
|
|
attempts INTEGER,
|
|
|
|
envelope TEXT,
|
|
|
|
decrypted TEXT,
|
|
|
|
source TEXT,
|
|
|
|
sourceDevice TEXT,
|
|
|
|
serverTimestamp INTEGER
|
|
|
|
);`);
|
|
|
|
|
|
|
|
await instance.run(`CREATE INDEX unprocessed_id ON unprocessed (
|
|
|
|
id
|
|
|
|
);`);
|
|
|
|
await instance.run(`CREATE INDEX unprocessed_timestamp ON unprocessed (
|
|
|
|
timestamp
|
|
|
|
);`);
|
|
|
|
|
|
|
|
await instance.run(`INSERT INTO unprocessed (
|
|
|
|
id,
|
|
|
|
timestamp,
|
|
|
|
version,
|
|
|
|
attempts,
|
|
|
|
envelope,
|
|
|
|
decrypted,
|
|
|
|
source,
|
|
|
|
sourceDevice,
|
|
|
|
serverTimestamp
|
|
|
|
) SELECT
|
|
|
|
id,
|
|
|
|
timestamp,
|
|
|
|
json_extract(json, '$.version'),
|
|
|
|
json_extract(json, '$.attempts'),
|
|
|
|
json_extract(json, '$.envelope'),
|
|
|
|
json_extract(json, '$.decrypted'),
|
|
|
|
json_extract(json, '$.source'),
|
|
|
|
json_extract(json, '$.sourceDevice'),
|
|
|
|
json_extract(json, '$.serverTimestamp')
|
|
|
|
FROM unprocessed_old;
|
|
|
|
`);
|
|
|
|
|
|
|
|
await instance.run('DROP TABLE unprocessed_old;');
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 10;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
console.log('updateToSchemaVersion10: success!');
|
|
|
|
}
|
|
|
|
|
2019-02-11 23:59:21 +00:00
|
|
|
async function updateToSchemaVersion11(currentVersion, instance) {
|
|
|
|
if (currentVersion >= 11) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log('updateToSchemaVersion11: starting...');
|
|
|
|
await instance.run('BEGIN TRANSACTION;');
|
|
|
|
|
|
|
|
await instance.run('DROP TABLE groups;');
|
|
|
|
|
|
|
|
await instance.run('PRAGMA schema_version = 11;');
|
|
|
|
await instance.run('COMMIT TRANSACTION;');
|
|
|
|
console.log('updateToSchemaVersion11: success!');
|
|
|
|
}
|
|
|
|
|
2018-08-09 01:32:10 +00:00
|
|
|
const SCHEMA_VERSIONS = [
|
|
|
|
updateToSchemaVersion1,
|
|
|
|
updateToSchemaVersion2,
|
|
|
|
updateToSchemaVersion3,
|
2018-09-21 01:47:19 +00:00
|
|
|
updateToSchemaVersion4,
|
2018-11-20 01:38:55 +00:00
|
|
|
() => null, // version 5 was dropped
|
2018-10-18 01:01:21 +00:00
|
|
|
updateToSchemaVersion6,
|
2018-11-20 01:38:55 +00:00
|
|
|
updateToSchemaVersion7,
|
2019-01-14 21:47:19 +00:00
|
|
|
updateToSchemaVersion8,
|
2019-01-30 20:15:07 +00:00
|
|
|
updateToSchemaVersion9,
|
2019-02-05 01:23:50 +00:00
|
|
|
updateToSchemaVersion10,
|
2019-02-11 23:59:21 +00:00
|
|
|
updateToSchemaVersion11,
|
2018-08-09 01:32:10 +00:00
|
|
|
];
|
2018-07-27 01:13:56 +00:00
|
|
|
|
|
|
|
async function updateSchema(instance) {
|
|
|
|
const sqliteVersion = await getSQLiteVersion(instance);
|
|
|
|
const schemaVersion = await getSchemaVersion(instance);
|
|
|
|
const cipherVersion = await getSQLCipherVersion(instance);
|
|
|
|
console.log(
|
|
|
|
'updateSchema:',
|
|
|
|
`Current schema version: ${schemaVersion};`,
|
|
|
|
`Most recent schema version: ${SCHEMA_VERSIONS.length};`,
|
|
|
|
`SQLite version: ${sqliteVersion};`,
|
|
|
|
`SQLCipher version: ${cipherVersion};`
|
|
|
|
);
|
|
|
|
|
|
|
|
for (let index = 0, max = SCHEMA_VERSIONS.length; index < max; index += 1) {
|
|
|
|
const runSchemaUpdate = SCHEMA_VERSIONS[index];
|
|
|
|
|
|
|
|
// Yes, we really want to do this asynchronously, in order
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
await runSchemaUpdate(schemaVersion, instance);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let db;
|
|
|
|
let filePath;
|
2018-11-01 23:40:19 +00:00
|
|
|
let indexedDBPath;
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2019-02-20 01:32:44 +00:00
|
|
|
async function initialize({ configDir, key, messages }) {
|
2018-07-27 01:13:56 +00:00
|
|
|
if (db) {
|
|
|
|
throw new Error('Cannot initialize more than once!');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isString(configDir)) {
|
|
|
|
throw new Error('initialize: configDir is required!');
|
|
|
|
}
|
|
|
|
if (!isString(key)) {
|
2019-02-20 01:32:44 +00:00
|
|
|
throw new Error('initialize: key is required!');
|
|
|
|
}
|
|
|
|
if (!isObject(messages)) {
|
|
|
|
throw new Error('initialize: message is required!');
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
|
2018-11-01 23:40:19 +00:00
|
|
|
indexedDBPath = path.join(configDir, 'IndexedDB');
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
const dbDir = path.join(configDir, 'sql');
|
|
|
|
mkdirp.sync(dbDir);
|
|
|
|
|
|
|
|
filePath = path.join(dbDir, 'db.sqlite');
|
2018-11-01 23:40:19 +00:00
|
|
|
|
2019-02-27 18:17:22 +00:00
|
|
|
try {
|
|
|
|
const sqlInstance = await openDatabase(filePath);
|
|
|
|
const promisified = promisify(sqlInstance);
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2019-02-27 18:17:22 +00:00
|
|
|
// promisified.on('trace', async statement => {
|
|
|
|
// if (!db || statement.startsWith('--')) {
|
|
|
|
// console._log(statement);
|
|
|
|
// return;
|
|
|
|
// }
|
|
|
|
// const data = await db.get(`EXPLAIN QUERY PLAN ${statement}`);
|
|
|
|
// console._log(`EXPLAIN QUERY PLAN ${statement}\n`, data && data.detail);
|
|
|
|
// });
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2019-02-27 18:17:22 +00:00
|
|
|
await setupSQLCipher(promisified, { key });
|
|
|
|
await updateSchema(promisified);
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2019-02-27 18:17:22 +00:00
|
|
|
db = promisified;
|
2019-02-20 01:32:44 +00:00
|
|
|
|
2019-02-27 18:17:22 +00:00
|
|
|
// test database
|
2019-02-20 01:32:44 +00:00
|
|
|
await getMessageCount();
|
|
|
|
} catch (error) {
|
|
|
|
console.log('Database startup error:', error.stack);
|
|
|
|
const buttonIndex = dialog.showMessageBox({
|
|
|
|
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 {
|
|
|
|
await close();
|
|
|
|
await removeDB();
|
|
|
|
removeUserConfig();
|
|
|
|
app.relaunch();
|
|
|
|
}
|
|
|
|
|
|
|
|
app.exit(1);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function close() {
|
|
|
|
const dbRef = db;
|
|
|
|
db = null;
|
|
|
|
await dbRef.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
async function removeDB() {
|
|
|
|
if (db) {
|
|
|
|
throw new Error('removeDB: Cannot erase database when it is open!');
|
|
|
|
}
|
|
|
|
|
|
|
|
rimraf.sync(filePath);
|
|
|
|
}
|
|
|
|
|
2018-11-01 23:40:19 +00:00
|
|
|
async function removeIndexedDBFiles() {
|
|
|
|
if (!indexedDBPath) {
|
|
|
|
throw new Error(
|
|
|
|
'removeIndexedDBFiles: Need to initialize and set indexedDBPath first!'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const pattern = path.join(indexedDBPath, '*.leveldb');
|
|
|
|
rimraf.sync(pattern);
|
|
|
|
indexedDBPath = null;
|
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
const IDENTITY_KEYS_TABLE = 'identityKeys';
|
|
|
|
async function createOrUpdateIdentityKey(data) {
|
|
|
|
return createOrUpdate(IDENTITY_KEYS_TABLE, data);
|
|
|
|
}
|
|
|
|
async function getIdentityKeyById(id) {
|
|
|
|
return getById(IDENTITY_KEYS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function bulkAddIdentityKeys(array) {
|
|
|
|
return bulkAdd(IDENTITY_KEYS_TABLE, array);
|
|
|
|
}
|
|
|
|
async function removeIdentityKeyById(id) {
|
|
|
|
return removeById(IDENTITY_KEYS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function removeAllIdentityKeys() {
|
|
|
|
return removeAllFromTable(IDENTITY_KEYS_TABLE);
|
|
|
|
}
|
2019-02-04 23:54:37 +00:00
|
|
|
async function getAllIdentityKeys() {
|
|
|
|
return getAllFromTable(IDENTITY_KEYS_TABLE);
|
|
|
|
}
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
const PRE_KEYS_TABLE = 'preKeys';
|
|
|
|
async function createOrUpdatePreKey(data) {
|
|
|
|
return createOrUpdate(PRE_KEYS_TABLE, data);
|
|
|
|
}
|
|
|
|
async function getPreKeyById(id) {
|
|
|
|
return getById(PRE_KEYS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function bulkAddPreKeys(array) {
|
|
|
|
return bulkAdd(PRE_KEYS_TABLE, array);
|
|
|
|
}
|
|
|
|
async function removePreKeyById(id) {
|
|
|
|
return removeById(PRE_KEYS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function removeAllPreKeys() {
|
|
|
|
return removeAllFromTable(PRE_KEYS_TABLE);
|
|
|
|
}
|
2019-02-04 23:54:37 +00:00
|
|
|
async function getAllPreKeys() {
|
|
|
|
return getAllFromTable(PRE_KEYS_TABLE);
|
|
|
|
}
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys';
|
|
|
|
async function createOrUpdateSignedPreKey(data) {
|
|
|
|
return createOrUpdate(SIGNED_PRE_KEYS_TABLE, data);
|
|
|
|
}
|
|
|
|
async function getSignedPreKeyById(id) {
|
|
|
|
return getById(SIGNED_PRE_KEYS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function getAllSignedPreKeys() {
|
|
|
|
const rows = await db.all('SELECT json FROM signedPreKeys ORDER BY id ASC;');
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
async function bulkAddSignedPreKeys(array) {
|
|
|
|
return bulkAdd(SIGNED_PRE_KEYS_TABLE, array);
|
|
|
|
}
|
|
|
|
async function removeSignedPreKeyById(id) {
|
|
|
|
return removeById(SIGNED_PRE_KEYS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function removeAllSignedPreKeys() {
|
|
|
|
return removeAllFromTable(SIGNED_PRE_KEYS_TABLE);
|
|
|
|
}
|
|
|
|
|
|
|
|
const ITEMS_TABLE = 'items';
|
|
|
|
async function createOrUpdateItem(data) {
|
|
|
|
return createOrUpdate(ITEMS_TABLE, data);
|
|
|
|
}
|
|
|
|
async function getItemById(id) {
|
|
|
|
return getById(ITEMS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function getAllItems() {
|
|
|
|
const rows = await db.all('SELECT json FROM items ORDER BY id ASC;');
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
async function bulkAddItems(array) {
|
|
|
|
return bulkAdd(ITEMS_TABLE, array);
|
|
|
|
}
|
|
|
|
async function removeItemById(id) {
|
|
|
|
return removeById(ITEMS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function removeAllItems() {
|
|
|
|
return removeAllFromTable(ITEMS_TABLE);
|
|
|
|
}
|
|
|
|
|
|
|
|
const SESSIONS_TABLE = 'sessions';
|
|
|
|
async function createOrUpdateSession(data) {
|
|
|
|
const { id, number } = data;
|
|
|
|
if (!id) {
|
|
|
|
throw new Error(
|
|
|
|
'createOrUpdateSession: Provided data did not have a truthy id'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (!number) {
|
|
|
|
throw new Error(
|
|
|
|
'createOrUpdateSession: Provided data did not have a truthy number'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
await db.run(
|
|
|
|
`INSERT OR REPLACE INTO sessions (
|
|
|
|
id,
|
|
|
|
number,
|
|
|
|
json
|
|
|
|
) values (
|
|
|
|
$id,
|
|
|
|
$number,
|
|
|
|
$json
|
|
|
|
)`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$number: number,
|
|
|
|
$json: objectToJSON(data),
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
async function getSessionById(id) {
|
|
|
|
return getById(SESSIONS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function getSessionsByNumber(number) {
|
|
|
|
const rows = await db.all('SELECT * FROM sessions WHERE number = $number;', {
|
|
|
|
$number: number,
|
|
|
|
});
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
async function bulkAddSessions(array) {
|
|
|
|
return bulkAdd(SESSIONS_TABLE, array);
|
|
|
|
}
|
|
|
|
async function removeSessionById(id) {
|
|
|
|
return removeById(SESSIONS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function removeSessionsByNumber(number) {
|
|
|
|
await db.run('DELETE FROM sessions WHERE number = $number;', {
|
|
|
|
$number: number,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
async function removeAllSessions() {
|
|
|
|
return removeAllFromTable(SESSIONS_TABLE);
|
|
|
|
}
|
2019-02-04 23:54:37 +00:00
|
|
|
async function getAllSessions() {
|
|
|
|
return getAllFromTable(SESSIONS_TABLE);
|
|
|
|
}
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
async function createOrUpdate(table, data) {
|
|
|
|
const { id } = data;
|
|
|
|
if (!id) {
|
|
|
|
throw new Error('createOrUpdate: Provided data did not have a truthy id');
|
|
|
|
}
|
|
|
|
|
|
|
|
await db.run(
|
|
|
|
`INSERT OR REPLACE INTO ${table} (
|
|
|
|
id,
|
|
|
|
json
|
|
|
|
) values (
|
|
|
|
$id,
|
|
|
|
$json
|
|
|
|
)`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$json: objectToJSON(data),
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function bulkAdd(table, array) {
|
|
|
|
let promise;
|
|
|
|
|
|
|
|
db.serialize(() => {
|
|
|
|
promise = Promise.all([
|
|
|
|
db.run('BEGIN TRANSACTION;'),
|
|
|
|
...map(array, data => createOrUpdate(table, data)),
|
|
|
|
db.run('COMMIT TRANSACTION;'),
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
await promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getById(table, id) {
|
|
|
|
const row = await db.get(`SELECT * FROM ${table} WHERE id = $id;`, {
|
|
|
|
$id: id,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!row) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return jsonToObject(row.json);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function removeById(table, id) {
|
|
|
|
if (!Array.isArray(id)) {
|
|
|
|
await db.run(`DELETE FROM ${table} WHERE id = $id;`, { $id: id });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!id.length) {
|
|
|
|
throw new Error('removeById: No ids to delete!');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Our node interface doesn't seem to allow you to replace one single ? with an array
|
|
|
|
await db.run(
|
|
|
|
`DELETE FROM ${table} WHERE id IN ( ${id.map(() => '?').join(', ')} );`,
|
|
|
|
id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function removeAllFromTable(table) {
|
|
|
|
await db.run(`DELETE FROM ${table};`);
|
|
|
|
}
|
|
|
|
|
2019-02-04 23:54:37 +00:00
|
|
|
async function getAllFromTable(table) {
|
|
|
|
const rows = await db.all(`SELECT json FROM ${table};`);
|
|
|
|
return rows.map(row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
// Conversations
|
|
|
|
|
2018-09-21 01:47:19 +00:00
|
|
|
async function getConversationCount() {
|
|
|
|
const row = await db.get('SELECT count(*) from conversations;');
|
|
|
|
|
|
|
|
if (!row) {
|
2019-02-20 01:32:44 +00:00
|
|
|
throw new Error(
|
|
|
|
'getConversationCount: Unable to get count of conversations'
|
|
|
|
);
|
2018-09-21 01:47:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return row['count(*)'];
|
|
|
|
}
|
|
|
|
|
|
|
|
async function saveConversation(data) {
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
const { id, active_at, type, members, name, profileName } = data;
|
|
|
|
|
|
|
|
await db.run(
|
|
|
|
`INSERT INTO conversations (
|
|
|
|
id,
|
|
|
|
json,
|
|
|
|
|
|
|
|
active_at,
|
|
|
|
type,
|
|
|
|
members,
|
|
|
|
name,
|
|
|
|
profileName
|
|
|
|
) values (
|
|
|
|
$id,
|
|
|
|
$json,
|
|
|
|
|
|
|
|
$active_at,
|
|
|
|
$type,
|
|
|
|
$members,
|
|
|
|
$name,
|
|
|
|
$profileName
|
|
|
|
);`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$json: objectToJSON(data),
|
|
|
|
|
|
|
|
$active_at: active_at,
|
|
|
|
$type: type,
|
|
|
|
$members: members ? members.join(' ') : null,
|
|
|
|
$name: name,
|
|
|
|
$profileName: profileName,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function saveConversations(arrayOfConversations) {
|
|
|
|
let promise;
|
|
|
|
|
|
|
|
db.serialize(() => {
|
|
|
|
promise = Promise.all([
|
|
|
|
db.run('BEGIN TRANSACTION;'),
|
|
|
|
...map(arrayOfConversations, conversation =>
|
|
|
|
saveConversation(conversation)
|
|
|
|
),
|
|
|
|
db.run('COMMIT TRANSACTION;'),
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
await promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function updateConversation(data) {
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
const { id, active_at, type, members, name, profileName } = data;
|
|
|
|
|
|
|
|
await db.run(
|
|
|
|
`UPDATE conversations SET
|
|
|
|
json = $json,
|
|
|
|
|
|
|
|
active_at = $active_at,
|
|
|
|
type = $type,
|
|
|
|
members = $members,
|
|
|
|
name = $name,
|
|
|
|
profileName = $profileName
|
|
|
|
WHERE id = $id;`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$json: objectToJSON(data),
|
|
|
|
|
|
|
|
$active_at: active_at,
|
|
|
|
$type: type,
|
|
|
|
$members: members ? members.join(' ') : null,
|
|
|
|
$name: name,
|
|
|
|
$profileName: profileName,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function removeConversation(id) {
|
|
|
|
if (!Array.isArray(id)) {
|
|
|
|
await db.run('DELETE FROM conversations WHERE id = $id;', { $id: id });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!id.length) {
|
|
|
|
throw new Error('removeConversation: No ids to delete!');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Our node interface doesn't seem to allow you to replace one single ? with an array
|
|
|
|
await db.run(
|
|
|
|
`DELETE FROM conversations WHERE id IN ( ${id
|
|
|
|
.map(() => '?')
|
|
|
|
.join(', ')} );`,
|
|
|
|
id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getConversationById(id) {
|
|
|
|
const row = await db.get('SELECT * FROM conversations WHERE id = $id;', {
|
|
|
|
$id: id,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!row) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return jsonToObject(row.json);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getAllConversations() {
|
|
|
|
const rows = await db.all('SELECT json FROM conversations ORDER BY id ASC;');
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getAllConversationIds() {
|
|
|
|
const rows = await db.all('SELECT id FROM conversations ORDER BY id ASC;');
|
|
|
|
return map(rows, row => row.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getAllPrivateConversations() {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM conversations WHERE
|
|
|
|
type = 'private'
|
|
|
|
ORDER BY id ASC;`
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getAllGroupsInvolvingId(id) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM conversations WHERE
|
|
|
|
type = 'group' AND
|
|
|
|
members LIKE $id
|
|
|
|
ORDER BY id ASC;`,
|
|
|
|
{
|
|
|
|
$id: `%${id}%`,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
2019-01-14 21:49:58 +00:00
|
|
|
async function searchConversations(query, { limit } = {}) {
|
2018-09-21 01:47:19 +00:00
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM conversations WHERE
|
2019-01-14 21:47:19 +00:00
|
|
|
(
|
|
|
|
id LIKE $id OR
|
|
|
|
name LIKE $name OR
|
|
|
|
profileName LIKE $profileName
|
|
|
|
)
|
2019-01-14 21:49:58 +00:00
|
|
|
ORDER BY id ASC
|
|
|
|
LIMIT $limit`,
|
2018-09-21 01:47:19 +00:00
|
|
|
{
|
|
|
|
$id: `%${query}%`,
|
|
|
|
$name: `%${query}%`,
|
|
|
|
$profileName: `%${query}%`,
|
2019-01-14 21:49:58 +00:00
|
|
|
$limit: limit || 50,
|
2018-09-21 01:47:19 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
2019-01-14 21:47:19 +00:00
|
|
|
async function searchMessages(query, { limit } = {}) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT
|
|
|
|
messages.json,
|
|
|
|
snippet(messages_fts, -1, '<<left>>', '<<right>>', '...', 15) as snippet
|
|
|
|
FROM messages_fts
|
|
|
|
INNER JOIN messages on messages_fts.id = messages.id
|
|
|
|
WHERE
|
|
|
|
messages_fts match $query
|
|
|
|
ORDER BY messages.received_at DESC
|
|
|
|
LIMIT $limit;`,
|
|
|
|
{
|
|
|
|
$query: query,
|
|
|
|
$limit: limit || 100,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => ({
|
|
|
|
...jsonToObject(row.json),
|
|
|
|
snippet: row.snippet,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function searchMessagesInConversation(
|
|
|
|
query,
|
|
|
|
conversationId,
|
|
|
|
{ limit } = {}
|
|
|
|
) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT
|
|
|
|
messages.json,
|
|
|
|
snippet(messages_fts, -1, '<<left>>', '<<right>>', '...', 15) as snippet
|
|
|
|
FROM messages_fts
|
|
|
|
INNER JOIN messages on messages_fts.id = messages.id
|
|
|
|
WHERE
|
|
|
|
messages_fts match $query AND
|
|
|
|
messages.conversationId = $conversationId
|
|
|
|
ORDER BY messages.received_at DESC
|
|
|
|
LIMIT $limit;`,
|
|
|
|
{
|
|
|
|
$query: query,
|
|
|
|
$conversationId: conversationId,
|
|
|
|
$limit: limit || 100,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => ({
|
|
|
|
...jsonToObject(row.json),
|
|
|
|
snippet: row.snippet,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2018-08-06 23:18:58 +00:00
|
|
|
async function getMessageCount() {
|
|
|
|
const row = await db.get('SELECT count(*) from messages;');
|
|
|
|
|
|
|
|
if (!row) {
|
|
|
|
throw new Error('getMessageCount: Unable to get count of messages');
|
|
|
|
}
|
|
|
|
|
|
|
|
return row['count(*)'];
|
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function saveMessage(data, { forceSave } = {}) {
|
|
|
|
const {
|
2019-01-14 21:47:19 +00:00
|
|
|
body,
|
2018-07-27 01:13:56 +00:00
|
|
|
conversationId,
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
expires_at,
|
|
|
|
hasAttachments,
|
|
|
|
hasFileAttachments,
|
|
|
|
hasVisualMediaAttachments,
|
|
|
|
id,
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
received_at,
|
|
|
|
schemaVersion,
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
sent_at,
|
|
|
|
source,
|
|
|
|
sourceDevice,
|
2018-08-07 19:33:56 +00:00
|
|
|
type,
|
2018-07-27 01:13:56 +00:00
|
|
|
unread,
|
2018-08-07 19:33:56 +00:00
|
|
|
expireTimer,
|
|
|
|
expirationStartTimestamp,
|
2018-07-27 01:13:56 +00:00
|
|
|
} = data;
|
|
|
|
|
2018-08-07 19:33:56 +00:00
|
|
|
const payload = {
|
|
|
|
$id: id,
|
|
|
|
$json: objectToJSON(data),
|
|
|
|
|
2019-01-14 21:47:19 +00:00
|
|
|
$body: body,
|
2018-08-07 19:33:56 +00:00
|
|
|
$conversationId: conversationId,
|
|
|
|
$expirationStartTimestamp: expirationStartTimestamp,
|
|
|
|
$expires_at: expires_at,
|
|
|
|
$expireTimer: expireTimer,
|
|
|
|
$hasAttachments: hasAttachments,
|
|
|
|
$hasFileAttachments: hasFileAttachments,
|
|
|
|
$hasVisualMediaAttachments: hasVisualMediaAttachments,
|
|
|
|
$received_at: received_at,
|
|
|
|
$schemaVersion: schemaVersion,
|
|
|
|
$sent_at: sent_at,
|
|
|
|
$source: source,
|
|
|
|
$sourceDevice: sourceDevice,
|
|
|
|
$type: type,
|
|
|
|
$unread: unread,
|
|
|
|
};
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
if (id && !forceSave) {
|
|
|
|
await db.run(
|
|
|
|
`UPDATE messages SET
|
|
|
|
json = $json,
|
2019-01-14 21:47:19 +00:00
|
|
|
body = $body,
|
2018-07-27 01:13:56 +00:00
|
|
|
conversationId = $conversationId,
|
2018-08-07 19:33:56 +00:00
|
|
|
expirationStartTimestamp = $expirationStartTimestamp,
|
2018-07-27 01:13:56 +00:00
|
|
|
expires_at = $expires_at,
|
2018-08-07 19:33:56 +00:00
|
|
|
expireTimer = $expireTimer,
|
2018-07-27 01:13:56 +00:00
|
|
|
hasAttachments = $hasAttachments,
|
|
|
|
hasFileAttachments = $hasFileAttachments,
|
|
|
|
hasVisualMediaAttachments = $hasVisualMediaAttachments,
|
|
|
|
id = $id,
|
|
|
|
received_at = $received_at,
|
|
|
|
schemaVersion = $schemaVersion,
|
|
|
|
sent_at = $sent_at,
|
|
|
|
source = $source,
|
|
|
|
sourceDevice = $sourceDevice,
|
2018-08-07 19:33:56 +00:00
|
|
|
type = $type,
|
2018-07-27 01:13:56 +00:00
|
|
|
unread = $unread
|
|
|
|
WHERE id = $id;`,
|
2018-08-07 19:33:56 +00:00
|
|
|
payload
|
2018-07-27 01:13:56 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
const toCreate = {
|
|
|
|
...data,
|
|
|
|
id: id || generateUUID(),
|
|
|
|
};
|
|
|
|
|
|
|
|
await db.run(
|
|
|
|
`INSERT INTO messages (
|
|
|
|
id,
|
|
|
|
json,
|
|
|
|
|
2019-01-14 21:47:19 +00:00
|
|
|
body,
|
2018-07-27 01:13:56 +00:00
|
|
|
conversationId,
|
2018-08-07 19:33:56 +00:00
|
|
|
expirationStartTimestamp,
|
2018-07-27 01:13:56 +00:00
|
|
|
expires_at,
|
2018-08-07 19:33:56 +00:00
|
|
|
expireTimer,
|
2018-07-27 01:13:56 +00:00
|
|
|
hasAttachments,
|
|
|
|
hasFileAttachments,
|
|
|
|
hasVisualMediaAttachments,
|
|
|
|
received_at,
|
|
|
|
schemaVersion,
|
|
|
|
sent_at,
|
|
|
|
source,
|
|
|
|
sourceDevice,
|
2018-08-07 19:33:56 +00:00
|
|
|
type,
|
2018-07-27 01:13:56 +00:00
|
|
|
unread
|
|
|
|
) values (
|
|
|
|
$id,
|
|
|
|
$json,
|
|
|
|
|
2019-01-14 21:47:19 +00:00
|
|
|
$body,
|
2018-07-27 01:13:56 +00:00
|
|
|
$conversationId,
|
2018-08-07 19:33:56 +00:00
|
|
|
$expirationStartTimestamp,
|
2018-07-27 01:13:56 +00:00
|
|
|
$expires_at,
|
2018-08-07 19:33:56 +00:00
|
|
|
$expireTimer,
|
2018-07-27 01:13:56 +00:00
|
|
|
$hasAttachments,
|
|
|
|
$hasFileAttachments,
|
|
|
|
$hasVisualMediaAttachments,
|
|
|
|
$received_at,
|
|
|
|
$schemaVersion,
|
|
|
|
$sent_at,
|
|
|
|
$source,
|
|
|
|
$sourceDevice,
|
2018-08-07 19:33:56 +00:00
|
|
|
$type,
|
2018-07-27 01:13:56 +00:00
|
|
|
$unread
|
|
|
|
);`,
|
|
|
|
{
|
2018-08-07 19:33:56 +00:00
|
|
|
...payload,
|
2018-07-27 01:13:56 +00:00
|
|
|
$id: toCreate.id,
|
|
|
|
$json: objectToJSON(toCreate),
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return toCreate.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function saveMessages(arrayOfMessages, { forceSave } = {}) {
|
2018-08-01 02:29:51 +00:00
|
|
|
let promise;
|
|
|
|
|
|
|
|
db.serialize(() => {
|
|
|
|
promise = Promise.all([
|
|
|
|
db.run('BEGIN TRANSACTION;'),
|
|
|
|
...map(arrayOfMessages, message => saveMessage(message, { forceSave })),
|
|
|
|
db.run('COMMIT TRANSACTION;'),
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
await promise;
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function removeMessage(id) {
|
|
|
|
if (!Array.isArray(id)) {
|
|
|
|
await db.run('DELETE FROM messages WHERE id = $id;', { $id: id });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!id.length) {
|
|
|
|
throw new Error('removeMessages: No ids to delete!');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Our node interface doesn't seem to allow you to replace one single ? with an array
|
|
|
|
await db.run(
|
|
|
|
`DELETE FROM messages WHERE id IN ( ${id.map(() => '?').join(', ')} );`,
|
|
|
|
id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessageById(id) {
|
|
|
|
const row = await db.get('SELECT * FROM messages WHERE id = $id;', {
|
|
|
|
$id: id,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!row) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return jsonToObject(row.json);
|
|
|
|
}
|
|
|
|
|
2018-09-21 01:47:19 +00:00
|
|
|
async function getAllMessages() {
|
|
|
|
const rows = await db.all('SELECT json FROM messages ORDER BY id ASC;');
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getAllMessageIds() {
|
|
|
|
const rows = await db.all('SELECT id FROM messages ORDER BY id ASC;');
|
|
|
|
return map(rows, row => row.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
async function getMessageBySender({ source, sourceDevice, sent_at }) {
|
2018-08-02 18:19:50 +00:00
|
|
|
const rows = await db.all(
|
2018-07-27 01:13:56 +00:00
|
|
|
`SELECT json FROM messages WHERE
|
|
|
|
source = $source AND
|
|
|
|
sourceDevice = $sourceDevice AND
|
|
|
|
sent_at = $sent_at;`,
|
|
|
|
{
|
|
|
|
$source: source,
|
|
|
|
$sourceDevice: sourceDevice,
|
|
|
|
$sent_at: sent_at,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getUnreadByConversation(conversationId) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM messages WHERE
|
2018-08-09 01:32:10 +00:00
|
|
|
unread = $unread AND
|
|
|
|
conversationId = $conversationId
|
2018-07-27 01:13:56 +00:00
|
|
|
ORDER BY received_at DESC;`,
|
|
|
|
{
|
|
|
|
$unread: 1,
|
2018-08-09 01:32:10 +00:00
|
|
|
$conversationId: conversationId,
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessagesByConversation(
|
|
|
|
conversationId,
|
|
|
|
{ limit = 100, receivedAt = Number.MAX_VALUE } = {}
|
|
|
|
) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM messages WHERE
|
|
|
|
conversationId = $conversationId AND
|
|
|
|
received_at < $received_at
|
|
|
|
ORDER BY received_at DESC
|
|
|
|
LIMIT $limit;`,
|
|
|
|
{
|
|
|
|
$conversationId: conversationId,
|
|
|
|
$received_at: receivedAt,
|
|
|
|
$limit: limit,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessagesBySentAt(sentAt) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT * FROM messages
|
|
|
|
WHERE sent_at = $sent_at
|
|
|
|
ORDER BY received_at DESC;`,
|
|
|
|
{
|
|
|
|
$sent_at: sentAt,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getExpiredMessages() {
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM messages WHERE
|
|
|
|
expires_at IS NOT NULL AND
|
|
|
|
expires_at <= $expires_at
|
|
|
|
ORDER BY expires_at ASC;`,
|
|
|
|
{
|
|
|
|
$expires_at: now,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
2018-08-07 19:33:56 +00:00
|
|
|
async function getOutgoingWithoutExpiresAt() {
|
|
|
|
const rows = await db.all(`
|
|
|
|
SELECT json FROM messages
|
|
|
|
WHERE
|
2018-08-09 01:32:10 +00:00
|
|
|
expireTimer > 0 AND
|
|
|
|
expires_at IS NULL AND
|
|
|
|
type IS 'outgoing'
|
2018-08-07 19:33:56 +00:00
|
|
|
ORDER BY expires_at ASC;
|
|
|
|
`);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getNextExpiringMessage() {
|
|
|
|
const rows = await db.all(`
|
|
|
|
SELECT json FROM messages
|
2018-08-09 01:32:10 +00:00
|
|
|
WHERE expires_at > 0
|
2018-07-27 01:13:56 +00:00
|
|
|
ORDER BY expires_at ASC
|
|
|
|
LIMIT 1;
|
|
|
|
`);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function saveUnprocessed(data, { forceSave } = {}) {
|
2019-02-05 01:23:50 +00:00
|
|
|
const { id, timestamp, version, attempts, envelope } = data;
|
|
|
|
if (!id) {
|
|
|
|
throw new Error('saveUnprocessed: id was falsey');
|
|
|
|
}
|
2018-07-27 01:13:56 +00:00
|
|
|
|
|
|
|
if (forceSave) {
|
|
|
|
await db.run(
|
|
|
|
`INSERT INTO unprocessed (
|
|
|
|
id,
|
|
|
|
timestamp,
|
2019-02-05 01:23:50 +00:00
|
|
|
version,
|
|
|
|
attempts,
|
|
|
|
envelope
|
2018-07-27 01:13:56 +00:00
|
|
|
) values (
|
|
|
|
$id,
|
|
|
|
$timestamp,
|
2019-02-05 01:23:50 +00:00
|
|
|
$version,
|
|
|
|
$attempts,
|
|
|
|
$envelope
|
2018-07-27 01:13:56 +00:00
|
|
|
);`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$timestamp: timestamp,
|
2019-02-05 01:23:50 +00:00
|
|
|
$version: version,
|
|
|
|
$attempts: attempts,
|
|
|
|
$envelope: envelope,
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
await db.run(
|
|
|
|
`UPDATE unprocessed SET
|
2019-02-05 01:23:50 +00:00
|
|
|
timestamp = $timestamp,
|
|
|
|
version = $version,
|
|
|
|
attempts = $attempts,
|
|
|
|
envelope = $envelope
|
2018-07-27 01:13:56 +00:00
|
|
|
WHERE id = $id;`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$timestamp: timestamp,
|
2019-02-05 01:23:50 +00:00
|
|
|
$version: version,
|
|
|
|
$attempts: attempts,
|
|
|
|
$envelope: envelope,
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function saveUnprocesseds(arrayOfUnprocessed, { forceSave } = {}) {
|
2018-08-01 02:29:51 +00:00
|
|
|
let promise;
|
|
|
|
|
|
|
|
db.serialize(() => {
|
|
|
|
promise = Promise.all([
|
|
|
|
db.run('BEGIN TRANSACTION;'),
|
|
|
|
...map(arrayOfUnprocessed, unprocessed =>
|
|
|
|
saveUnprocessed(unprocessed, { forceSave })
|
|
|
|
),
|
|
|
|
db.run('COMMIT TRANSACTION;'),
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
await promise;
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
|
2019-02-05 01:23:50 +00:00
|
|
|
async function updateUnprocessedAttempts(id, attempts) {
|
|
|
|
await db.run('UPDATE unprocessed SET attempts = $attempts WHERE id = $id;', {
|
2018-07-27 01:13:56 +00:00
|
|
|
$id: id,
|
2019-02-05 01:23:50 +00:00
|
|
|
$attempts: attempts,
|
2018-07-27 01:13:56 +00:00
|
|
|
});
|
2019-02-05 01:23:50 +00:00
|
|
|
}
|
|
|
|
async function updateUnprocessedWithData(id, data = {}) {
|
|
|
|
const { source, sourceDevice, serverTimestamp, decrypted } = data;
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2019-02-05 01:23:50 +00:00
|
|
|
await db.run(
|
|
|
|
`UPDATE unprocessed SET
|
|
|
|
source = $source,
|
|
|
|
sourceDevice = $sourceDevice,
|
|
|
|
serverTimestamp = $serverTimestamp,
|
|
|
|
decrypted = $decrypted
|
|
|
|
WHERE id = $id;`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$source: source,
|
|
|
|
$sourceDevice: sourceDevice,
|
|
|
|
$serverTimestamp: serverTimestamp,
|
|
|
|
$decrypted: decrypted,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2019-02-05 01:23:50 +00:00
|
|
|
async function getUnprocessedById(id) {
|
|
|
|
const row = await db.get('SELECT * FROM unprocessed WHERE id = $id;', {
|
|
|
|
$id: id,
|
|
|
|
});
|
|
|
|
|
|
|
|
return row;
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
|
2018-09-28 22:51:26 +00:00
|
|
|
async function getUnprocessedCount() {
|
|
|
|
const row = await db.get('SELECT count(*) from unprocessed;');
|
|
|
|
|
|
|
|
if (!row) {
|
|
|
|
throw new Error('getMessageCount: Unable to get count of unprocessed');
|
|
|
|
}
|
|
|
|
|
|
|
|
return row['count(*)'];
|
|
|
|
}
|
|
|
|
|
2018-07-27 01:13:56 +00:00
|
|
|
async function getAllUnprocessed() {
|
|
|
|
const rows = await db.all(
|
2019-02-05 01:23:50 +00:00
|
|
|
'SELECT * FROM unprocessed ORDER BY timestamp ASC;'
|
2018-07-27 01:13:56 +00:00
|
|
|
);
|
|
|
|
|
2019-02-05 01:23:50 +00:00
|
|
|
return rows;
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function removeUnprocessed(id) {
|
|
|
|
if (!Array.isArray(id)) {
|
|
|
|
await db.run('DELETE FROM unprocessed WHERE id = $id;', { $id: id });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!id.length) {
|
|
|
|
throw new Error('removeUnprocessed: No ids to delete!');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Our node interface doesn't seem to allow you to replace one single ? with an array
|
|
|
|
await db.run(
|
|
|
|
`DELETE FROM unprocessed WHERE id IN ( ${id.map(() => '?').join(', ')} );`,
|
|
|
|
id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function removeAllUnprocessed() {
|
|
|
|
await db.run('DELETE FROM unprocessed;');
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
const ATTACHMENT_DOWNLOADS_TABLE = 'attachment_downloads';
|
|
|
|
async function getNextAttachmentDownloadJobs(limit, options = {}) {
|
|
|
|
const timestamp = options.timestamp || Date.now();
|
|
|
|
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM attachment_downloads
|
|
|
|
WHERE pending = 0 AND timestamp < $timestamp
|
|
|
|
ORDER BY timestamp DESC
|
|
|
|
LIMIT $limit;`,
|
|
|
|
{
|
|
|
|
$limit: limit,
|
|
|
|
$timestamp: timestamp,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
async function saveAttachmentDownloadJob(job) {
|
|
|
|
const { id, pending, timestamp } = job;
|
|
|
|
if (!id) {
|
|
|
|
throw new Error(
|
|
|
|
'saveAttachmentDownloadJob: Provided job did not have a truthy id'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
await db.run(
|
|
|
|
`INSERT OR REPLACE INTO attachment_downloads (
|
|
|
|
id,
|
|
|
|
pending,
|
|
|
|
timestamp,
|
|
|
|
json
|
|
|
|
) values (
|
|
|
|
$id,
|
|
|
|
$pending,
|
|
|
|
$timestamp,
|
|
|
|
$json
|
|
|
|
)`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$pending: pending,
|
|
|
|
$timestamp: timestamp,
|
|
|
|
$json: objectToJSON(job),
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
async function setAttachmentDownloadJobPending(id, pending) {
|
|
|
|
await db.run(
|
|
|
|
'UPDATE attachment_downloads SET pending = $pending WHERE id = $id;',
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$pending: pending,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
async function resetAttachmentDownloadPending() {
|
|
|
|
await db.run(
|
|
|
|
'UPDATE attachment_downloads SET pending = 0 WHERE pending != 0;'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
async function removeAttachmentDownloadJob(id) {
|
|
|
|
return removeById(ATTACHMENT_DOWNLOADS_TABLE, id);
|
|
|
|
}
|
|
|
|
async function removeAllAttachmentDownloadJobs() {
|
|
|
|
return removeAllFromTable(ATTACHMENT_DOWNLOADS_TABLE);
|
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
// All data in database
|
2018-07-27 01:13:56 +00:00
|
|
|
async function removeAll() {
|
2018-08-01 02:29:51 +00:00
|
|
|
let promise;
|
|
|
|
|
|
|
|
db.serialize(() => {
|
|
|
|
promise = Promise.all([
|
|
|
|
db.run('BEGIN TRANSACTION;'),
|
2018-10-18 01:01:21 +00:00
|
|
|
db.run('DELETE FROM conversations;'),
|
|
|
|
db.run('DELETE FROM identityKeys;'),
|
|
|
|
db.run('DELETE FROM items;'),
|
2018-08-01 02:29:51 +00:00
|
|
|
db.run('DELETE FROM messages;'),
|
2018-10-18 01:01:21 +00:00
|
|
|
db.run('DELETE FROM preKeys;'),
|
|
|
|
db.run('DELETE FROM sessions;'),
|
|
|
|
db.run('DELETE FROM signedPreKeys;'),
|
|
|
|
db.run('DELETE FROM unprocessed;'),
|
2019-01-30 20:15:07 +00:00
|
|
|
db.run('DELETE FROM attachment_downloads;'),
|
|
|
|
db.run('DELETE FROM messages_fts;'),
|
2018-10-18 01:01:21 +00:00
|
|
|
db.run('COMMIT TRANSACTION;'),
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
await promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Anything that isn't user-visible data
|
|
|
|
async function removeAllConfiguration() {
|
|
|
|
let promise;
|
|
|
|
|
|
|
|
db.serialize(() => {
|
|
|
|
promise = Promise.all([
|
|
|
|
db.run('BEGIN TRANSACTION;'),
|
|
|
|
db.run('DELETE FROM identityKeys;'),
|
|
|
|
db.run('DELETE FROM items;'),
|
|
|
|
db.run('DELETE FROM preKeys;'),
|
|
|
|
db.run('DELETE FROM sessions;'),
|
|
|
|
db.run('DELETE FROM signedPreKeys;'),
|
2018-08-01 02:29:51 +00:00
|
|
|
db.run('DELETE FROM unprocessed;'),
|
|
|
|
db.run('COMMIT TRANSACTION;'),
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
await promise;
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessagesNeedingUpgrade(limit, { maxVersion }) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM messages
|
2018-08-09 01:32:10 +00:00
|
|
|
WHERE schemaVersion IS NULL OR schemaVersion < $maxVersion
|
2018-07-27 01:13:56 +00:00
|
|
|
LIMIT $limit;`,
|
|
|
|
{
|
|
|
|
$maxVersion: maxVersion,
|
|
|
|
$limit: limit,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessagesWithVisualMediaAttachments(
|
|
|
|
conversationId,
|
|
|
|
{ limit }
|
|
|
|
) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM messages WHERE
|
|
|
|
conversationId = $conversationId AND
|
|
|
|
hasVisualMediaAttachments = 1
|
|
|
|
ORDER BY received_at DESC
|
|
|
|
LIMIT $limit;`,
|
|
|
|
{
|
|
|
|
$conversationId: conversationId,
|
|
|
|
$limit: limit,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getMessagesWithFileAttachments(conversationId, { limit }) {
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM messages WHERE
|
|
|
|
conversationId = $conversationId AND
|
|
|
|
hasFileAttachments = 1
|
|
|
|
ORDER BY received_at DESC
|
|
|
|
LIMIT $limit;`,
|
|
|
|
{
|
|
|
|
$conversationId: conversationId,
|
|
|
|
$limit: limit,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return map(rows, row => jsonToObject(row.json));
|
|
|
|
}
|
2018-08-06 23:18:58 +00:00
|
|
|
|
|
|
|
function getExternalFilesForMessage(message) {
|
2019-01-16 03:03:56 +00:00
|
|
|
const { attachments, contact, quote, preview } = message;
|
2018-08-06 23:18:58 +00:00
|
|
|
const files = [];
|
|
|
|
|
|
|
|
forEach(attachments, attachment => {
|
|
|
|
const { path: file, thumbnail, screenshot } = attachment;
|
|
|
|
if (file) {
|
|
|
|
files.push(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (thumbnail && thumbnail.path) {
|
|
|
|
files.push(thumbnail.path);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (screenshot && screenshot.path) {
|
|
|
|
files.push(screenshot.path);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (quote && quote.attachments && quote.attachments.length) {
|
|
|
|
forEach(quote.attachments, attachment => {
|
|
|
|
const { thumbnail } = attachment;
|
|
|
|
|
|
|
|
if (thumbnail && thumbnail.path) {
|
|
|
|
files.push(thumbnail.path);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (contact && contact.length) {
|
|
|
|
forEach(contact, item => {
|
|
|
|
const { avatar } = item;
|
|
|
|
|
|
|
|
if (avatar && avatar.avatar && avatar.avatar.path) {
|
|
|
|
files.push(avatar.avatar.path);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-01-16 03:03:56 +00:00
|
|
|
if (preview && preview.length) {
|
|
|
|
forEach(preview, item => {
|
|
|
|
const { image } = item;
|
|
|
|
|
|
|
|
if (image && image.path) {
|
|
|
|
files.push(image.path);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-08-06 23:18:58 +00:00
|
|
|
return files;
|
|
|
|
}
|
|
|
|
|
2018-09-21 01:47:19 +00:00
|
|
|
function getExternalFilesForConversation(conversation) {
|
|
|
|
const { avatar, profileAvatar } = conversation;
|
|
|
|
const files = [];
|
|
|
|
|
|
|
|
if (avatar && avatar.path) {
|
|
|
|
files.push(avatar.path);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (profileAvatar && profileAvatar.path) {
|
|
|
|
files.push(profileAvatar.path);
|
|
|
|
}
|
|
|
|
|
|
|
|
return files;
|
|
|
|
}
|
|
|
|
|
2018-08-06 23:18:58 +00:00
|
|
|
async function removeKnownAttachments(allAttachments) {
|
|
|
|
const lookup = fromPairs(map(allAttachments, file => [file, true]));
|
|
|
|
const chunkSize = 50;
|
|
|
|
|
|
|
|
const total = await getMessageCount();
|
|
|
|
console.log(
|
|
|
|
`removeKnownAttachments: About to iterate through ${total} messages`
|
|
|
|
);
|
|
|
|
|
|
|
|
let count = 0;
|
|
|
|
let complete = false;
|
|
|
|
let id = '';
|
|
|
|
|
|
|
|
while (!complete) {
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM messages
|
|
|
|
WHERE id > $id
|
|
|
|
ORDER BY id ASC
|
|
|
|
LIMIT $chunkSize;`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$chunkSize: chunkSize,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const messages = map(rows, row => jsonToObject(row.json));
|
|
|
|
forEach(messages, message => {
|
|
|
|
const externalFiles = getExternalFilesForMessage(message);
|
|
|
|
forEach(externalFiles, file => {
|
|
|
|
delete lookup[file];
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
const lastMessage = last(messages);
|
|
|
|
if (lastMessage) {
|
|
|
|
({ id } = lastMessage);
|
|
|
|
}
|
|
|
|
complete = messages.length < chunkSize;
|
|
|
|
count += messages.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`removeKnownAttachments: Done processing ${count} messages`);
|
|
|
|
|
2018-09-21 01:47:19 +00:00
|
|
|
complete = false;
|
|
|
|
count = 0;
|
|
|
|
// Though conversations.id is a string, this ensures that, when coerced, this
|
|
|
|
// value is still a string but it's smaller than every other string.
|
|
|
|
id = 0;
|
|
|
|
|
|
|
|
const conversationTotal = await getConversationCount();
|
|
|
|
console.log(
|
|
|
|
`removeKnownAttachments: About to iterate through ${conversationTotal} conversations`
|
|
|
|
);
|
|
|
|
|
|
|
|
while (!complete) {
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
const rows = await db.all(
|
|
|
|
`SELECT json FROM conversations
|
|
|
|
WHERE id > $id
|
|
|
|
ORDER BY id ASC
|
|
|
|
LIMIT $chunkSize;`,
|
|
|
|
{
|
|
|
|
$id: id,
|
|
|
|
$chunkSize: chunkSize,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const conversations = map(rows, row => jsonToObject(row.json));
|
|
|
|
forEach(conversations, conversation => {
|
|
|
|
const externalFiles = getExternalFilesForConversation(conversation);
|
|
|
|
forEach(externalFiles, file => {
|
|
|
|
delete lookup[file];
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
const lastMessage = last(conversations);
|
|
|
|
if (lastMessage) {
|
|
|
|
({ id } = lastMessage);
|
|
|
|
}
|
|
|
|
complete = conversations.length < chunkSize;
|
|
|
|
count += conversations.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`removeKnownAttachments: Done processing ${count} conversations`);
|
|
|
|
|
2018-08-06 23:18:58 +00:00
|
|
|
return Object.keys(lookup);
|
|
|
|
}
|