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;
};