SolidScribe/server/models/Note.js
Max G e87e8513bc * Made splash page dark and updated description
* Cleaned up unused things
* Updated squire which had a comment typo update...thats it
* Background color picker has matching colors and styles to text color picker
* Added new black theme
* Moved search to main page, show it on mobile and added options to push things to notes from search with experimental tag searching
* Added active note menu buttons based on cursor location in text
* Added more instant updating if app is open in two locations for the same user Scratch Pad and home page update with new notes and new text in real time
2020-05-15 23:12:09 +00:00

1076 lines
30 KiB
JavaScript

let db = require('@config/database')
let Note = module.exports = {}
let Tags = require('@models/Tag')
let Attachment = require('@models/Attachment')
let ShareNote = require('@models/ShareNote')
let ProcessText = require('@helpers/ProcessText')
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
const crypto = require('crypto')
const cs = require('@helpers/CryptoString')
const rp = require('request-promise');
const fs = require('fs')
const gm = require('gm')
Note.test = (userId, masterKey) => {
return new Promise((resolve, reject) => {
let testNoteId = 0
Note.create(null, userId, '','', masterKey)
.then(newNoteId => {
console.log('Test: Create Note - Pass')
testNoteId = newNoteId
return Note.update
(null, userId, testNoteId, 'Note text', 'Test Note beans Title', 0, 0, 0, 'hash', masterKey)
})
.then(() => {
console.log('Test: Update Note - Pass')
return Note.get(userId, testNoteId, masterKey)
})
.then(updatedText => {
console.log('Test: Open Updated Note - Pass')
const shareUserId = 61
return ShareNote.migrateNoteToShared(userId, testNoteId, shareUserId, masterKey)
})
.then(shareResults => {
console.log('Test: Set Note To Shared - Pass')
return Note.get(userId, testNoteId, masterKey)
})
.then(() => {
console.log('Test: Open Shared Note - Pass')
return Note.update
(null, userId, testNoteId, 'Shared Update', 'Test Note beans Title', 0, 0, 0, 'hash', masterKey)
})
.then(() => {
console.log('Test: Update Shared Note - Pass')
return Note.reindex(userId, masterKey)
})
.then( reindexResults => {
console.log(`Test: Reindex Notes - ${reindexResults?'Pass':'Fail'}`)
return Note.encryptedIndexSearch(userId, 'beans', null, masterKey)
})
.then(textSearchResults => {
if(textSearchResults['ids'] && textSearchResults['ids'].length >= 1){
console.log('Test: Search Index - Pass')
} else { console.log('Test: Search Index - Fail') }
return Note.delete(userId, testNoteId)
})
.then(results => {
console.log('Test: Delete Note - Pass')
return resolve('Test: Complete')
})
})
}
//User doesn't have an encrypted note set. Encrypt all notes
Note.encryptEveryNote = (userId, masterKey) => {
return new Promise((resolve, reject) => {
//Select all the user notes
db.promise().query(`
SELECT * FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE salt IS NULL AND user_id = ? AND encrypted = 0 AND shared = 0`, [userId])
.then((rows, fields) => {
let foundNotes = rows[0]
console.log('Encrypting user notes ',rows[0].length)
// return resolve(true)
let allTheUpdates = []
let timeoutAdder = 0
foundNotes.forEach(note => {
timeoutAdder += 100
const newUpdate = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Encrypting Note ', note.id)
const created = Math.round((+new Date)/1000)
const salt = cs.createSmallSalt()
const noteText = note.text
const noteTitle = note.title
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
const noteSnippet = cs.encrypt(masterKey, salt, snippet)
const textObject = JSON.stringify([noteTitle, noteText])
const encryptedText = cs.encrypt(masterKey, salt, textObject)
db.promise()
.query('UPDATE note_raw_text SET title = ?, text = ?, snippet = ?, salt = ? WHERE id = ?',
[null, encryptedText, noteSnippet, salt, note.note_raw_text_id])
.then(() => {
resolve(true)
})
}, timeoutAdder)
})
allTheUpdates.push(newUpdate)
})
Promise.all(allTheUpdates).then(done => {
console.log('Indexing first 100')
return Note.reindex(userId, masterKey)
}).then(results => {
console.log('Done')
resolve(true)
})
})
})
}
//Returns insertedId of new note
Note.create = (io, userId, noteTitle = '', noteText = '', masterKey) => {
return new Promise((resolve, reject) => {
if(userId == null || userId < 10){ reject('User Id required to create note') }
const created = Math.round((+new Date)/1000)
const salt = cs.createSmallSalt()
const snippetSalt = cs.createSmallSalt()
const snippetObj = JSON.stringify([noteTitle, noteText.substring(0, 500)])
const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj)
const textObject = JSON.stringify([noteTitle, noteText])
const encryptedText = cs.encrypt(masterKey, salt, textObject)
db.promise()
.query(`INSERT INTO note_raw_text (text, salt, updated) VALUE (?, ?, ?)`, [encryptedText, salt, created])
.then( (rows, fields) => {
const rawTextId = rows[0].insertId
return db.promise()
.query('INSERT INTO note (user_id, note_raw_text_id, created, quick_note, snippet, snippet_salt) VALUES (?,?,?,?,?,?)',
[userId, rawTextId, created, 0, snippet, snippetSalt])
})
.then((rows, fields) => {
if(io){
io.to(userId).emit('new_note_created', rows[0].insertId)
}
// Indexing is done on save
resolve(rows[0].insertId) //Only return the new note ID when creating a new note
})
.catch(console.log)
})
}
// Called when a note is close
// Will attempt to reindex all notes that are flagged in database as not indexed
// Limit to 100 notes per batch
Note.reindex = (userId, masterKey) => {
return new Promise((resolve, reject) => {
if(!masterKey || masterKey.length == 0){
return reject('Master key needed for reindex')
}
let notIndexedNoteIds = []
let searchIndex = null
let searchIndexSalt = null
let foundNotes = null
//First check if we have any notes to index
db.promise().query(`
SELECT note.id, text, salt FROM note
JOIN note_raw_text ON note.note_raw_text_id = note_raw_text.id
WHERE indexed = 0 AND encrypted = 0 AND salt IS NOT NULL
AND user_id = ? LIMIT 100`, [userId])
.then((rows, fields) => {
//Halt execution if there are no new notes
foundNotes = rows[0]
if(foundNotes.length == 0){
throw new Error('No new notes to index')
}
//Select search index, if it doesn't exist, create it
return db.promise().query(`SELECT * FROM user_encrypted_search_index WHERE user_id = ? LIMIT 1`, [userId])
})
.then((rows, fields) => {
if(rows[0].length == 0){
//Create search index entry, return an object
searchIndexSalt = cs.createSmallSalt()
//Select all user notes to recreate index
return db.promise().query(`
SELECT note.id, text, salt FROM note
JOIN note_raw_text ON note.note_raw_text_id = note_raw_text.id
WHERE encrypted = 0 AND user_id = ?`, [userId])
.then((rows, fields) => {
foundNotes = rows[0]
return db.promise().query("INSERT INTO user_encrypted_search_index (`user_id`, `salt`) VALUES (?,?)", [userId, searchIndexSalt])
})
.then((rows, fields) => {
//return a fresh search index
return new Promise((resolve, reject) => { resolve('{}') })
})
} else {
const row = rows[0][0]
searchIndexSalt = row.salt
//Decrypt search index and continue.
let decipheredSearchIndex = '{}'
if(row.index && row.index.length > 0){
//Decrypt json, do not parse json yet, we want raw text
decipheredSearchIndex = cs.decrypt(masterKey, searchIndexSalt, row.index)
}
return new Promise((resolve, reject) => { resolve( decipheredSearchIndex ) })
}
})
.then(rawSearchIndex => {
searchIndex = rawSearchIndex
//Remove all instances of IDs from text
foundNotes.forEach(note => {
notIndexedNoteIds.push(note.id)
//Remove every instance of note id
const removeId = new RegExp(note.id,"gm")
const removeDoubles = new RegExp(',,',"g")
// const removeTrail = new RegExp(',]',"g")
searchIndex = searchIndex
.replace(removeId, '')
.replace(removeDoubles, ',')
.replace(/,]/g, ']')
.replace(/\[\,/g, '[') //search [,
})
searchIndex = JSON.parse(searchIndex)
//Remove unused words, this may not be needed and it increases overhead
Object.keys(searchIndex).forEach(word => {
if(searchIndex[word].length == 0){
delete searchIndex[word]
}
})
//Process text of each note and add it to the index
let reindexQueue = []
let reindexTimer = 0
foundNotes.forEach(note => {
reindexTimer += 50
let reindexPromise = new Promise((resolve, reject) => {
setTimeout(() => {
if(masterKey == null || note.salt == null){
console.log('Error indexing note', note.id)
return resolve(true)
}
const noteHtml = cs.decrypt(masterKey, note.salt, note.text)
const rawText =
ProcessText.removeHtml(noteHtml) //Remove HTML
.toLowerCase()
.replace(/style=".*?"/g,'') //Remove inline styles
.replace (/&#{0,1}[a-z0-9]+;/ig, '') //remove HTML entities
.replace(/[^A-Za-z0-9]/g, ' ') //Convert all to a-z only
.replace(/ +(?= )/g,'') //Remove double spaces
rawText.split(' ').forEach(word => {
//Skip small words
if(word.length <= 2){ return }
if(Array.isArray( searchIndex[word] )){
if(searchIndex[word].indexOf( note.id ) == -1){
searchIndex[word].push( note.id )
}
} else {
searchIndex[word] = [ note.id ]
}
})
return resolve(true)
}, reindexTimer)
})
reindexQueue.push(reindexPromise)
})
return Promise.all(reindexQueue)
})
.then(rawSearchIndex => {
// console.log('All notes indexed')
const created = Math.round((+new Date)/1000)
const jsonSearchIndex = JSON.stringify(searchIndex)
const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
[encryptedJsonIndex, created, userId])
.then((rows, fields) => {
return db.promise().query('UPDATE note SET `indexed` = 1 WHERE (`id` IN (?))', [notIndexedNoteIds])
})
.then((rows, fields) => {
// console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
resolve(true)
})
}).catch(error => {
console.log('Reindex Error')
console.log(error)
})
//Find all note Ids that need to be reindexed
// return resolve(true)
return
Note.get(userId, noteId)
.then(note => {
let noteText = note.text
if(note.encrypted == 1){
noteText = '' //Don't put note text in encrypted notes
}
//
// Update Solr index
//
Tags.string(userId, noteId)
.then(tagString => {
const fullText = note.title + ' ' + ProcessText.removeHtml(noteText) +' '+ tagString
db.promise()
.query(`
INSERT INTO note_text_index (note_id, user_id, text)
VALUES (?,?,?)
ON DUPLICATE KEY UPDATE text = ?
`, [noteId, userId, fullText, fullText])
.then((rows, fields) => {
resolve(true)
})
.catch(console.log)
})
})
})
}
// Returns updated note text
Note.update = (io, userId, noteId, noteText, noteTitle, color, pinned, archived, hash, masterKey) => {
return new Promise((resolve, reject) => {
const now = Math.round((+new Date)/1000)
let noteSnippet = ''
let User = require('@models/User')
let userPrivateKey = null
User.getPrivateKey(userId, masterKey)
.then(privateKey => {
userPrivateKey = privateKey
return db.promise()
.query(`
SELECT note_raw_text_id, salt, snippet_salt, encrypted_share_password_key FROM note
JOIN note_raw_text ON note_raw_text_id = note_raw_text.id
WHERE note.id = ? AND user_id = ?`, [noteId, userId])
})
.then((rows, fields) => {
const textId = rows[0][0]['note_raw_text_id']
let salt = rows[0][0]['salt']
let snippetSalt = rows[0][0]['snippet_salt']
//Shared notes use encrypted key - decrypt key then decrypt note
const encryptedShareKey = rows[0][0].encrypted_share_password_key
if(encryptedShareKey != null){
masterKey = crypto.privateDecrypt(userPrivateKey,
Buffer.from(encryptedShareKey, 'base64') )
}
let encryptedNoteText = ''
//Create encrypted snippet
const snippet = JSON.stringify([noteTitle, noteText.substring(0, 500)])
noteSnippet = cs.encrypt(masterKey, snippetSalt, snippet)
//Encrypt note text
const textObject = JSON.stringify([noteTitle, noteText])
encryptedNoteText = cs.encrypt(masterKey, salt, textObject)
//
// @TODO - this needs some kind of rate limiting
// A note shared with a lot of users could do a ton of updates every save
//
//Update text snippet for all other shared users
db.promise().query('SELECT * FROM note WHERE note_raw_text_id = ? AND id != ?', [textId, noteId])
.then((rows, fields) => {
for (var i = 0; i < rows[0].length; i++) {
const otherNote = rows[0][i]
//Re-encrypt for other user
const updatedSnippet = cs.encrypt(masterKey, otherNote.snippet_salt, snippet)
db.promise().query('UPDATE note SET snippet = ? WHERE id = ?', [updatedSnippet, otherNote.id])
}
})
//Update Note text
return db.promise()
.query('UPDATE note_raw_text SET text = ?, updated = ? WHERE id = ?', [encryptedNoteText, now, textId])
})
.then( (rows, fields) => {
//Update other note attributes
return db.promise()
.query('UPDATE note SET pinned = ?, archived = ?, color = ?, snippet = ?, indexed = 0 WHERE id = ? AND user_id = ? LIMIT 1',
[pinned, archived, color, noteSnippet, noteId, userId])
})
.then((rows, fields) => {
if(io){
io.to(userId).emit('new_note_text_saved', {noteId, hash})
}
//Async attachment reindex
Attachment.scanTextForWebsites(io, userId, noteId, noteText)
//Send back updated response
resolve(rows[0])
})
.catch(console.log)
})
}
Note.setPinned = (userId, noteId, pinnedBoolean) => {
return new Promise((resolve, reject) => {
const pinned = pinnedBoolean ? 1:0
//Update other note attributes
return db.promise()
.query('UPDATE note SET pinned = ? WHERE id = ? AND user_id = ? LIMIT 1',
[pinned, noteId, userId])
.then((rows, fields) => {
resolve(true)
})
})
}
Note.setArchived = (userId, noteId, archivedBoolead) => {
return new Promise((resolve, reject) => {
const archived = archivedBoolead ? 1:0
//Update other note attributes
return db.promise()
.query('UPDATE note SET archived = ? WHERE id = ? AND user_id = ? LIMIT 1',
[archived, noteId, userId])
.then((rows, fields) => {
resolve(true)
})
})
}
//
// Delete a note and all its remaining parts
//
Note.delete = (userId, noteId) => {
return new Promise((resolve, reject) => {
//
// Delete, note, text, search index and associated tags
// Leave the attachments, they can be deleted on their own
// Leave Tags, their text is shared
let rawTextId = null
let noteTextCount = 0
// Lookup the note text ID, we need this to count usages
db.promise()
.query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
.then((rows, fields) => {
//Save the raw text ID
rawTextId = rows[0][0]['note_raw_text_id']
return db.promise()
.query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
})
.then((rows, fields) => {
//Save the number of times the note is used
noteTextCount = rows[0][0]['count']
//Don't delete text if its shared
if(noteTextCount == 1){
//If text is only used on one note, delete it (its not shared)
return db.promise()
.query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
} else {
return new Promise((resolve, reject) => {
resolve(true)
})
}
})
.then( results => {
// Delete Note entry for this user.
return db.promise()
.query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId, userId])
})
.then((rows, fields) => {
// Delete search index
return db.promise()
.query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId])
})
.then((rows, fields) => {
// delete tags
return db.promise()
.query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
})
.then((rows, fields) => {
//IF there are nots with a matching raw text id, we want to under their share status
db.promise().query('SELECT id FROM note WHERE note_raw_text_id = ?',[rawTextId])
.then((rows, fields) => {
if(rows[0].length == 1){
db.promise().query('UPDATE note SET shared = 0 WHERE id = ?', [rows[0][0]['id']])
}
})
resolve(true)
})
})
}
//text is the current text for the note that will be compared to the text in the database
Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
return new Promise((resolve, reject) => {
Note.get(userId, noteId)
.then(noteObject => {
if(!noteObject.text || !usersCurrentText || noteObject.encrypted == 1){
return resolve(null)
}
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
if(noteObject.updated == lastUpdated){
// console.log('No note diff')
return resolve(null)
}
if(noteObject.updated > lastUpdated){
newText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
oldText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
}
const dmp = new DiffMatchPatch.diff_match_patch()
const diff = dmp.diff_main(oldText, newText)
dmp.diff_cleanupSemantic(diff)
const patch_list = dmp.patch_make(oldText, newText, diff);
const patch_text = dmp.patch_toText(patch_list);
//Patch text - shows a list of changes
var patches = dmp.patch_fromText(patch_text);
// console.log(patch_text)
//results[1] - contains diagnostic data for patch apply, its possible it can fail
var results = dmp.patch_apply(patches, oldText);
//Compile return data for front end
const returnData = {
updatedText: results[0],
diffs: results[1].length, //Only use length for now
updated: Math.max(noteObject.updated,lastUpdated) //Return most recently updated date
}
//Final change in notes
// console.log(returnData)
resolve(returnData)
})
})
}
Note.get = (userId, noteId, masterKey) => {
return new Promise((resolve, reject) => {
let User = require('@models/User')
if(!masterKey || masterKey.length == 0){
return reject('Get note called without master key')
}
let userPrivateKey = null;
User.getPrivateKey(userId, masterKey)
.then(privateKey => {
//Grab users private key
userPrivateKey = privateKey
return db.promise()
.query(`
SELECT
note_raw_text.text,
note_raw_text.salt,
note_raw_text.updated as updated,
note.id,
note.user_id,
note.created,
note.pinned,
note.archived,
note.color,
note.encrypted_share_password_key,
count(distinct attachment.id) as attachment_count,
note.note_raw_text_id as rawTextId,
shareUser.username as shareUsername
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
LEFT JOIN attachment ON (note.id = attachment.note_id)
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId, noteId])
})
.then((rows, fields) => {
const nowTime = Math.round((+new Date)/1000)
let noteLockedOut = false
let noteData = rows[0][0]
// const rawTextId = noteData['rawTextId']
//Block access to notes if invalid or user doesn't have access
if(!noteData || !noteData['user_id'] || noteData['user_id'] != userId || noteData['id'] != noteId){
return resolve(false)
}
//Shared notes use encrypted key - decrypt key then decrypt note
if(noteData.encrypted_share_password_key != null){
masterKey = crypto.privateDecrypt(userPrivateKey,
Buffer.from(noteData.encrypted_share_password_key, 'base64') )
}
//Normal Encrypted note
const decipheredText = cs.decrypt(masterKey, noteData.salt, noteData.text)
if(decipheredText == null){
throw new Error('Unable to decropt note text')
}
//Parse title and text from encrypted data and update object
const textObject = JSON.parse(decipheredText)
noteData.title = textObject[0]
noteData.text = textObject[1]
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
//Return note data
delete noteData.salt //remove salt from return data
delete noteData.encrypted_share_password_key
noteData.lockedOut = noteLockedOut
resolve(noteData)
})
.catch(error => {
console.log(error)
resolve(false)
})
})
}
//Public note share action -> may not be used
Note.getShared = (noteId) => {
return new Promise((resolve, reject) => {
db.promise()
.query('SELECT text, color FROM note WHERE id = ? AND shared = 1 LIMIT 1', [noteId])
.then((rows, fields) => {
//Return note data
resolve(rows[0][0])
})
.catch(console.log)
})
}
// Searches text index, returns nothing if there is no search query
Note.encryptedIndexSearch = (userId, searchQuery, searchTags, masterKey) => {
return new Promise((resolve, reject) => {
if(searchQuery.length == 0){
resolve(null)
} else {
if(!masterKey || masterKey == null){
console.log('Attempting to search wiouth key')
return resolve(null)
}
//Search the search index
db.promise().query(`SELECT * FROM user_encrypted_search_index WHERE user_id = ? LIMIT 1`, [userId])
.then((rows, fields) => {
if(rows[0].length == 1){
//Lookup, decrypt and parse search index
const row = rows[0][0]
const decipheredSearchIndex = cs.decrypt(masterKey, row.salt, row.index)
const searchIndex = JSON.parse(decipheredSearchIndex)
//Clean up search word
const word = searchQuery.toLowerCase().replace(/[^a-z0-9]/g, '')
let noteIds = []
let partials = []
Object.keys(searchIndex).forEach(wordIndex => {
if( wordIndex.indexOf(word) != -1 && wordIndex != word){
partials.push(wordIndex)
noteIds.push(...searchIndex[wordIndex])
}
})
const exactArray = searchIndex[word] ? searchIndex[word] : []
let searchData = {
'word':word,
'exact': exactArray,
'partials': partials,
'partial': [...new Set(noteIds) ],
}
//Remove exact matches from partials set if there is overlap
if(searchData['exact'].length > 0 && searchData['partial'].length > 0){
searchData['partial'] = searchData['partial']
.filter( ( el ) => !searchData['exact'].includes( el ) )
}
searchData['ids'] = searchData['exact'].concat(searchData['partial'])
searchData['total'] = searchData['ids'].length
// console.log(searchData['total'])
return resolve({ 'ids':searchData['ids'] })
} else {
return resolve(null)
}
})
}
})
}
Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => {
return new Promise((resolve, reject) => {
//Define return data objects
let returnData = {
'notes':[],
'total':0,
}
let userPrivateKey = null
let User = require('@models/User')
User.generateKeypair(userId, masterKey)
.then(({publicKey, privateKey}) => {
userPrivateKey = privateKey
return Note.encryptedIndexSearch(userId, searchQuery, searchTags, masterKey)
})
.then( (textSearchResults) => {
//Pull out search results from previous query
let textSearchIds = []
let returnTagResults = false
let searchAllNotes = false
if(textSearchResults != null){
textSearchIds = textSearchResults['ids']
returnData['total'] = textSearchIds.length
// highlights = textSearchResults['snippets']
}
//No results, return empty data
if(textSearchIds.length == 0 && searchQuery.length > 0){
return resolve(returnData)
}
// Base of the query, modified with fastFilters
// Add to query for character counts -> CHAR_LENGTH(note.text) as chars
//SUBSTRING(note_raw_text.text, 1, 500) as text,
let searchParams = [userId]
let noteSearchQuery = `
SELECT note.id,
note.snippet as snippet,
note.snippet_salt as salt,
note_raw_text.updated as updated,
opened,
color,
count(distinct note_tag.id) as tag_count,
count(distinct attachment.id) as attachment_count,
note.pinned,
note.archived,
note.encrypted,
GROUP_CONCAT(DISTINCT tag.text) as tags,
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
shareUser.username as shareUsername,
note.shared,
note.encrypted_share_password_key
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
LEFT JOIN note_tag ON (note.id = note_tag.note_id)
LEFT JOIN tag ON (tag.id = note_tag.tag_id)
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1)
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
WHERE note.user_id = ?
`
//Show shared notes
if(fastFilters.onlyShowSharedNotes == 1){
//share_user_id means your shared them, a note with a shared user id filled in means it was shared
noteSearchQuery += ` AND share_user_id IS NOT NULL OR (note.shared = 2 AND note.user_id = ?)`
searchParams.push(userId)
//Show notes shared with you
} else {
noteSearchQuery += ' AND note.share_user_id IS NULL'
}
//If text search returned results, limit search to those ids
if(textSearchIds.length > 0){
searchParams.push(textSearchIds)
noteSearchQuery += ' AND note.id IN (?)'
searchAllNotes = true
}
if(fastFilters.noteIdSet && fastFilters.noteIdSet.length > 0){
searchParams.push(fastFilters.noteIdSet)
noteSearchQuery += ' AND note.id IN (?)'
searchAllNotes = true
}
//Encrypted Note
if(fastFilters.onlyShowEncrypted == 1){
noteSearchQuery += ' AND encrypted = 1'
searchAllNotes = true
}
//If tags are passed, use those tags in search
if(searchTags.length > 0){
searchParams.push(searchTags)
noteSearchQuery += ' AND note_tag.tag_id IN (?)'
}
//Show archived notes, only if fast filter is set, default to not archived
if(searchAllNotes == false){
if(fastFilters.onlyArchived == 1){
noteSearchQuery += ' AND note.archived = 1' //Show Archived
} else {
noteSearchQuery += ' AND note.archived = 0' //Exclude archived
}
}
//Finish up note query
noteSearchQuery += ' GROUP BY note.id'
//Only show notes with Tags
if(fastFilters.withTags == 1){
returnTagResults = true
noteSearchQuery += ' HAVING tag_count > 0'
}
//Only show notes with links
if(fastFilters.withLinks == 1){
returnTagResults = true
noteSearchQuery += ' HAVING attachment_count > 0'
}
//Only show archived notes
if(fastFilters.onlyArchived == 1){
returnTagResults = true
noteSearchQuery += ' HAVING note.archived = 1'
}
//
// Always prioritize pinned notes in searches.
//Default Sort, order by last updated
let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, note.created DESC, note.opened DESC, id DESC'
//Order by Last Created Date
if(fastFilters.lastCreated == 1){
defaultOrderBy = ' ORDER BY note.pinned DESC, note.created DESC, updated DESC, note.opened DESC, id DESC'
}
//Order by last Opened Date
if(fastFilters.lastOpened == 1){
defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, updated DESC, note.created DESC, id DESC'
}
//Append Order by to query
noteSearchQuery += defaultOrderBy
//Manage limit params if set
if(fastFilters.limitSize > 0 || fastFilters.limitOffset > 0){
const limitSize = parseInt(fastFilters.limitSize, 10) || 10 //Use int or default to 10
const limitOffset = parseInt(fastFilters.limitOffset, 10) || 0 //Either parse int, or use zero
// console.log(` LIMIT ${limitOffset}, ${limitSize}`)
noteSearchQuery += ` LIMIT ${limitOffset}, ${limitSize}`
}
// console.log('------------- Final Query --------------')
// console.log(noteSearchQuery)
// console.log('------------- ----------- --------------')
db.promise()
.query(noteSearchQuery, searchParams)
.then((noteRows, noteFields) => {
//Current note key may change, default to master key
let currentNoteKey = masterKey
//Push all notes
returnData['notes'] = noteRows[0]
//pull out all note ids so we can fetch all tags for those notes
let noteIds = []
returnData['notes'].forEach(note => {
//Grab note ID for finding tags
noteIds.push(note.id)
//Shared notes use encrypted key - decrypt key then decrypt note
const encryptedShareKey = note.encrypted_share_password_key
if(encryptedShareKey != null){
currentNoteKey = crypto.privateDecrypt(userPrivateKey,
Buffer.from(encryptedShareKey, 'base64') )
}
//Decrypt note text
if(note.snippet && note.salt){
const decipheredText = cs.decrypt(currentNoteKey, note.salt, note.snippet)
const textObject = JSON.parse(decipheredText)
if(textObject != null && textObject.length == 2){
note.title = textObject[0]
note.text = textObject[1]
}
}
//Deduce note title
const textData = ProcessText.deduceNoteTitle(note.title, note.text)
note.title = textData.title
note.subtext = textData.sub
//Remove these variables
note.note_highlights = []
note.attachment_highlights = []
note.tag_highlights = []
//Limit number of attachment thumbs to 4
if(note.thumbs){
//Convert comma delimited string to array
let thumbArray = note.thumbs.split(',').reverse()
//Limit array to 4 or size of array
thumbArray.length = Math.min(thumbArray.length, 4)
note.thumbs = thumbArray
}
//Clear out note.text before sending it to front end, its being used in title and subtext
delete note.snippet
delete note.salt
})
return resolve(returnData)
})
.catch(console.log)
})
.catch(console.log)
})
}