SymphonyElectron/js/search/search.js

464 lines
14 KiB
JavaScript
Raw Normal View History

2017-07-25 10:28:38 -05:00
'use strict';
const fs = require('fs');
const randomString = require('randomstring');
2017-07-27 00:18:11 -05:00
const electron = require('electron');
2017-07-31 01:19:16 -05:00
const childProcess = require('child_process');
2017-07-27 00:18:11 -05:00
const app = electron.app;
const path = require('path');
const isDevEnv = require('../utils/misc.js').isDevEnv;
const isMac = require('../utils/misc.js').isMac;
2017-07-31 08:07:30 -05:00
// Search library
const libSymphonySearch = require('./searchLibrary');
// Crypto Library
const Crypto = require('../cryptoLib');
2017-07-31 08:07:30 -05:00
// Path for the exec file and the user data folder
const userData = path.join(app.getPath('userData'));
const execPath = path.dirname(app.getPath('exe'));
2017-07-27 09:50:26 -05:00
2017-07-31 08:07:30 -05:00
// Constants paths for temp indexing folders
const TEMP_BATCH_INDEX_FOLDER = isDevEnv ? './data/temp_batch_indexes' : path.join(userData, 'data/temp_batch_indexes');
2017-08-08 00:35:40 -05:00
const TEMP_REAL_TIME_INDEX = isDevEnv ? './data/temp_realtime_index' : path.join(userData, 'data/temp_realtime_index');
// Main User Index path
2017-07-31 08:07:30 -05:00
const INDEX_PREFIX = isDevEnv ? './data/search_index' : path.join(userData, 'data/search_index');
// Folder contains real time, batch and user index
2017-07-31 08:07:30 -05:00
const INDEX_DATA_FOLDER = isDevEnv ? './data' : path.join(userData, 'data');
//3 Months
const SEARCH_PERIOD_SUBTRACTOR = 3 * 31 * 24 * 60 * 60 * 1000;
2017-07-25 10:28:38 -05:00
const MINIMUM_DATE = '0000000000000';
const MAXIMUM_DATE = '9999999999999';
const INDEX_VERSION = 'v1';
2017-08-08 00:35:40 -05:00
const SORT_BY_SCORE = 0;
const BATCH_RANDOM_INDEX_PATH_LENGTH = 20;
2017-07-31 08:07:30 -05:00
2017-08-08 00:35:40 -05:00
// library path contractor
2017-08-03 04:18:56 -05:00
const winArchPath = process.arch === 'ia32' ? 'library/indexvalidator-x86.exe' : 'library/indexvalidator-x64.exe';
const rootPath = isMac ? 'library/indexvalidator.exec' : winArchPath;
const productionPath = path.join(execPath, isMac ? '..' : '', rootPath);
const devPath = path.join(__dirname, '..', '..', rootPath);
const INDEX_VALIDATOR = isDevEnv ? devPath : productionPath;
2017-07-25 10:28:38 -05:00
2017-08-08 00:35:40 -05:00
/**
* This search class communicates with the SymphonySearchEngine C library via node-ffi.
* There should be only 1 instance of this class in the Electron
*/
2017-07-25 10:28:38 -05:00
class Search {
2017-08-08 00:35:40 -05:00
/**
* Constructor for the SymphonySearchEngine library
* @param userId (for the index folder name)
* @param key
2017-08-08 00:35:40 -05:00
*/
constructor(userId, key) {
2017-07-25 10:28:38 -05:00
this.isInitialized = false;
this.userId = userId;
this.key = key;
this.indexFolderName = INDEX_PREFIX + '_' + this.userId + '_' + INDEX_VERSION;
2017-08-07 00:12:46 -05:00
this.dataFolder = INDEX_DATA_FOLDER;
2017-08-08 00:35:40 -05:00
this.realTimeIndex = TEMP_REAL_TIME_INDEX;
2017-08-07 00:12:46 -05:00
this.batchIndex = TEMP_BATCH_INDEX_FOLDER;
this.messageData = [];
this.crypto = new Crypto(userId, key);
this.decryptAndInit();
}
decryptAndInit() {
this.crypto.decryption().then(() => {
this.init();
}).catch(() => {
this.init();
});
2017-07-25 10:28:38 -05:00
}
2017-08-08 00:35:40 -05:00
/**
* returns isInitialized boolean
* @returns {boolean}
2017-08-08 00:35:40 -05:00
*/
2017-07-25 10:28:38 -05:00
isLibInit() {
return this.isInitialized;
2017-07-25 10:28:38 -05:00
}
2017-08-08 00:35:40 -05:00
/**
* This init function
* initialise the SymphonySearchEngine library
* and creates a folder in the userData
*/
2017-07-25 10:28:38 -05:00
init() {
libSymphonySearch.symSEInit();
2017-08-07 00:12:46 -05:00
libSymphonySearch.symSEEnsureFolderExists(this.dataFolder);
libSymphonySearch.symSERemoveFolder(this.realTimeIndex);
libSymphonySearch.symSERemoveFolder(this.batchIndex);
2017-07-31 08:07:30 -05:00
Search.indexValidator(this.indexFolderName);
2017-08-07 00:12:46 -05:00
Search.indexValidator(this.realTimeIndex);
2017-07-25 10:28:38 -05:00
let indexDateStartFrom = new Date().getTime() - SEARCH_PERIOD_SUBTRACTOR;
// Deleting all the messages except 3 Months from now
2017-07-25 10:28:38 -05:00
libSymphonySearch.symSEDeleteMessages(this.indexFolderName, null,
MINIMUM_DATE, indexDateStartFrom.toString());
this.isInitialized = true;
}
2017-08-08 00:35:40 -05:00
/**
* An array of messages is passed for indexing
* it will be indexed in a temporary index folder
* @param {Array} messages
2017-08-08 00:35:40 -05:00
* @returns {Promise}
*/
2017-07-25 10:28:38 -05:00
indexBatch(messages) {
return new Promise((resolve, reject) => {
if (!messages) {
reject(new Error('Messages is required'));
return;
}
if (!(JSON.parse(messages) instanceof Array)) {
reject(new Error('Messages must be an array'));
return;
}
2017-07-25 10:28:38 -05:00
if (!this.isInitialized) {
reject(new Error('Library not initialized'));
return;
2017-07-25 10:28:38 -05:00
}
const indexId = randomString.generate(BATCH_RANDOM_INDEX_PATH_LENGTH);
libSymphonySearch.symSECreatePartialIndexAsync(this.batchIndex, indexId, messages, (err, res) => {
if (err) {
reject(new Error(err));
}
2017-07-27 09:50:26 -05:00
resolve(res);
2017-07-25 10:28:38 -05:00
});
});
}
2017-08-08 00:35:40 -05:00
/**
* Merging the temporary
* created from indexBatch()
*/
2017-07-25 10:28:38 -05:00
mergeIndexBatches() {
return new Promise((resolve, reject) => {
libSymphonySearch.symSEMergePartialIndexAsync(this.indexFolderName, this.batchIndex, (err, res) => {
if (err) {
reject(new Error(err));
}
libSymphonySearch.symSERemoveFolder(this.batchIndex);
resolve(res);
});
2017-07-25 10:28:38 -05:00
});
}
2017-08-08 00:35:40 -05:00
/**
* An array of messages to be indexed
* in real time
* @param message
*/
2017-07-27 09:50:26 -05:00
realTimeIndexing(message) {
if (!message) {
return new Error('Message is required');
}
if (!(JSON.parse(message) instanceof Array)){
return new Error('Message must be an array');
}
if (!this.isInitialized) {
return new Error('Library not initialized');
}
let result = libSymphonySearch.symSEIndexRealTime(this.realTimeIndex, message);
return result === 0 ? "Successful" : result
2017-07-27 09:50:26 -05:00
}
2017-08-08 00:35:40 -05:00
/**
* Reading a json file
* for the demo search app only
* @param {String} batch
2017-08-08 00:35:40 -05:00
* @returns {Promise}
*/
2017-07-27 09:50:26 -05:00
readJson(batch) {
2017-07-25 10:28:38 -05:00
return new Promise((resolve, reject) => {
2017-08-01 08:05:51 -05:00
let dirPath = path.join(execPath, isMac ? '..' : '', 'msgsjson', batch);
2017-07-31 08:07:30 -05:00
let messageFolderPath = isDevEnv ? path.join('./msgsjson', batch) : dirPath;
2017-07-27 00:18:11 -05:00
let files = fs.readdirSync(messageFolderPath);
this.messageData = [];
files.forEach((file) => {
2017-07-27 09:50:26 -05:00
let tempPath = path.join(messageFolderPath, file);
let data = fs.readFileSync(tempPath, "utf8");
2017-07-25 10:28:38 -05:00
if (data) {
try {
this.messageData.push(JSON.parse(data));
} catch (err) {
reject(new Error(err))
}
2017-07-25 10:28:38 -05:00
} else {
reject(new Error('Error reading batch'))
2017-07-25 10:28:38 -05:00
}
});
resolve(this.messageData);
2017-07-25 10:28:38 -05:00
});
}
/**
* Encrypting the index after the merging the index
* to the main user index
*/
encryptIndex() {
this.crypto.encryption().then(() => {
return 'Success'
}).catch((e) => {
throw new Error(e)
});
}
2017-08-08 00:35:40 -05:00
/**
* This returns the search results
2017-08-08 00:35:40 -05:00
* which returns a char *
* @param {String} query
* @param {Array} senderIds
* @param {Array} threadIds
* @param {String} fileType
* @param {String} startDate
* @param {String} endDate
* @param {Number} limit
* @param {Number} offset
* @param {Number} sortOrder
2017-08-08 00:35:40 -05:00
* @returns {Promise}
*/
searchQuery(query, senderIds, threadIds, fileType, startDate,
2017-08-14 06:27:07 -05:00
endDate, limit, offset, sortOrder) {
2017-07-25 10:28:38 -05:00
2017-08-07 00:12:46 -05:00
let _limit = limit;
let _offset = offset;
let _sortOrder = sortOrder;
2017-07-25 10:28:38 -05:00
return new Promise((resolve, reject) => {
if (!this.isInitialized) {
reject(new Error('Library not initialized'));
return;
}
if (!fs.existsSync(this.indexFolderName) || !fs.existsSync(this.realTimeIndex)) {
reject('Index folder does not exist.');
return;
2017-07-25 10:28:38 -05:00
}
let q = Search.constructQuery(query, senderIds, threadIds, fileType);
2017-07-25 10:28:38 -05:00
if (q === undefined) {
reject(new Error('Search query error'));
return;
2017-07-25 10:28:38 -05:00
}
let sd = new Date().getTime() - SEARCH_PERIOD_SUBTRACTOR;
let sd_time = MINIMUM_DATE;
2017-08-01 07:10:00 -05:00
if (startDate && startDate !== "" && typeof startDate === 'object') {
sd_time = new Date(startDate).getTime();
if (sd_time >= sd) {
sd_time = sd;
2017-07-25 10:28:38 -05:00
}
}
let ed_time = MAXIMUM_DATE;
2017-08-01 07:10:00 -05:00
if (endDate && endDate !== "" && typeof endDate === 'object') {
ed_time = new Date(endDate).getTime();
2017-07-25 10:28:38 -05:00
}
2017-08-07 00:12:46 -05:00
if (!_limit && _limit === "" && typeof _limit !== 'number' && Math.round(_limit) !== _limit) {
_limit = 25;
2017-07-25 10:28:38 -05:00
}
2017-08-07 00:12:46 -05:00
if (!_offset && _offset === "" && typeof _offset !== 'number' && Math.round(_offset) !== _offset) {
_offset = 0
2017-07-25 10:28:38 -05:00
}
2017-08-07 00:12:46 -05:00
if (!_sortOrder && _sortOrder === "" && typeof _sortOrder !== 'number' && Math.round(_sortOrder) !== _sortOrder) {
_sortOrder = SORT_BY_SCORE;
2017-07-25 10:28:38 -05:00
}
2017-08-07 00:12:46 -05:00
const returnedResult = libSymphonySearch.symSESearch(this.indexFolderName, this.realTimeIndex, q, sd_time.toString(), ed_time.toString(), _offset, _limit, _sortOrder);
try {
let ret = returnedResult.readCString();
resolve(JSON.parse(ret));
} finally {
2017-07-27 09:50:26 -05:00
libSymphonySearch.symSEFreeResult(returnedResult);
2017-07-27 00:18:11 -05:00
}
2017-07-25 10:28:38 -05:00
});
}
/**
* returns the latest message timestamp
* from the indexed data
* @returns {Promise}
*/
getLatestMessageTimestamp() {
return new Promise((resolve, reject) => {
if (!this.isInitialized) {
reject('Not initialized');
return;
}
if (!fs.existsSync(this.indexFolderName)) {
reject('Index folder does not exist.');
return;
}
libSymphonySearch.symSEGetLastMessageTimestampAsync(this.indexFolderName, (err, res) => {
if (err) {
reject(new Error(err));
}
const returnedResult = res;
try {
let ret = returnedResult.readCString();
resolve(ret);
} finally {
libSymphonySearch.symSEFreeResult(returnedResult);
}
});
});
}
2017-08-08 00:35:40 -05:00
/**
* This the query constructor
* for the search function
* @param {String} searchQuery
* @param {Array} senderId
* @param {Array} threadId
2017-10-09 04:00:10 -05:00
* @param {String} fileType
2017-08-08 00:35:40 -05:00
* @returns {string}
*/
static constructQuery(searchQuery, senderId, threadId, fileType) {
2017-10-09 05:31:07 -05:00
let searchText = "";
if(searchQuery !== undefined) {
2017-10-09 05:31:07 -05:00
searchText = searchQuery.trim().toLowerCase(); //to prevent injection of AND and ORs
}
2017-08-04 03:37:48 -05:00
let q = "";
2017-10-09 05:31:07 -05:00
let hashTags = Search.getHashTags(searchText);
let hashCashTagQuery = "";
2017-08-04 03:37:48 -05:00
if(hashTags.length > 0) {
hashCashTagQuery = " OR tags:(";
hashTags.forEach((item) => {
hashCashTagQuery = hashCashTagQuery + "\"" + item + "\" "
});
hashCashTagQuery += ")";
}
2017-08-04 03:37:48 -05:00
let hasAttachments = false;
let additionalAttachmentQuery = "";
if(fileType) {
hasAttachments = true;
if(fileType === "attachment") {
additionalAttachmentQuery = "(hasfiles:true)";
} else {
additionalAttachmentQuery = "(filetype:(" + fileType +"))";
}
}
2017-10-09 05:31:07 -05:00
if (searchText.length > 0 ) {
q = "((text:(" + searchText + "))" + hashCashTagQuery ;
if(hasAttachments) {
2017-10-09 05:31:07 -05:00
q += " OR (filename:(" + searchText + "))" ;
}
q = q + ")";
2017-08-14 06:27:07 -05:00
}
q = Search.appendFilterQuery(q, "senderId", senderId);
q = Search.appendFilterQuery(q, "threadId", threadId);
2017-08-04 03:37:48 -05:00
if(q === "") {
if(hasAttachments) {
q = additionalAttachmentQuery;
} else {
q = undefined; //will be handled in the search function
}
} else {
if(hasAttachments){
q = q + " AND " + additionalAttachmentQuery
}
2017-08-04 03:37:48 -05:00
}
return q;
}
2017-08-04 03:37:48 -05:00
/**
* appending the senderId and threadId for the query
2017-10-09 05:31:07 -05:00
* @param {String} searchText
* @param {String} fieldName
* @param {Array} valueArray
* @returns {string}
*/
2017-10-09 05:31:07 -05:00
static appendFilterQuery(searchText, fieldName, valueArray) {
let q = "";
if (valueArray && valueArray.length > 0 ) {
q += "(" + fieldName +":(";
valueArray.forEach((item)=>{
q+= "\"" + item + "\" ";
});
q += "))";
2017-10-09 05:31:07 -05:00
if(searchText.length > 0 ) {
q = searchText + " AND " + q;
}
} else {
2017-10-09 05:31:07 -05:00
q = searchText;
}
2017-08-07 00:12:46 -05:00
return q;
2017-07-25 10:28:38 -05:00
}
// hashtags can have any characters(before the latest release it was
// not like this). So the only regex is splitting the search query based on
// whitespaces
/**
* return the hash cash
* tags from the query
2017-10-09 05:31:07 -05:00
* @param {String} searchText
* @returns {Array}
*/
2017-10-09 05:31:07 -05:00
static getHashTags(searchText) {
let hashTags = [];
2017-10-09 05:31:07 -05:00
let tokens = searchText.toLowerCase()
.trim()
.replace(/\s\s+/g, ' ')
.split(' ').filter((el) => {return el.length !== 0});
tokens.forEach((item) => {
if (item.startsWith('#') || item.startsWith('$')) {
hashTags.push(item);
}
});
return hashTags;
}
2017-08-08 00:35:40 -05:00
/**
* Validate the index folder exist or not
* @param {String} file
2017-08-08 00:35:40 -05:00
* @returns {*}
*/
2017-07-31 08:07:30 -05:00
static indexValidator(file) {
let data;
let result = childProcess.execFileSync(INDEX_VALIDATOR, [file]).toString();
try {
data = JSON.parse(result);
if (data.status === 'OK') {
2017-07-31 10:25:19 -05:00
return data;
2017-07-31 08:07:30 -05:00
}
2017-07-31 10:25:19 -05:00
return new Error('Unable validate index folder')
2017-07-31 08:07:30 -05:00
} catch (err) {
throw new Error(err);
2017-07-31 08:07:30 -05:00
}
}
2017-07-25 10:28:38 -05:00
}
2017-08-08 00:35:40 -05:00
/**
* Exporting the search library
* @type {{Search: Search}}
*/
2017-07-25 10:28:38 -05:00
module.exports = {
Search: Search
};