NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name RPH Log Manager // @namespace https://openuserjs.org/scripts/shuffyiosys/RPH_Log_Manager // @updateURL https://openuserjs.org/meta/shuffyiosys/RPH_Log_Manager.meta.js // @match https://chat.rphaven.com/ // @version 1.0.5 // @description Imports and exports private message logs from RPH // @copyright (c)2021 shuffyiosys@github // @grant none // @license MIT // ==/UserScript== const VERSION_STRING = "1.0.5"; const INDEXED_DB_VERS = 20; let request let logDb let fileContent let logDbDump = {} let logEntryDump = {} let logEntries = {} let idsToNames = {} let searchName = ""; let exactSearch = false; let deleteTimer = null; let byUsername = false; $(function () { const labelStyle = `padding-left: 0px; text-align:justify; display:inline-block; cursor:default;`; const spacingStyle = `width: 240px;`; const logContentStyle = `border:#888 solid 1px;border-radius:10px padding-bottom:12px;margin-bottom:12px; width: 100%; height: 720px; overflow: auto;`; const html = { tabId: 'logExporter', tabName: 'Logs', tabContents: ` <h5>Log Manager</h5><br /> <p><strong>Version: ${VERSION_STRING} </strong> | <a href="https://openuserjs.org/install/shuffyiosys/RPH_Log_Manager.user.js" target="_blank">Install the latest version</a> | <a href="https://openuserjs.org/scripts/shuffyiosys/RPH_Log_Manager" target="_blank">OpenUserJs page</a> | <a href="https://discord.gg/HBEaGjs" target="_blank">Discord channel</a> | <a href="https://github.com/shuffyiosys/rph-log-tools/blob/main/help.md" target="_blank">How to use</a> </p> <div id="log-import-container"> <h4>Log Importer</h4><br /> <input id="logFileInput" type="file" /> <button style="display: none;" id="retryImportButton">Retry</button><br /><br /> <p id="importStatus"></p> </div> <div id="log-export-container"> <h4>Log Exporter</h4><br /> <p><strong>Search Options</strong></p><br /> <p style="line-height: 2em;"> <label class="rphlm-label rphlm-spacing">Date</label> <input type="date" id="startDateInput" name="startDate" style="min-width: 0px;"> to <input type="date" id="endDateInput" name="endDate" style="min-width: 0px;"> </p> <p style="line-height: 2em;"> <label class="rphlm-label rphlm-spacing" for="searchNameInput">Name search</label> <input style="width: 360px; min-width: initial;" id="searchNameInput" type="text"> </p> <p style="line-height: 2em;"> <label class="rphlm-label rphlm-spacing" for="exactSearchCheckbox">Exact name search</label> <input id="exactSearchCheckbox" type="checkbox"> </p> <p style="line-height: 2em;"> <label class="rphlm-label rphlm-spacing" for="reverseNamesCheckbox">Select by your name first</label> <input id="reverseNamesCheckbox" type="checkbox"> </p> <p style="line-height: 2em;"> <label class="rphlm-label rphlm-spacing"></label> <button id="getLogsButton">Get logs</button> </p> <hr> <div id="logEntriesContainer" style="display: none"> <div id="downloadLinks"> <p><strong>Log Management</strong></p><br /> <p>Download: <a id="downloadPlainTextLink">Download log as plaintext</a> | <a id="downloadJsonLink">Export log for importing</a> | <a id="downloadAllLink">Download all logs</a> </p> <br> <p> Delete: <button id="deleteButton" style="background:red">Delete this log</button> | <button id="deleteFromNameButton" style="background:red">Delete logs from...</button> </p> </div> <hr style="margin-top: 20px;" /> <p><strong>View log</strong></p><br /> <label id="logFirstName" class="rphlm-label rphlm-spacing">Others name </label> <select class="rphlm-spacing" id="nameOneDropList"></select> <a id="yourProfileLink" style="margin-left: 10px; display: none;" target="_blank">See profile</a><br /><br /> <label id="logSecondName" class="rphlm-label rphlm-spacing">Your name </label> <select class="rphlm-spacing" id="nameTwoDropList"></select> <a id="otherProfileLink" style="margin-left: 10px; display: none;" target="_blank">See profile</a><br /><br /> <div class="rphlm-logContent" id="log-contents"></div> </div> </div>` }; const rphLogManagerCss = `<style> .rphlm-label {padding-left: 0px; text-align:justify; display:inline-block; cursor:default;} .rphlm-spacing {width: 240px;} .rphlm-logContent {border:#888 solid 1px;border-radius:10px padding-bottom:12px;margin-bottom:12px; width: 100%; height: 720px; overflow: auto;} .dropdownContainer {display:inline-block; min-width: 280px; max-width: 280px;} .dropdownOptions { max-height: 320px; overflow-y: auto; position: absolute; width: 230px; display: none; background: #f6f6f6;} .dropdownOptions > a {padding: 12px 16px; text-decoration: none; display: block;} </style> ` $('head').append(rphLogManagerCss); const $settingsDialog = $('#settings-dialog') $('#settings-dialog .inner ul.tabs').append('<h3>Log Manager</h3>'); $('#settings-dialog .inner ul.tabs').append(`<li><a href="#${html.tabId}">${html.tabName}</a></li>`); $('#settings-dialog .inner div.content div.inner').append(`<div id="${html.tabId}" style="display: none;">${html.tabContents}</div>`); $settingsDialog.find('.tabs a[href="#' + html.tabId + '"]').click( function (ev) { $settingsDialog.find('.content .inner > div').hide(); $settingsDialog.find($(this).attr('href')).show(); ev.preventDefault(); }); $('#logFileInput').change(handleFileInput); $('#retryImportButton').click(() => { $('#retryImportButton').hide(); loadLogFile(fileContent); }); $('#getLogsButton').click(getLogs); $('#nameOneDropList').change(updateDropdownLists); $('#nameTwoDropList').change(() => { const otherName = $('#nameTwoDropList option:selected').val() $('#otherProfileLink').attr('href', `https://profiles.rphaven.com/${otherName}`) fillInLogContents(); }); $('#deleteButton').click(() => { handleDelete('#deleteButton', 'Delete logs from...', deleteLog); }) $('#deleteFromNameButton').click(() => { handleDelete('#deleteFromNameButton', 'Delete this log', deleteLogsByName); }) socket.on('account-users', createLogDatabase); }); /** UI related functions *****************************************************/ function handleFileInput() { let file = $("#logFileInput")[0].files[0]; (async () => { fileContent = await file.text(); loadLogFile(fileContent) })(); } function handleDelete(elementId, defaultText, deleteFunction) { if (typeof (deleteFunction) !== 'function') { return; } if (deleteTimer === null) { $(elementId).html('Press again to delete...'); deleteTimer = setTimeout(() => { deleteTimer = null; $(elementId).html(defaultText); }, 5000); } else { deleteFunction().then(() => { return 1; }); clearTimeout(deleteTimer); deleteTimer = null; } } function handleNameInputSearch(event) { let input = document.getElementById(event.target.id); let filter = input.value.toUpperCase(); let div = document.getElementById(event.target.parentElement.id); let a = div.getElementsByTagName("a"); for (let i = 0; i < a.length; i++) { txtValue = a[i].textContent || a[i].innerText; if (txtValue.toUpperCase().indexOf(filter) > -1) { a[i].style.display = ""; } else { a[i].style.display = "none"; } } } function fillInLogContents() { const username = $('#nameOneDropList option:selected').val() const otherName = $('#nameTwoDropList option:selected').val() const entry = logEntries[username][otherName] $('#log-contents').empty() for (let timestamp in entry) { $('#log-contents').append(`<p>${createTimestamp(parseInt(timestamp))} ${entry[timestamp].author}: ${entry[timestamp].msg}</p>`) logEntryDump[entry[timestamp].dBkey] = logDbDump[entry[timestamp].dBkey] } $('#downloadPlainTextLink').attr('href', makeTextFile($('#log-contents').html().replace(/<p>/g, '').replace(/<\/p>/g, '\n'))); $('#downloadPlainTextLink').attr('download', `${username}-${otherName}-log.txt`) $('#downloadJsonLink').attr('href', makeTextFile(JSON.stringify(logEntryDump, null, 4))); $('#downloadJsonLink').attr('download', `${username}-${otherName}-log.json`); $('#deleteFromNameButton').text(`Delete logs from ${username}`); } function updateDropdownLists() { const username = $('#nameOneDropList option:selected').val(); const otherNames = Object.keys(logEntries[username]).sort(); $('#nameTwoDropList').empty(); otherNames.forEach((name) => { addToDroplist(name, name, "#nameTwoDropList"); }); const otherName = $('#nameTwoDropList option:selected').val(); $('#yourProfileLink').attr('href', `https://profiles.rphaven.com/${username}`); $('#otherProfileLink').attr('href', `https://profiles.rphaven.com/${otherName}`); $('#yourProfileLink').show(); $('#otherProfileLink').show(); fillInLogContents(); } function refreshNameDropLists() { $('#nameOneDropList').empty(); const names = Object.keys(logEntries).sort(); names.forEach(name => { addToDroplist(name, name, '#nameOneDropList') }); updateDropdownLists(); } function addToDroplist(value, label, droplist) { let droplist_elem = $(droplist) droplist_elem.append($('<option>', { value: value, text: label })) } const toggleableElements = [ '#getLogsButton', '#searchNameInput', '#exactSearchCheckbox', '#reverseNamesCheckbox', '#nameOneDropList', '#nameTwoDropList', '#deleteButton', '#deleteFromNameButton' ] function disableControls() { toggleableElements.forEach(element => { $(element).prop("disabled", true); }); $('#downloadPlainTextLink').removeAttr('href download'); $('#downloadJsonLink').removeAttr('href download'); $('#downloadAllLink').removeAttr('href download'); } function enableControls() { toggleableElements.forEach(element => { $(element).prop("disabled", false); }); } /* Functions related to database manipulation ********************************/ function createLogDatabase() { // If this database was not created, create it. request = indexedDB.open(`${account.props.accid}`, INDEXED_DB_VERS) request.onupgradeneeded = function (event) { logDb = event.target.result logDb.onerror = function (event) { console.log(event) }; let newObjectStore = logDb.createObjectStore("msgs", { keyPath: [ ["date", "fromid", "userid"], "userid", ["userid", "otherid"], ["fromid", "date"], "date" ] }); newObjectStore.transaction.oncomplete = () => { logDb.close() } } } function getLogs() { logEntries = {}; logDbDump = {}; byUsername = $('#reverseNamesCheckbox').is(":checked"); searchName = $('input#searchNameInput').val(); if ($('#exactSearchCheckbox').is(":checked") == true) { getUserByName(searchName) .then(() => { startSearch(); }) .catch(() => { $('input#searchNameInput').css('background', '#FF7F7F'); }) } else { startSearch(); } } function startSearch() { $('input#searchNameInput').css('background', ''); $('#log-contents').empty(); $('#nameTwoDropList').empty(); $('#nameOneDropList').empty(); $('#logEntriesContainer').show(); if (byUsername == true) { $(`label#logFirstName`).first().text("Your name"); $(`label#logSecondName`).first().text("Other's name"); } else { $(`label#logFirstName`).first().text("Other's name"); $(`label#logSecondName`).first().text("Your name"); } request = indexedDB.open(`${account.props.accid}`, INDEXED_DB_VERS); request.onsuccess = function (event) { logDb = event.target.result; logDb.transaction(['msgs']).objectStore('msgs').openCursor().onsuccess = processLogEntry; }; } function processLogEntry(event) { const startTime = (isNaN($('#startDateInput')[0].valueAsNumber)) ? 0 : $('#startDateInput')[0].valueAsNumber; const endTime = (isNaN($('#endDateInput')[0].valueAsNumber)) ? Date.now() : $('#endDateInput')[0].valueAsNumber; let cursor = event.target.result; $('#getLogsButton').html("Getting logs..."); disableControls(); if (!cursor || (cursor && cursor.value.date > endTime)) { let link = $('#downloadAllLink'); link.attr('href', makeTextFile(`${JSON.stringify(logDbDump, null, 4)}`)); link.attr('download', `${account.props.accid}-all-logs.txt`); setTimeout(() => { $('#getLogsButton').html("Get logs"); enableControls(); if(Object.keys(logEntries).length > 0) { refreshNameDropLists(); } }, 100) return; } let logEntry = cursor.value; let key = cursor.key.join(); if ((Math.log(logEntry.date) * Math.LOG10E + 1 | 0) < 11) { logEntry.date *= 1000; } if (startTime > logEntry.date) { cursor.continue(); } else { logDbDump[key] = cursor.value; if ((logEntry.otherid in idsToNames) && (logEntry.fromid in idsToNames) && (logEntry.userid in idsToNames)) { logEntry.other_name = idsToNames[logEntry.otherid]; logEntry.from_name = idsToNames[logEntry.fromid]; logEntry.user_name = idsToNames[logEntry.userid]; addLogEntry(logEntry); } else { getUserById(logEntry.otherid) .then((user) => { logEntry.other_name = user.props.name; idsToNames[user.props.id] = user.props.name; return getUserById(logEntry.fromid); }) .then((user) => { logEntry.from_name = user.props.name; idsToNames[user.props.id] = user.props.name; return getUserById(logEntry.userid); }) .then((user) => { logEntry.user_name = user.props.name; idsToNames[user.props.id] = user.props.name; addLogEntry(logEntry, key); return Promise.resolve(); }) } cursor.continue(); } } function addLogEntry(logEntry, key) { const username = ($('#reverseNamesCheckbox').is(":checked")) ? logEntry.user_name : logEntry.other_name; const otherName = ($('#reverseNamesCheckbox').is(":checked")) ? logEntry.other_name : logEntry.user_name; if (searchName.length > 0) { const reTerm = new RegExp(searchName, ((exactSearch ? `i` : ``))); if(username.search(reTerm) == -1 && otherName.search(reTerm) == -1) { return; } } if (username in logEntries === false) { logEntries[username] = {}; /* Sort names as they come in */ let options = $('#nameOneDropList option'); let arr = options.map(function (_, o) { return { t: $(o).text(), v: o.value }; }).get(); arr.sort(function (o1, o2) { return o1.t > o2.t ? 1 : o1.t < o2.t ? -1 : 0; }); options.each(function (i, o) { o.value = arr[i].v; $(o).text(arr[i].t); }); } if (otherName in logEntries[username] === false) { logEntries[username][otherName] = {}; } logEntries[username][otherName][logEntry.date] = { author: logEntry.from_name, msg: logEntry.msg, dBkey: key } } function loadLogFile(jsonString) { $('#importStatus').text('Importing log...') try { request = indexedDB.open(`${account.props.accid}`, INDEXED_DB_VERS); request.onsuccess = function (event) { logDb = event.target.result processLogFile(JSON.parse(jsonString)) }; } catch (e) { $('#importStatus').text('Error importing log') $('#retryImportButton').show() console.log(e); } } function processLogFile(jsonBlob) { let tx = logDb.transaction("msgs", 'readwrite') let store = tx.objectStore("msgs") for (let key in jsonBlob) { let keypath = key.split(',') for (let i = 0; i < 3; i++) { keypath[i] = parseInt(keypath[i]) } store.put({ id: key, date: jsonBlob[key].date, fromid: jsonBlob[key].fromid, userid: jsonBlob[key].userid, otherid: jsonBlob[key].otherid, msg: jsonBlob[key].msg }) } /* Remove the ID key in each log to conform with how RPH stores logs */ tx = logDb.transaction('msgs', 'readwrite') store = tx.objectStore('msgs').openCursor(null, 'prev').onsuccess = function (event) { var cursor = event.target.result if (cursor && ("id" in cursor.value)) { delete cursor.value.id; cursor.update(cursor.value); cursor.continue() } } tx.oncomplete = () => { $('#importStatus').text('Importing done!') }; } async function deleteLog() { let otherUser = await getUserByName($('#nameOneDropList option:selected').val()); let acctUser = await getUserByName($('#nameTwoDropList option:selected').val()); if (byUsername == true) { acctUser = await getUserByName($('#nameOneDropList option:selected').val()); otherUser = await getUserByName($('#nameTwoDropList option:selected').val()); } $('#deleteButton').html("Deleting logs..."); disableControls(); let tx = logDb.transaction('msgs', 'readwrite'); let store = tx.objectStore('msgs').openCursor(null, 'prev').onsuccess = function (event) { var cursor = event.target.result; if (!cursor) { let primaryKey = (byUsername === true) ? acctUser.props.name : otherUser.props.name; let secondaryKey = (byUsername === true) ? otherUser.props.name : acctUser.props.name; delete logEntries[primaryKey][secondaryKey]; if (Object.keys(logEntries[primaryKey]).length === 0) { delete logEntries[primaryKey]; } refreshNameDropLists(); enableControls(); $('#deleteButton').html("Delete this log"); if (primaryKey in logEntries) { $(`#nameOneDropList option[value=${primaryKey}]`).prop('selected', true); updateDropdownLists(); } return } else if (cursor.value.userid == acctUser.props.id && cursor.value.otherid == otherUser.props.id) { cursor.delete(); } cursor.continue(); } } async function deleteLogsByName() { let userData = await getUserByName($('#nameOneDropList option:selected').val()) $('#deleteFromNameButton').html("Deleting logs..."); disableControls() let tx = logDb.transaction('msgs', 'readwrite') let store = tx.objectStore('msgs').openCursor(null, 'prev').onsuccess = function (event) { var cursor = event.target.result if (!cursor) { $('#deleteFromNameButton').html("Delete logs from this name"); delete logEntries[userData.props.name]; enableControls() refreshNameDropLists(); return; } else if (cursor.value.otherid == userData.props.id) { cursor.delete() } cursor.continue() } } /** Utility functions ********************************************************/ function createTimestamp(time) { const timestamp = new Date(time); const dateString = timestamp.toLocaleDateString(navigator.language); const timeString = timestamp.toTimeString().substring(0, 5); return `${dateString} ${timeString}`; } function makeTextFile(text) { let textFile = null; let data = new Blob([text], { type: 'text/plain' }); // If we are replacing a previously generated file we need to // manually revoke the object URL to avoid memory leaks. if (textFile !== null) { window.URL.revokeObjectURL(textFile); } textFile = window.URL.createObjectURL(data); return textFile; };