NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name RPH Tools
// @namespace https://openuserjs.org/scripts/shuffyiosys/RPH_Tools
// @version 4.4.5
// @description Adds extended settings to RPH
// @match https://chat.rphaven.com/
// @copyright (c)2014 shuffyiosys@github
// @grant none
// @updateURL https://openuserjs.org/meta/shuffyiosys/RPH_Tools.meta.js
// @license MIT
// ==/UserScript==
const VERSION_STRING = "4.4.5";
const SETTINGS_NAME = "rph_tools_settings";
/**
* Marks an HTML element with red or white if there's a problem
* @param {string} element Full selector of the HTML element to mark
* @param {boolean} mark If the mark is for good or bad
*/
function markProblem(element, mark) {
if (mark === true) {
$(element).css("background", "#FF7F7F");
}
else {
$(element).css("background", "#FFF");
}
}
/**
* Checks to see if an input is valid or not and marks it accordingly
* @param {string} settingId Full selector of the HTML element to check
* @param {string} setting What kind of setting is being checked
* @return If the input is valid or not
*/
function validateSetting(settingId, setting) {
let validInput = false;
let input = $(settingId).val();
if (setting === "url") {
validInput = validateUrl(input);
}
else if (setting === "color") {
input = input.replace("#", "");
validInput = validateColor(input);
}
markProblem(settingId, !validInput);
return validInput;
}
/**
* Makes sure the color input is a valid hex color input
* @param {string} color Color input
* @returns If the color input is valid
*/
function validateColor(color) {
let pattern = new RegExp(/([0-9A-Fa-f]{6}$)|([0-9A-Fa-f]{3}$)/i);
return pattern.test(color);
}
/**
* Makes sure the URL input is valid
* @param {string} url URL input
* @returns If the URL input is valid
*/
let validateUrl = function (url) {
if (url === "") {
return true;
}
else {
let regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
return regexp.test(url) === true;
}
};
/**
* Adds an option to a select element with a value and its label
* @param {string} value Value of the option element
* @param {string} label Label of the option element
* @param {object} droplist Which select element to add option to
*/
function addToDroplist(value, label, droplist) {
let droplist_elem = $(droplist);
droplist_elem.append(
$("<option>", {
value: value,
text: label,
})
);
}
/**
* In an array of objects, return the first instance where a key matches the
* value being searched.
* @param {array} objArray Array of objects
* @param {*} key Key to look for
* @param {*} value Value of the key to match
* @return Index of the first instance where the key matches the value, -1
* otherwise.
*/
function arrayObjectIndexOf(objArray, key, value) {
for (let i = 0; i < objArray.length; i++) {
if (objArray[i][key] === value) {
return i;
}
}
return -1;
}
/**
* Sorts the account's username list to alphabetical order
*/
function getSortedNames() {
let namesToIds = {};
account.users.forEach(function (userObj) {
namesToIds[userObj.props.name] = userObj.props.id;
});
let sorted = [];
for (let key in namesToIds) {
sorted[sorted.length] = key;
}
sorted.sort();
let tempDict = {};
for (let i = 0; i < sorted.length; i++) {
tempDict[sorted[i]] = namesToIds[sorted[i]];
}
namesToIds = tempDict;
return namesToIds;
}
/**
* Generates a randum number using the Linear congruential generator algorithm
* @param {*} seed - RNG seed value
*/
function LcgRng(seed, init = true) {
if (init) {
seed = seed % 2147483647;
if (seed <= 0) {
seed += 2147483646;
}
}
return (seed * 16807) % 2147483647;
}
function calculateDiceRolls(dieNum, dieSides, seed) {
let results = [];
let result = LcgRng(seed);
results.push((result % dieSides) + 1);
for (let die = 1; die < dieNum; die++) {
result = LcgRng(result, false);
results.push((result % dieSides) + 1);
}
total = results.reduce((a, b) => a + b, 0);
return {
results: results,
total: total,
};
}
function generateRngResult(command, message, seed) {
const MSG_ARGS = message.split(/ /gm);
let resultMsg = "";
if (command === "rng-coinflip") {
const outcomes = ["heads", "tails"];
resultMsg += `flips a coin. It lands on... ${outcomes[LcgRng(seed) % 2]}!`;
}
else if (command === "rng-roll") {
let diceArgs;
if (MSG_ARGS.length === 1) {
diceArgs = [1, 20];
}
else {
diceArgs = parseRoll(MSG_ARGS[1]);
}
const result = calculateDiceRolls(diceArgs[0], diceArgs[1], seed);
resultMsg += `rolled ${diceArgs[0]}d${diceArgs[1]}: ${result["results"].join(" ")} (total: ${result.total})`;
}
else if (command === "rng-rps") {
const outcomes = ["Rock!", "Paper!", "Scissors!"];
resultMsg += `plays Rock, Paper, Scissors and chooses... ${outcomes[
Math.ceil(Math.random() * 3) % 3
].toString()}`;
}
return resultMsg;
}
function parsePostCommand(message) {
let command = "";
if (message.startsWith("/roll")) {
command = "rng-roll";
}
else if (message.startsWith("/coinflip")) {
command = "rng-coinflip";
}
else if (message.startsWith("/rps")) {
command = "rng-rps";
}
else if (message.startsWith("/me")) {
command = "me";
}
return command;
}
/**
* Gets the list of vanity names and maps them to an ID
*/
function getVanityNamesToIds() {
let vanityToIds = {};
for (let user in messenger.users) {
let vanityName = messenger.users[user].props.vanity;
if (vanityName) vanityToIds[vanityName] = user;
}
return vanityToIds;
}
function parseRoll(rollArgs) {
const DIE_NUM_MIN = 1;
const DIE_NUM_MAX = 100;
const DIE_SIDE_MIN = 2;
const DIE_SIDE_MAX = 1000000;
let die = 1;
let sides = 20;
die = parseInt(rollArgs.split("d")[0]);
sides = parseInt(rollArgs.split("d")[1]);
die = Math.min(Math.max(die, DIE_NUM_MIN), DIE_NUM_MAX);
sides = Math.min(Math.max(sides, DIE_SIDE_MIN), DIE_SIDE_MAX);
return [die, sides];
}
function getCssRoomName(roomName) {
return roomName.replace(/[^a-z0-9]/g, function (s) {
var c = s.charCodeAt(0);
if (c === 32) return "-";
if (c >= 65 && c <= 90) return "" + s.toLowerCase();
return ("000" + c.toString(16)).slice(-4);
});
}
function displayNotification(message) {
if (document.hidden) {
let notification = new Notification(message);
setTimeout(() => {
notification.close();
}, 6000);
}
}
function createTimestamp(time) {
const timestamp = new Date(time);
const dateString = timestamp.toLocaleDateString(navigator.language, {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
const delim = dateString.indexOf("/", 3);
const timeString = timestamp.toTimeString().substring(0, 5);
return `${dateString.substring(0, delim)} ${timeString}`;
}
/**
* Generates a hash value for a string
* This was modified from https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery
*/
String.prototype.hashCode = function () {
let hash = 0,
i,
chr,
len;
if (this.length === 0) return hash;
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = (hash << 31) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
/****
* This module handles the chat functions of the script.
****/
let chatModule = (function () {
const chatLogsStorageName = "chatLogs";
const AUTOJOIN_TIMEOUT_SEC = 5 * 1000;
const MAX_ROOMS = 30;
const AUTOJOIN_INTERVAL = 2 * 1000;
const RNG_TIMEOUT = 30 * 1000;
const ALERT_HIGHLIGHT = `background: #F00; color: #FFF; font-weight: bold;`;
const html = {
tabId: "chat-module",
tabName: "Chat",
tabContents: `
<h3>Chat Options</h3><br/>
<h4>Appearance & Behavior</h4>
<div class="rpht-option-block">
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="snapRoomListEnable">Snap to room list to room</label>
<label class="switch"><input type="checkbox" id="snapRoomListEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">When you select a chat tab, snap the room list to the room and expand it if it's collapsed</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="chatColorSelection">Stylize user's messages</label>
<select style="float: right; width: 110px;" id="chatColorSelection">
<option value="0">None</option>
<option value="1" selected>Highlight speech</option>
<option value="2">Everything</option>
</select>
<label class="rpht-label descript-label">Changes the color of user's messages</label>
</div>
<div class="rpht-option-section">
<label style="font-weight: bold; width:522px; padding: 0px;" for="unreadMarkerSelection">Mark rooms with unread messages</label>
<select style="float: right; width: 110px;" id="unreadMarkerSelection">
<option value="0">No marker</option>
<option value="1" selected>Simple</option>
<option value="2"># unread</option>
</select>
<label class="rpht-label descript-label">Marks room tabs with unread messages</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="chatmsgPaddingEnable">Add padding between messages</label>
<label class="switch"><input type="checkbox" id="chatmsgPaddingEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Adds padding between messages to increase readibility</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="showCommandWindowEnable">Show command window</label>
<label class="switch"><input type="checkbox" id="showCommandWindowEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Shows the command window when typing a command</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="removeHighlightingEnable">Remove message highlighting</label>
<label class="switch"><input type="checkbox" id="removeHighlightingEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Removes the subtle highlighting from certain chat messages</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="enableTabSwitch">Enable tab switch hotkey</label>
<label class="switch"><input type="checkbox" id="enableTabSwitch"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Press Alt + Shift + Left/Right to switch between tabs
This will not work if you have auto-sorting on in the UI options</label>
</div>
<div class="rpht-option-section option-section-bottom">
<label class="rpht-label checkbox-label" for="enableImagePreview">Enable image previews</label>
<label class="switch"><input type="checkbox" id="enableImagePreview"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Links to images show up in chat. This may not work for some links.</label>
</div>
</div>
<h4>Chat Pinging</h4>
<div class="rpht-option-block">
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="notifyPingEnable">Enable pings</label>
<label class="switch"><input type="checkbox" id="notifyPingEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label"> Turns on ping notifications in chat</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="selfPingEnable">Can ping yourself</label>
<label class="switch"><input type="checkbox" id="selfPingEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Pings will trigger on your own messages</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="notifyNotificationEnable">Enable desktop notifications</label>
<label class="switch"><input type="checkbox" id="notifyNotificationEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Pops up a notification when you get pinged</label>
</div>
<div class="rpht-option-section">
<p>Names to be pinged (comma separated)</p>
<textarea id="pingNames" rows="8" class="rpht_textarea"> </textarea>
</div>
<div class="rpht-option-section">
<label><strong>Ping sound URL</strong></label><br>
<input type="text" class="rpht-long-input" id="pingURL"><br><br>
<label class="rpht-label descript-label">URL to an audio file, or leave blank for no sound</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="pingExactMatch">Exact match</label>
<label class="switch"><input type="checkbox" id="pingExactMatch"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">e.g., If pinging on "Mel", matches on "Mel" and not "Melody"</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="pingCaseSense">Case sensitive</label>
<label class="switch"><input type="checkbox" id="pingCaseSense"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">e.g., If pinging on "Mel", matches on "Mel" and not "mel"</label>
</div>
<div class="rpht-option-section">
<h4>Ping styling</h4>
<label class="rpht-label text-input-label">Text Color</label><input type="text" class="rpht-short-input" id="pingTextColor" value="#000"><br /><br />
<label class="rpht-label text-input-label">Highlight</label><input type="text" class="rpht-short-input" id="pingHighlightColor" value="#FFA"><br><br>
<label class="rpht-label checkbox-label" style="font-weight:initial;" for="pingBoldEnable">Add <strong>bold</strong></label>
<label class="switch"><input type="checkbox" id="pingBoldEnable"><span class="rpht-slider round"></span></label><br><br>
<label class="rpht-label checkbox-label" style="font-weight:initial;" for="pingItalicsEnable">Add <em>Italics</em></label>
<label class="switch"><input type="checkbox" id="pingItalicsEnable"><span class="rpht-slider round"></span></label>
</div>
<div class="rpht-option-section option-section-bottom">
<label class="rpht-label checkbox-label">Ping Tester: </label>
<input type="text" class="rpht-long-input" id="pingPreviewInput" placeholder="Enter ping word to test"><br /><br />
<label>Ping preview:</label><span id="pingPreviewText"></span>
</div>
</div>
<h4>Auto Joining</h4>
<div class="rpht-option-block">
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="trackSession">Sessioning</label>
<label class="switch"><input type="checkbox" id="trackSession"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Keeps track of which rooms you were in, then rejoins them when you log in again.</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="joinFavEnable">Join favorites</label>
<label class="switch"><input type="checkbox" id="joinFavEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Join rooms that are in the favorite rooms list</label>
</div>
<div class="rpht-option-section option-section-bottom">
<h4>Favorite Rooms</h4>
<label class="rpht-label split-input-label">Username </label><select class="split-input-label" id="favUserDropList"></select><br /><br />
<label class="rpht-label split-input-label">Room </label><input class="split-input-label" type="text" id="favRoom" name="favRoom"><br /><br />
<label class="rpht-label split-input-label">Password</label><input class="split-input-label" type="text" id="favRoomPw" name="favRoomPw"><br /><br />
<button style="width: 60px; float:right;" type="button" id="favAdd">Add</button><br /><br />
<select style="width: 100%;" id="favRoomsList" size="10"></select><br><br>
<button style="float:right;" type="button" id="favRemove">Remove</button><br />
</div>
</div>`,
};
const CHAT_COMMANDS = new(function () {
this.away = `<tr><td><code>/away [message]</code></td><td style="padding-bottom:10px;">Sets your status to "Away" and the status message<br>Example: <code>/away I'm away</code></td></tr>`;
this.coinflip = `<tr><td><code>/coinflip</code></td><td style="padding-bottom:10px;">Performs a coin flip</td></tr>`;
this.leave = `<tr><td><code>/leave</code></td><td style="padding-bottom:10px;">Leaves the current room</td></tr>`;
this.me = `<tr><td><code>/me</code></td><td style="padding-bottom:10px;">Formats text as an action</td></tr>`;
this.roll = `<tr><td><code>/roll [N]d[S]</code></td><td style="padding-bottom:10px;">Performs a dice roll with N die of S sides. For example /roll 3d12 will roll three, 12-sided die. Doing /roll by itself will default to 1d20</td></tr>`;
this.rps = `<tr><td><code>/rps</code></td><td style="padding-bottom:10px;">Performs a Rock/Paper/Scissors action</td></tr>`;
this.status = `<tr><td><code>/status [message]</code></td><td style="padding-bottom:10px;">Sets your status message<br>Example: <code>/status I'm tabbed out</code></td></tr>`;
this.kick = `<tr><td><code>/kick [username],[reason]</code></td><td style="padding-bottom:10px;">Kicks [username] from the current room with [reason] (optional)</td></tr>`;
this.ban = `<tr><td><code>/ban [username],[reason]</code></td><td style="padding-bottom:10px;">Bans [username] from the current room with [reason] (optional)</td></tr>`;
this[
"add-mod"
] = `<tr><td><code>/add-mod [username]</code></td><td style="padding-bottom:10px;">Adds [username] as a mod of the current room</td></tr>`;
this[
"add-owner"
] = `<tr><td><code>/add-onwer [username]</code></td><td style="padding-bottom:10px;">Adds [username] as the owner of the current room</td></tr>`;
this.unban = `<tr><td><code>/unban [username],[reason]</code></td><td style="padding-bottom:10px;">Unbans [username] from the current room with [reason] (optional)</td></tr>`;
this[
"remove-mod"
] = `<tr><td><code>/remove-mod [username]</code></td><td style="padding-bottom:10px;">Removes [username] as a mod of the current room</td></tr>`;
this[
"remove-owner"
] = `<tr><td><code>/remove-owner [username]</code></td><td style="padding-bottom:10px;">Removes [username] as the owner of the current room</td></tr>`;
})();
const CHAT_COMMAND_HTML = `<div id="chatCommandTooltip" class="rpht-tooltip-common rpht-cmd-tooltip"></div>`;
const DICE_ROLL_POPUP_HTML = `<div id="diceRollerPopup" class="rpht-tooltip-common">
<p style="margin-bottom:10px;">Dice Roller <span id="diceRollerClose" class="rpht-close-btn"> X </span></p>
<label class="rpht-die-label"># of die</label> <input id="rpht_dieRollerCount" class="rpht-die-updown" type="number" max="100" min="1" value="1">
<br>
<label class="rpht-die-label"># of sides</label> <input id="rpht_dieRollerSides" class="rpht-die-updown" type="number"max="1000" min="2" value="20">
<br><br>
<button id="dieRollButton">
Let's roll!
</button>
<hr style="margin-top: 6px; margin-bottom: 6px; ">
<button id="coinFlipButton">
Flip a coin!
</button>
</div>`;
let chatSettings = {};
let chatRoomLogs = null;
let localStorageName = "chatSettings";
let isRoomMod = {};
let autoDismissTimer = null;
let autoJoinTimer = null;
let pingHighlightText = "";
function updateSetting(settingName, selector) {
let element = $(selector);
if (element.length < 1) {
return;
}
let value = null;
if (element[0].localName === "input" && element[0].type === "checkbox") {
value = $(selector).is(":checked");
}
else if (element[0].localName === "option") {
value = parseInt($(selector).val());
}
chatSettings[settingName] = value;
saveSettings();
}
function setupSnapRoomList() {
for (idx in rph.roomsJoined) {
const room = rph.roomsJoined[idx];
const roomCssName = getCssRoomName(room.roomname);
if (chatSettings.snapRoomList === true) {
$(`li.${room.user}_${roomCssName}`).click(() => {
scrollToRoomList(roomCssName);
});
}
else {
$._data($(`li.${room.user}_${roomCssName}`)[0], "events").click.pop();
}
}
}
function init() {
loadSettings();
$("#chat-bottom").append(CHAT_COMMAND_HTML);
$("#chat-bottom").append(DICE_ROLL_POPUP_HTML);
$("#chatCommandTooltip").hide();
$("#diceRollerPopup").hide();
/* General Options */
$("#snapRoomListEnable").change(() => {
updateSetting("snapRoomList", "#snapRoomListEnable");
setupSnapRoomList();
});
$("#chatColorSelection").change(() => {
updateSetting("colorStylizing", "#chatColorSelection option:selected");
});
$("#unreadMarkerSelection").change(() => {
updateSetting("unreadMarkerSelection", "#unreadMarkerSelection option:selected");
});
$("#chatmsgPaddingEnable").change(() => {
chatSettings.msgPadding = $("#chatmsgPaddingEnable").is(":checked");
saveSettings();
});
$("#showCommandWindowEnable").change(() => {
chatSettings.showCommandWindow = $("#showCommandWindowEnable").is(":checked");
saveSettings();
});
$("#removeHighlightingEnable").change(() => {
chatSettings.removeHighlighting = $("#removeHighlightingEnable").is(":checked");
saveSettings();
});
$(`#enableTabSwitch`).change(() => {
chatSettings.enableTabSwitch = $("#enableTabSwitch").is(":checked");
if (chatSettings.enableTabSwitch === true) {
$(document).on("keydown", changeTab);
}
else {
$(document).off("keydown", changeTab);
}
saveSettings();
});
$(`#enableImagePreview`).change(() => {
updateSetting("enableImagePreview", "#enableImagePreview");
});
/* Pinging Options */
$("#notifyPingEnable").change(() => {
chatSettings.enablePings = $("#notifyPingEnable").is(":checked");
saveSettings();
});
$("#notifyNotificationEnable").change(() => {
chatSettings.pingNotify = $("#notifyNotificationEnable").is(":checked");
saveSettings();
});
$("#selfPingEnable").change(() => {
chatSettings.selfPing = $("#selfPingEnable").is(":checked");
saveSettings();
});
$("#pingNames").blur(() => {
let triggers = $("#pingNames").val().replace("\n", "").replace("\r", "");
chatSettings.triggers = triggers;
saveSettings();
});
$("#pingURL").blur(() => {
chatSettings.audioUrl = $("#pingURL").val();
rph.sounds.notify = new Audio(chatSettings.audioUrl);
saveSettings();
});
$("#pingTextColor").blur(() => {
let colorInput = $("#pingTextColor").val();
if (validateColor(colorInput) === true) {
chatSettings.color = colorInput;
generateHighlightStyle();
saveSettings();
}
});
$("#pingHighlightColor").blur(() => {
if (validateSetting("#pingHighlightColor", "color") === true) {
chatSettings.highlight = $("#pingHighlightColor").val();
generateHighlightStyle();
saveSettings();
}
});
$("#pingBoldEnable").change(() => {
chatSettings.bold = $("#pingBoldEnable").is(":checked");
generateHighlightStyle();
saveSettings();
});
$("#pingItalicsEnable").change(() => {
chatSettings.italics = $("#pingItalicsEnable").is(":checked");
generateHighlightStyle();
saveSettings();
});
$("#pingExactMatch").change(() => {
chatSettings.exact = $("#pingExactMatch").is(":checked");
saveSettings();
});
$("#pingCaseSense").change(() => {
chatSettings.case = $("#pingCaseSense").is(":checked");
saveSettings();
});
$("#pingPreviewInput").keyup(() => {
let msg = $("#pingPreviewInput").val();
let testRegex = matchPing(msg);
if (testRegex !== null) {
msg = msg.replace(
testRegex,
`<span style="${pingHighlightText}">${msg.match(testRegex)}</span>`
);
rph.sounds.notify.play();
$("#pingPreviewText").html(` ${msg}`);
}
else {
$("#pingPreviewText").html(` No match`);
}
});
/* Session Options */
$("#trackSession").click(() => {
chatSettings.trackSession = $("#trackSession").is(":checked");
if (chatSettings.trackSession) {
chatSettings.session = rph.roomsJoined;
}
else {
chatSettings.session = [];
}
saveSettings();
});
$("#joinFavEnable").click(() => {
chatSettings.joinFavorites = $("#joinFavEnable").is(":checked");
saveSettings();
});
$("#favAdd").click(() => {
parseFavoriteRoom($("#favRoom").val());
settingsModule.saveSettings(localStorageName, chatSettings);
});
$("#favRemove").click(() => {
removeFavoriteRoom();
settingsModule.saveSettings(localStorageName, chatSettings);
});
/* Die roller */
$("#dieRollButton").click(() => {
const DIE_COUNT = $("#rpht_dieRollerCount").val();
const DIE_SIDES = $("#rpht_dieRollerSides").val();
$(`textarea.${$("li.tab.active")[0].className.split(" ")[2]}.active`).val(
`/roll ${DIE_COUNT}d${DIE_SIDES}`
);
$(`textarea.${$("li.tab.active")[0].className.split(" ")[2]}.active`).trigger({
type: "keydown",
which: 13,
keyCode: 13,
});
$("#diceRollerPopup").hide();
});
$("#coinFlipButton").click(() => {
$(`textarea.${$("li.tab.active")[0].className.split(" ")[2]}.active`).val(`/coinflip`);
$(`textarea.${$("li.tab.active")[0].className.split(" ")[2]}.active`).trigger({
type: "keydown",
which: 13,
keyCode: 13,
});
$("#diceRollerPopup").hide();
});
$("#diceRollerClose").click(() => {
$("#diceRollerPopup").hide();
});
$(window).unload(function () {
chatRoomLogs.timestamp = Date.now();
settingsModule.saveSettings(chatLogsStorageName, chatRoomLogs);
});
/* General intialization */
$(window).resize(resizeChatTabs);
socket.on("confirm-room-join", (data) => {
roomSetup(data);
});
socket.on("room-users-leave", () => {
chatSettings.session = rph.roomsJoined;
saveSettings();
});
socket.on("msg", (data) => {
for (let dataIdx = 0; dataIdx < data.length; dataIdx++) {
const msgData = data[data.length - 1 - dataIdx];
let thisRoom = getRoom(msgData.room);
let messages = $(`div[data-roomname="${msgData.room}"]`).children();
for (let idx = messages.length - 2 - dataIdx; idx > 0; idx--) {
let message = messages[idx];
if ($(message.children[0].children[0]).attr("data-userid") == msgData.userid) {
message.children[0].children[0].innerHTML = createTimestamp(msgData.time);
processMsg(thisRoom, msgData, message, isRoomMod[msgData.room]);
break;
}
}
}
});
/* Setup the timer for automatically dismissing the opening dialog once
rooms are available. The timer clears after. */
autoDismissTimer = setInterval(() => {
if (Object.keys(rph.rooms).length > 0) {
$("button span:contains('Continue')").trigger("click");
clearTimeout(autoDismissTimer);
}
}, 500);
socket.on("account-users", () => {
setTimeout(() => {
$("#favUserDropList").empty();
let namesToIds = getSortedNames();
for (let name in namesToIds) {
addToDroplist(namesToIds[name], name, "#favUserDropList");
}
}, 3000);
});
/* Fix the room management dialog */
$("#room-management-dialog > div.inner").css("height", "100%");
$("#room-management-dialog > div.inner > div").css("width", "640px");
$("#room-management-dialog > div.inner > div").css("float", "right");
$("iframe.group-iframe").css("width", "calc(100% - 640px)");
$("iframe.group-iframe").css("height", "100%");
/* Add user list toggling */
$("#chat-menu").append('<li class="toggleUserList"><a>👤</a></li>');
$("li.toggleUserList").click(toggleRoomList);
/* Kick off auto joining */
if (chatSettings.joinFavorites || chatSettings.trackSession) {
autoJoinTimer = setInterval(autoJoiningHandler, AUTOJOIN_INTERVAL);
}
}
function toggleRoomList() {
$(".rooms.nano-content").toggle();
if ($(".rooms.nano-content").is(":visible")) {
$("#chat").width("");
$("#chat-bottom").width("");
}
else {
$("#chat").width("97%");
$("#chat-bottom").width("97%");
}
}
/**
* When user joins a room, do the following:
* - Set up the .onMessage function for pinging
* - Add the user's name to the chat tab and textarea
* - Create a room-pair name for the Modding section
* - Add the room the session.
* @param {object} room Room that the user has joined
*/
function roomSetup(room) {
let thisRoom = getRoom(room.room);
/* This is to filter out double room leaving. */
thisRoom.userLeave = (function () {
let cached_function = thisRoom.userLeave;
return function () {
if (thisRoom.users.indexOf(arguments[0]) > -1) {
cached_function.apply(this, arguments);
}
};
})();
const NUM_USERS = account.userids.length;
for (let idx = 0; idx < NUM_USERS && !isRoomMod[room.room]; idx++) {
if (
thisRoom.props.mods.indexOf(account.userids[idx]) > -1 ||
thisRoom.props.owners.indexOf(account.userids[idx]) > -1
) {
isRoomMod[room.room] = true;
break;
}
}
getUserById(room.userid, (User) => {
const roomCss = getCssRoomName(thisRoom.props.name);
const moddingModule = rphToolsModule.getModule("Modding Module");
if (moddingModule !== null && isRoomMod[room.room]) {
moddingModule.addModRoomPair(User.props, thisRoom.props.name);
}
$(`li.${User.props.id}_${roomCss}`).click(() => {
for (let roomTab of thisRoom.$tabs) {
roomTab.removeAttr("style");
}
});
/* Set up room tab and input box */
setupRoomTabs(User, roomCss);
/* Setup popups and tooltips */
setupTextboxInput(User, roomCss, thisRoom);
/* Adjust chat tab size */
$("#chat-tabs").addClass("rpht_chat_tab");
resizeChatTabs();
});
chatSettings.session = rph.roomsJoined;
saveSettings();
}
function setupRoomTabs(User, roomCss) {
const userId = User.props.id;
const username = User.props.name;
const color = User.props.color[0];
const buttonCommonStyle = "cursor:pointer; float: right; width: 21px; height: 21px;";
$(`li.${userId}_${roomCss}`).prepend(
`<p style="font-size: x-small; height:16px; margin-top: -10px;">${username}</p>`
);
$(`li.${userId}_${roomCss} > a.close`).on("click", () => {
if (chatSettings.trackSession) {
chatSettings.session = rph.roomsJoined;
saveSettings();
}
});
$(`textarea.${userId}_${roomCss}`).prop("placeholder", `Post as ${username}`);
$(`textarea${userId}_${roomCss}`).css("color", `${color}`);
$(`div.${userId}_${roomCss} .user-for-textarea span`).css("overflow", "hidden");
$(`div.${userId}_${roomCss} .user-for-textarea div`)
.css("width", "234px")
.append(
`<button class="${userId}_${roomCss} roller-button" style="cursor:pointer; float: right; width: auto; height: 21px;" title="Dice roller">🎲</button>`
)
.append(
`<button class="${userId}_${roomCss} bold-button" style="${buttonCommonStyle} font-weight: bold;" title="Bold selection">B</button>`
)
.append(
`<button class="${userId}_${roomCss} italics-button" style="${buttonCommonStyle} font-style: italic;" title="Italics selection">I</button>`
)
.append(
`<button class="${userId}_${roomCss} underline-button" style="${buttonCommonStyle} text-decoration: underline;" title="Underline selection">U</button>`
)
.append(
`<button class="${userId}_${roomCss} linethrough-button" style="${buttonCommonStyle} text-decoration: line-through;" title="Linethrough selection">S</button>`
)
.append(
`<button class="${userId}_${roomCss} spoiler-button" style="${buttonCommonStyle} font-style: italic;" title="Spoiler selection"><span class="spoiler">S</span></button>`
)
.append(
`<button class="${userId}_${roomCss} superscript-button" style="${buttonCommonStyle} font-style: italic;" title="Superscript selection"><sup>S</sup></button>`
)
.append(
`<button class="${userId}_${roomCss} subscript-button" style="${buttonCommonStyle} font-size: smaller;" title="Subscript selection"><sub>S</sub></button>`
);
$(`button.${userId}_${roomCss}.roller-button`).click(() => {
$("#diceRollerPopup").toggle();
});
$(`button.${userId}_${roomCss}.bold-button`).click(() => {
styleSelection(`${userId}_${roomCss}`, "bold");
});
$(`button.${userId}_${roomCss}.italics-button`).click(() => {
styleSelection(`${userId}_${roomCss}`, "italics");
});
$(`button.${userId}_${roomCss}.underline-button`).click(() => {
styleSelection(`${userId}_${roomCss}`, "underline");
});
$(`button.${userId}_${roomCss}.linethrough-button`).click(() => {
styleSelection(`${userId}_${roomCss}`, "linethrough");
});
$(`button.${userId}_${roomCss}.spoiler-button`).click(() => {
styleSelection(`${userId}_${roomCss}`, "spoiler");
});
$(`button.${userId}_${roomCss}.subscript-button`).click(() => {
styleSelection(`${userId}_${roomCss}`, "sub");
});
$(`button.${userId}_${roomCss}.superscript-button`).click(() => {
styleSelection(`${userId}_${roomCss}`, "super");
});
if (chatSettings.snapRoomList === true) {
$(`li.${userId}_${roomCss}`).click(() => {
scrollToRoomList(roomCss);
});
}
}
function styleSelection(textareaClass, styleType) {
const chatTextbox = $(`textarea.${textareaClass}`)[0];
const start = chatTextbox.selectionStart;
const end = chatTextbox.selectionEnd;
let tag = "";
if (styleType == "bold") {
tag = "**";
}
else if (styleType == "italics") {
tag = "//";
}
else if (styleType == "underline") {
tag = "__";
}
else if (styleType == "linethrough") {
tag = "--";
}
else if (styleType == "spoiler") {
tag = "[spoiler]";
}
else if (styleType == "sub") {
tag = "vv";
}
else if (styleType == "super") {
tag = "^^";
}
const original = chatTextbox.value;
const highlighted = original.substring(start, end).trim();
chatTextbox.value = original.substring(0, start);
if (original[start] == " ") {
chatTextbox.value += " ";
}
chatTextbox.value += tag;
chatTextbox.value += highlighted;
if (styleType == "spoiler") {
chatTextbox.value += "[/spoiler]";
}
else {
chatTextbox.value += tag;
}
if (original[end - 1] == " ") {
chatTextbox.value += " ";
}
chatTextbox.value += original.substring(end);
$(`textarea.${textareaClass}`).focus();
if (start != end) {
$(`textarea.${textareaClass}`)[0].setSelectionRange(start + tag.length, end + tag.length);
}
else {
$(`textarea.${textareaClass}`)[0].setSelectionRange(start + tag.length, start + tag.length);
}
}
function setupTextboxInput(User, roomCss, thisRoom) {
const userId = User.props.id;
let chatTextArea = $(`textarea.${userId}_${roomCss}`);
$(`li.${userId}_${roomCss} a.close`).click(() => {
$("#chatCommandTooltip").hide();
$("#diceRollerPopup").hide();
});
chatTextArea.unbind("keyup");
chatTextArea.bind("keydown", (ev) => {
intputChatText(ev, User, thisRoom);
});
chatTextArea.on("input", () => {
const chatInput = chatTextArea.val().trim();
$("#chatCommandTooltip").hide();
if (chatInput[0] === "/" && chatSettings.showCommandWindow) {
const commandTable = buildComamndTable(chatTextArea.val().trim());
$("#chatCommandTooltip").html(commandTable).show();
}
});
}
function processMsg(thisRoom, msgData, msgHtml, isMod) {
let contentQuery = $(msgHtml.children[1].children[0]);
/* If the message was an action, switch the query to where it really is */
if (msgHtml.className.includes("action")) {
contentQuery = $(msgHtml.children[1].children[1]);
}
/* Separate the new content from the previous content */
const msgLineCount = msgData.msg.split("\n").length;
const contentLines = contentQuery[0].innerHTML.split("<br>");
const prevMsgs = contentLines.slice(0, contentLines.length - msgLineCount);
// Fix issues with the URL parser inserting formatting tags
let latestLine = contentLines[contentLines.length - 1];
let sections = latestLine.split(`"`);
for (let i = 0; i < sections.length; i++) {
let section = sections[i];
if (sections[i].indexOf("http") !== 0) {
continue;
}
section = section.replaceAll(`<u>`, `__`);
section = section.replaceAll(`</u>`, `__`);
section = section.replaceAll(`<sub>`, `vv`);
section = section.replaceAll(`</sub>`, `vv`);
section = section.replaceAll(`<strike>`, `--`);
section = section.replaceAll(`</strike>`, `--`);
section = section.replaceAll(`<em>`, `//`);
section = section.replaceAll(`</em>`, `//`);
section = section.replaceAll(`<strong>`, `**`);
section = section.replaceAll(`</strong>`, `**`);
sections[i] = section;
}
contentLines[contentLines.length - 1] = sections.join(`"`);
/* Add padding and remove stlying of the content */
if (chatSettings.msgPadding && !$(msgHtml.children[1])[0].className.includes("msg-padding")) {
$(msgHtml.children[1])[0].className += " msg-padding";
contentQuery.removeAttr("style");
}
if (!thisRoom.active && msgData.room === thisRoom.props.name) {
switch (chatSettings.unreadMarkerSelection) {
case 2:
break;
case 1:
$(`li.tab.tab-${getCssRoomName(thisRoom.props.name)}`).css(
"border-bottom",
"4px solid #EEE"
);
/* Falling through intentionally */
default:
for (let roomTab of thisRoom.$tabs) {
$(roomTab.children()[2]).hide();
}
break;
}
}
getUserById(msgData.userid, (user) => {
let newMsgLines = contentLines.slice(contentLines.length - msgLineCount);
for (let msgIdx = 0; msgIdx < newMsgLines.length; msgIdx++) {
if (newMsgLines[msgIdx].indexOf("​") === -1) {
continue;
}
const SEED = newMsgLines[msgIdx].split("​")[1];
const MSG_CHUNKS = newMsgLines[msgIdx].split(/ /g);
let validResult = true;
newMsgLines[msgIdx] = newMsgLines[msgIdx].substring(
0,
newMsgLines[msgIdx].indexOf(" @​")
);
/* If the RNG was a dice roll, verify the roll by running the RNG with the same seed */
if (newMsgLines[msgIdx].search("rolled") > -1) {
const ROLL_PARAMS = parseRoll(MSG_CHUNKS[2]);
const ROLL_RESULTS = calculateDiceRolls(ROLL_PARAMS[0], ROLL_PARAMS[1], SEED);
for (let idx = 0; idx < ROLL_PARAMS[0] && validResult; idx++) {
validResult = !(ROLL_RESULTS["results"][idx] !== parseInt(MSG_CHUNKS[idx + 3]));
}
}
else if (newMsgLines[msgIdx].search("flips" > -1)) {
const outcomes = ["heads", "tails"];
let outcome = outcomes[LcgRng(SEED) % 2];
validResult = newMsgLines[msgIdx].search(outcome) > -1;
}
if (validResult === false) {
newMsgLines[
msgIdx
] += ` <span style="background:#F44; color: #FFF;" title="Do not use this result">☒</span>`;
}
else if (msgData.time - SEED > RNG_TIMEOUT) {
newMsgLines[
msgIdx
] += ` <span style="background:#FFD800; color: #000;" title="This result is outdated">⍰</span>`;
}
else {
newMsgLines[
msgIdx
] += ` <span style="background:#4A4; color: #FFF;" title="This result is good">☑</span>`;
}
}
let newMsg = ``;
if (contentLines.length !== 1 && !("buffer" in msgData)) {
newMsg += `<br>`;
}
newMsg += `${newMsgLines.join("<br>")}`;
if (!msgData.buffer) {
const selfMsg = account.userids.includes(msgData.userid);
let notificationTrigger = 0;
if (
chatSettings.enablePings &&
((chatSettings.selfPing && selfMsg === true) || selfMsg === false)
) {
let testRegex = matchPing(newMsg);
if (testRegex) {
newMsg = newMsg.replace(
testRegex,
`<span style="${pingHighlightText}">${newMsg.match(testRegex)}</span>`
);
rph.sounds.notify.play();
notificationTrigger = 1;
if (chatSettings.pingNotify && thisRoom.active === false) {
displayNotification(
`${user.props.name} pinged you in ${thisRoom.props.name}`,
chatSettings.notifyTime
);
}
}
}
/* Process other's messages for issues if a mod */
if (isMod && moddingModule && selfMsg === false) {
let alertRegex = null;
let alertWords = moddingModule.getAlertWords();
alertRegex = matchPing(newMsg, alertWords, false, true);
// Process alert
if (alertRegex) {
newMsg = newMsg.replace(
alertRegex,
`<span style="${ALERT_HIGHLIGHT}">${newMsg.match(alertRegex)}</span>`
);
moddingModule.playAlert();
notificationTrigger = 2;
}
}
if (thisRoom.active === false && notificationTrigger > 0) {
let background = notificationTrigger === 2 ? "#F00" : chatSettings.highlight;
let textColor = notificationTrigger === 2 ? "#FFF" : chatSettings.color;
$(`li.tab.tab-${getCssRoomName(thisRoom.props.name)}`).css({
"background-color": background,
color: textColor,
});
}
}
contentQuery.html(`${prevMsgs.join("<br>")} ${newMsg}`);
if (chatSettings.colorStylizing == 0) {
const CHILD_NODE_COUNT = contentQuery[0].childNodes.length;
for (let i = 0; i < CHILD_NODE_COUNT; i++) {
console.log(contentQuery[0].childNodes[i]);
if ("classLlist" in contentQuery[0].childNodes[i]) {
contentQuery[0].childNodes[i].classList = [];
}
}
}
else if (chatSettings.colorStylizing == 2) {
const colorClasses = ["", "two-color", "three-color"];
let classString = `${contentQuery[0].className}`;
let styleString = `color: #${user.props.color[0]};`;
classString += ` ${colorClasses[user.props.color.length - 1]}`;
contentQuery[0].className = classString.trim();
contentQuery.attr("style", styleString);
}
if (chatSettings.enableImagePreview) {
let contentsChildren = contentQuery[0].children;
let images = [];
for (let i = 0; i < contentsChildren.length; i++) {
const child = contentsChildren[i];
if (child.tagName == "A") {
const url = child.attributes["href"].nodeValue;
if (url.search(`\.png|\.jpe?g|\.gif|\.webm`) > -1) {
images.push(url);
}
}
}
contentQuery.find("div.rpht-images").remove();
let imageArea = `<div class="rpht-images">`;
images.forEach((url) => {
imageArea += `<img src="${url}" width="240px" alt="${url}"> `;
});
imageArea += `</div>`;
contentQuery.append(imageArea);
}
if (chatSettings.removeHighlighting) {
$(msgHtml).removeClass("action");
$(msgHtml).removeClass("group-member");
$(msgHtml).removeClass("self");
$(msgHtml).removeClass("mod");
$(msgHtml).removeClass("friend");
}
if (thisRoom.props.name in chatRoomLogs === false) {
chatRoomLogs[thisRoom.props.name] = [];
}
if (chatRoomLogs[thisRoom.props.name].length >= 30) {
chatRoomLogs[thisRoom.props.name].shift();
}
if (contentLines.length !== 1) {
const lastIdx = chatRoomLogs[thisRoom.props.name].length - 1;
chatRoomLogs[thisRoom.props.name][lastIdx] = msgHtml.innerHTML;
}
else {
chatRoomLogs[thisRoom.props.name].push(msgHtml.innerHTML);
}
});
}
function buildComamndTable(message) {
let commandEntry = "";
let commandTable = "";
if (message.length === 1) {
commandEntry = Object.values(CHAT_COMMANDS).join("\n");
}
else {
const command = message.split(" ")[0].substring(1);
Object.keys(CHAT_COMMANDS)
.filter((key) => key.startsWith(command))
.forEach((key) => (commandEntry += CHAT_COMMANDS[key]));
}
if (commandEntry.length > 0) {
commandTable = `<table style="width: 100%;">
<tbody>
<tr><td>Chat Commands:</td> <td style="width: 68%;"> </td></tr>
${commandEntry}
</tbody>
</table>`;
$("#chatCommandTooltip").addClass("rpht-tooltip-common");
}
else {
$("#chatCommandTooltip").removeClass("rpht-tooltip-common");
}
return commandTable;
}
/**
* Parses a slash command from an input source.
* @param {object} inputTextBox HTML element that holds the input textbox
* @param {object} Room Room data
* @param {object} User User data
*/
function parseSlashCommand(inputTextBox, Room, User) {
let newMessage = inputTextBox.val();
let error = false;
let cmdArgs = newMessage.split(/ (.+)/);
switch (cmdArgs[0]) {
case "/status":
case "/away":
if (cmdArgs.length != 3) {
error = true;
}
else {
let type = 0;
if (cmdArgs[0] === "/away") {
type = 1;
}
socket.emit("modify", {
userid: User.props.id,
statusMsg: cmdArgs[1],
statusType: type,
});
inputTextBox.val("");
}
break;
case "/coinflip": {
const outcomes = ["heads", "tails"];
const seed = new Date().getTime();
let resultMsg = `/me flips a coin. It lands on... **${
outcomes[LcgRng(seed) % 2]
}**! @​${seed}`;
Room.sendMessage(resultMsg, User.props.id);
}
break;
case "/roll": {
const seed = new Date().getTime();
let diceArgs = [1, 20];
let resultMsg = `/me `;
let results = [];
let result = LcgRng(seed);
if (cmdArgs[1]) {
diceArgs = parseRoll(cmdArgs[1]);
}
results.push((result % diceArgs[1]) + 1);
for (let die = 1; die < diceArgs[0]; die++) {
result = LcgRng(result);
results.push((result % diceArgs[1]) + 1);
}
total = results.reduce((a, b) => a + b, 0);
resultMsg += `rolled ${diceArgs[0]}d${diceArgs[1]}: ${results.join(
" "
)} (total: ${total}) @​${seed}`;
Room.sendMessage(resultMsg, User.props.id);
}
break;
case "/rps": {
const results = ["Rock!", "Paper!", "Scissors!"];
newMessage = `/me plays Rock, Paper, Scissors and chooses... ${results[
Math.ceil(Math.random() * 3) % 3
].toString()}`;
Room.sendMessage(newMessage, User.props.id);
}
break;
case "/leave":
socket.emit("leave", {
userid: User.props.id,
name: Room.props.name,
});
break;
case "/kick":
case "/ban":
case "/unban":
case "/add-owner":
case "/add-mod":
case "/remove-owner":
case "/remove-mod":
let moddingModule = rphToolsModule.getModule("Modding Module");
if (cmdArgs.length < 2) {
error = true;
}
else if (moddingModule) {
let action = cmdArgs[0].substring(1, cmdArgs[0].length);
let commaIdx = cmdArgs[1].indexOf(",");
let targetName = cmdArgs[1];
let reason = "";
if (commaIdx > -1) {
targetName = cmdArgs[1].substring(0, commaIdx);
reason = cmdArgs[1].substring(commaIdx + 1, cmdArgs[1].length);
}
moddingModule.emitModAction(action, targetName, User.props.name, Room.props.name, reason);
inputTextBox.val("");
}
break;
default:
Room.sendMessage(newMessage, User.props.id);
break;
}
if (error) {
Room.appendMessage(
'<span class="first"> </span><span title=>Error in command input</span>'
).addClass("sys");
}
}
/**
*
* @param {object} ev - Event
* @param {object} User - User the textbox is attached to
* @param {oject} Room - Room the textbox is attached to
*/
function intputChatText(ev, User, Room) {
let inputTextarea = $(`textarea.${User.props.id}_${getCssRoomName(Room.props.name)}.active`);
let message = inputTextarea.val().trim();
if (message.length > 4000) {
Room.appendMessage(
`<span class="first"> </span><span title="">Message too long</span>`
).addClass("sys");
return;
}
else if (message.length === 0) {
return;
}
else if (ev.keyCode !== 13 || ev.shiftKey === true || ev.ctrlKey === true) {
return;
}
else if (ev.keyCode === 13 && (ev.shiftKey === true || ev.ctrlKey === true)) {
inputTextarea.val(inputTextarea.val() + "\n");
}
$("#chatCommandTooltip").hide();
if (message[0] === "/" && message.substring(0, 2) !== "//" && chatModule) {
parseSlashCommand(inputTextarea, Room, User);
}
else {
Room.sendMessage(message, User.props.id);
}
inputTextarea.val("");
}
/**
* Checks if the message has any ping terms
* @param {string} msg - The message for the chat
* @returns Returns the match or null
*/
function matchPing(
msg,
triggers = chatSettings.triggers,
caseSensitive = chatSettings.case,
exactMatch = chatSettings.exact
) {
if (triggers.length === 0) {
return;
}
let result = null;
const pingNames = triggers.split(",");
const regexParam = caseSensitive ? "m" : "im";
for (i = 0; i < pingNames.length; i++) {
let trigger = pingNames[i].trim();
if (trigger === "") {
continue;
}
const regexPattern = exactMatch ? `\\b${trigger}\\b` : trigger;
const urlRegex = new RegExp(`href=".*?${trigger}.*?"`, "");
let testRegex = new RegExp(regexPattern, regexParam);
/* Check if search term is not in a link as well */
if (!urlRegex.test(msg) && testRegex.test(msg)) {
result = testRegex;
break;
}
}
return result;
}
/**
* Resizes chat tabs based on the width of the tabs vs. the screen size.
*/
function resizeChatTabs() {
/* Window is smaller than the tabs width */
if (
$("#chat-tabs")[0].clientWidth < $("#chat-tabs")[0].scrollWidth ||
$("#chat-tabs")[0].clientWidth > $("#chat-bottom")[0].clientWidth
) {
$("#chat-top").css("padding-bottom", "136px");
$("#chat-bottom").css("margin-top", "-138px");
}
else {
$("#chat-top").css("padding-bottom", "120px");
$("#chat-bottom").css("margin-top", "-118px");
}
// Debouce the function.
$(window).off("resize", resizeChatTabs);
setTimeout(() => {
$(window).resize(resizeChatTabs);
}, 100);
}
function generateHighlightStyle() {
pingHighlightText = `color: ${chatSettings.color}; background: ${chatSettings.highlight};`;
if (chatSettings.bold === true) {
pingHighlightText += " font-weight: bold;";
}
if (chatSettings.italics === true) {
pingHighlightText += " font-style:italic;";
}
}
function scrollToRoomList(roomName) {
const elementPrefix = "div.room-header-";
if ($(`${elementPrefix}${roomName} > .content > .users`).is(":visible") === false) {
$(`${elementPrefix}${roomName} > .header`).click();
}
$(`${elementPrefix}${roomName}`)[0].scrollIntoView();
}
function changeTab(e) {
if (e.altKey === false || e.shiftKey === false) {
return;
}
const FOCUS_TIMEOUT_MS = 100;
if (e.which == 37) {
let prevTab = $("ul.chat-tabs>li.active").prev();
if (prevTab.hasClass("thumb") === true) {
prevTab = $("ul.chat-tabs>li.active").parent().prev();
if (prevTab.length > 0) {
$(prevTab[0].children[1]).click();
}
}
else {
prevTab.click();
}
setTimeout(() => {
$("textarea.active").focus();
}, FOCUS_TIMEOUT_MS);
return false;
}
else if (e.which == 39) {
let nextTab = $("ul.chat-tabs>li.active").parent().next();
if (nextTab.length === 0) {
nextTab = $("ul.chat-tabs>li.active").next();
if (nextTab.length > 0) {
nextTab.click();
}
}
else {
$(nextTab[0].children[1]).click();
}
setTimeout(() => {
$("textarea.active").focus();
}, FOCUS_TIMEOUT_MS);
return false;
}
}
/** AUTO JOINING FUNCTIONS **********************************************/
/**
* Handler for the auto-joining mechanism.
**/
function autoJoiningHandler() {
/* Don't run this if there's no rooms yet. */
if (Object.keys(rph.rooms).length === 0) {
return;
}
else if ($("#chat-tabs")[0].childNodes.length > 0) {
/* If RPH's sessioning kicked in, clear the timeout and return. */
setTimeout(() => {
rph.roomsJoined.forEach((joinedRoom) => {
populateChatLog(joinedRoom.roomname);
});
}, 250);
clearTimeout(autoJoinTimer);
return;
}
$(
'<div id="rpht-autojoin" class="inner" style="background: #333;">' +
"<p>Autojoining or restoring session in about 5 seconds.</p>" +
'<p>Press "Cancel" to stop.</p>' +
"</div>"
)
.dialog({
open: function (event, ui) {
setTimeout(() => {
$("#rpht-autojoin").dialog("close");
}, AUTOJOIN_TIMEOUT_SEC);
},
buttons: {
Cancel: () => {
joinedSession = true;
chatSettings.session = [];
clearTimeout(autoJoinTimer);
$("#rpht-autojoin").dialog("close");
},
},
})
.dialog("open");
clearTimeout(autoJoinTimer);
autoJoinTimer = setTimeout(joinRooms, AUTOJOIN_TIMEOUT_SEC);
}
function populateChatLog(roomName) {
const thisRoom = getRoom(roomName);
if (roomName in chatRoomLogs) {
chatRoomLogs[roomName].forEach((logEntry) => {
thisRoom.appendMessage(logEntry);
});
}
}
/**
* Join rooms in the favorites and what was in the session.
*/
function joinRooms() {
if ($("#chat-tabs")[0].childNodes.length > 0) {
return;
}
if (chatSettings.joinFavorites === true) {
joinFavoriteRooms();
}
if (chatSettings.trackSession === true) {
joinPreviousSession();
}
}
function joinFavoriteRooms() {
chatSettings.favRooms.forEach((favRoom) => {
socket.emit("join", {
name: favRoom.room,
userid: favRoom.userId,
pw: favRoom.roomPw,
});
});
}
function joinPreviousSession() {
const sessionLen = chatSettings.session.length;
for (let i = 0; i < sessionLen; i++) {
const favoritesLen = chatSettings.favRooms.length;
const sessionRoom = chatSettings.session[i];
let canJoin = true;
for (let j = 0; chatSettings.joinFavorites && j < favoritesLen; j++) {
const favRoom = chatSettings.favRooms[j];
if (favRoom.name == sessionRoom.roomname && favRoom.userId == sessionRoom.user) {
canJoin = false;
break;
}
}
if (canJoin) {
socket.emit("join", {
name: sessionRoom.roomname,
userid: sessionRoom.user,
});
}
}
joinedSession = true;
}
/**
* Adds an entry to the Favorite Chat Rooms list from an input
* @param {string} roomname - Name of the room
*/
function parseFavoriteRoom(roomname) {
let room = getRoom(roomname);
if (room === undefined) {
markProblem("favRoom", true);
return;
}
if (chatSettings.favRooms.length < MAX_ROOMS) {
let selectedFav = $("#favUserDropList option:selected");
let hashStr = $("#favRoom").val() + selectedFav.html();
let favRoomObj = {
_id: hashStr.hashCode(),
user: selectedFav.html(),
userId: parseInt(selectedFav.val()),
room: $("#favRoom").val(),
roomPw: $("#favRoomPw").val(),
};
addFavoriteRoom(favRoomObj);
markProblem("favRoom", false);
}
}
/**
* Adds a favorite room to the settings list
* @param {Object} favRoomObj - Object containing the favorite room parameters.
*/
function addFavoriteRoom(favRoomObj) {
if (arrayObjectIndexOf(chatSettings.favRooms, "_id", favRoomObj._id) === -1) {
$("#favRoomsList").append(
'<option value="' +
favRoomObj._id +
'">' +
favRoomObj.user +
": " +
favRoomObj.room +
"</option>"
);
chatSettings.favRooms.push(favRoomObj);
}
if (chatSettings.favRooms.length >= MAX_ROOMS) {
$("#favAdd").text("Favorites Full");
$("#favAdd")[0].disabled = true;
}
}
/**
* Removes an entry to the Favorite Chat Rooms list
*/
function removeFavoriteRoom() {
let favItem = document.getElementById("favRoomsList");
let favItemId = $("#favRoomsList option:selected").val();
favItem.remove(favItem.selectedIndex);
for (let idx = 0; idx < chatSettings.favRooms.length; idx++) {
if (chatSettings.favRooms[idx]._id == favItemId) {
chatSettings.favRooms.splice(idx, 1);
break;
}
}
if (chatSettings.favRooms.length < 10) {
$("#favAdd").text("Add");
$("#favAdd")[0].disabled = false;
}
}
/**
* Save current settings
*/
function saveSettings() {
settingsModule.saveSettings(localStorageName, chatSettings);
}
/**
* Loads settings from local storage
* @param {object} storedSettings Object containing the settings
*/
function loadSettings() {
let storedSettings = settingsModule.getSettings(localStorageName);
chatRoomLogs = settingsModule.getSettings(chatLogsStorageName);
if (chatRoomLogs === null) {
chatRoomLogs = {};
}
chatSettings = {
snapRoomList: true,
colorStylizing: 1,
unreadMarkerSelection: 1,
msgPadding: false,
showCommandWindow: false,
enableTabSwitch: false,
removeHighlighting: false,
enableImagePreview: false,
enablePings: true,
pingNotify: false,
selfPing: false,
triggers: [],
audioUrl: "https://www.rphaven.com/sounds/boop.mp3",
color: "#000",
highlight: "#FFA",
bold: false,
italics: false,
exact: false,
case: false,
joinFavorites: false,
trackSession: false,
favRooms: [],
session: [],
};
if (storedSettings) {
chatSettings = Object.assign(chatSettings, storedSettings);
}
$("#snapRoomListEnable").prop("checked", chatSettings.snapRoomList);
$(`#chatColorSelection option[value='${chatSettings.colorStylizing}']`).prop("selected", true);
$(`#unreadMarkerSelection option[value='${chatSettings.unreadMarkerSelection}']`).prop(
"selected",
true
);
$("#chatmsgPaddingEnable").prop("checked", chatSettings.msgPadding);
$("#showCommandWindowEnable").prop("checked", chatSettings.showCommandWindow);
$("#removeHighlightingEnable").prop("checked", chatSettings.removeHighlighting);
$("#enableTabSwitch").prop("checked", chatSettings.enableTabSwitch);
$("#enableImagePreview").prop("checked", chatSettings.enableImagePreview);
$("#notifyPingEnable").prop("checked", chatSettings.enablePings);
$("#selfPingEnable").prop("checked", chatSettings.selfPing);
$("#notifyNotificationEnable").prop("checked", chatSettings.pingNotify);
$("#pingNames").val(chatSettings.triggers);
$("#pingURL").val(chatSettings.audioUrl);
$("#pingTextColor").val(chatSettings.color);
$("#pingHighlightColor").val(chatSettings.highlight);
$("input#pingBoldEnable").prop("checked", chatSettings.bold);
$("input#pingItalicsEnable").prop("checked", chatSettings.italics);
$("input#pingExactMatch").prop("checked", chatSettings.exact);
$("input#pingCaseSense").prop("checked", chatSettings.case);
$("#trackSession").prop("checked", chatSettings.trackSession);
$("#joinFavEnable").prop("checked", chatSettings.joinFavorites);
for (let i = 0; i < chatSettings.favRooms.length; i++) {
let favRoomObj = chatSettings.favRooms[i];
$("#favRoomsList").append(
'<option value="' +
favRoomObj._id +
'">' +
favRoomObj.user +
": " +
favRoomObj.room +
"</option>"
);
}
if (chatSettings.enableTabSwitch === true) {
$(document).on("keydown", changeTab);
}
else {
$(document).off("keydown", changeTab);
}
generateHighlightStyle();
rph.sounds.notify = new Audio(chatSettings.audioUrl);
}
function getHtml() {
return html;
}
function toString() {
return "Chat Module";
}
return {
init: init,
loadSettings: loadSettings,
getHtml: getHtml,
toString: toString,
};
})();
/**
* This module handles features for the PM system.
*/
let pmModule = (function () {
let pmSettings = {};
let localStorageName = "pmSettings";
let html = {
tabId: "pm-module",
tabName: "PMs",
tabContents: `<h3>PM Settings</h3><br>
<h4>Appearance</h4>
<div class="rpht-option-block">
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="receivePmsEnable">Receive PMs</label>
<label class="switch"><input type="checkbox" id="receivePmsEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Enables receiving of PMs (this does not persist between sessions)</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="pmColorEnable">Use user text colors</label>
<label class="switch"><input type="checkbox" id="pmColorEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Use the user\'s color to stylize their text</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="pmSideTabsEnable">Tabs on side</label>
<label class="switch"><input type="checkbox" id="pmSideTabsEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Puts the PM tabs on the side, listing them vertically. Requires page refresh for changes to take effect</label>
</div>
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="keepInBgEnable">Keep window in the background</label>
<label class="switch"><input type="checkbox" id="keepInBgEnable"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Upon receiving a PM, prevents the window from showing if it's already closed.</label>
</div>
<div class="rpht-option-section option-section-bottom">
<label class="rpht-label checkbox-label" for="pmEnableImagePreview">Enable image previews</label>
<label class="switch"><input type="checkbox" id="pmEnableImagePreview"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Links to images show up in chat. This may not work for some links.</label>
</div>
</div>
<h4>Notifications</h4>
<div class="rpht-option-block">
<div class="rpht-option-section">
<label class="rpht-label checkbox-label" for="pmNotify">Desktop notifications</label>
<label class="switch"><input type="checkbox" id="pmNotify"><span class="rpht-slider round"></span></label>
<label class="rpht-label descript-label">Pushes a desktop notification when you get a PM</label>
</div>
<div class="rpht-option-section option-section-bottom">
<label class="rpht-label split-input-label">PM sound URL</label>
<input class="split-input-label" type="text" id="pmPingURL" name="pmPingURL" style="margin-bottom: 12px;">
</div>
</div>
<h4>Force removal</h4>
<div class="rpht-option-block">
<div class="rpht-option-section">
<p><strong>Force unfriend</strong></p>
<p>This forces unfriending of someone if they "deleted" their name to help clean up your friend list</p>
<input type="text" class="rpht-long-input" id="unfriendYourNameInput"
maxlength="300" placeholder="Your name"> <br><br>
<input type="text" class="rpht-long-input" id="unfriendOtherNameInput"
maxlength="300" placeholder="Friend's name"> <br><br>
<button type="button" style="float:right; width:60px" id="forceUnfriendButton">Unfriend</button>
</div>
<div class="rpht-option-section option-section-bottom">
<p><strong>Force unblock</strong></p>
<p>This forces unblocking of someone if they "deleted" their name to help clean up your block list</p>
<input type="text" class="rpht-long-input" id="forceUnblockInput"
maxlength="300" placeholder="User to unblock..."> <br><br>
<button type="button" style="float:right; width:60px" id="forceUnblockButton">Unblock</button>
</div>
</div>
<h4>Away message</h4>
<div class="rpht-option-block">
<div class="rpht-option-section option-section-bottom">
<p>Usernames</p> <select style="width: 100%;" id="pmNamesDroplist" size="10"></select><br><br>
<label><strong>Away Message </strong></label><input type="text" class="rpht-long-input" id="awayMessageTextbox"
maxlength="300" placeholder="Away message..."> <br><br> <button type="button"
style="float:right; width:60px" id="setAwayButton">Enable</button> <button type="button"
style="float:right; margin-right: 20px; width:60px" id="removeAwayButton">Disable</button>
</div>
</div>`,
};
let awayMessages = {};
let pmHandlers = [];
function init() {
loadSettings();
$("#receivePmsEnable").change(() => {
const receivePms = $("#receivePmsEnable").is(":checked");
console.log("Receive PMs?", receivePms);
if (receivePms) {
pmHandlers.forEach((handler) => {
socket.on("pm", handler);
});
}
else {
pmHandlers = socket.listeners("pm");
socket.removeAllListeners("pm");
}
});
$("#pmSideTabsEnable").change(() => {
pmSettings.sideTabs = $("#pmSideTabsEnable").is(":checked");
settingsModule.saveSettings(localStorageName, pmSettings);
});
$("#keepInBgEnable").change(() => {
pmSettings.keepInBgEnable = $("#keepInBgEnable").is(":checked");
settingsModule.saveSettings(localStorageName, pmSettings);
});
$("#pmEnableImagePreview").change(() => {
pmSettings.pmEnableImagePreview = $("#pmEnableImagePreview").is(":checked");
settingsModule.saveSettings(localStorageName, pmSettings);
});
$("#pmColorEnable").change(() => {
pmSettings.colorText = $("#pmColorEnable").is(":checked");
settingsModule.saveSettings(localStorageName, pmSettings);
});
$("#pmNotify").change(() => {
pmSettings.notify = $("#pmNotify").is(":checked");
settingsModule.saveSettings(localStorageName, pmSettings);
});
$("#pmNamesDroplist").change(() => {
let userId = $("#pmNamesDroplist option:selected").val();
let message = "";
if (awayMessages[userId] !== undefined) {
message = awayMessages[userId].message;
}
$("input#awayMessageTextbox").val(message);
});
$("#forceUnfriendButton").click(() => {
forceUnfriend($("#unfriendYourNameInput").val(), $("#unfriendOtherNameInput").val());
});
$("#forceUnblockButton").click(() => {
forceUnblock($("#forceUnblockInput").val());
});
$("#setAwayButton").click(() => {
setPmAway();
});
$("#removeAwayButton").click(() => {
removePmAway($("#pmNamesDroplist option:selected").val());
});
$("#pmPingURL").change(() => {
if (validateSetting("#pmPingURL", "url")) {
pmSettings.audioUrl = $("#pmPingURL").val();
rph.sounds.im = new Audio(pmSettings.audioUrl);
settingsModule.saveSettings(localStorageName, pmSettings);
}
});
$("#pmNotify").change(() => {
pmSettings.notify = $("#pmNotify").is(":checked");
settingsModule.saveSettings(localStorageName, pmSettings);
});
$("#pm-msgs span").css("opacity", 0.85);
socket.on("pm", handlePm);
socket.on("pm-confirmation", handlePmConfirmation);
socket.on("account-users", handleAccountUsers);
}
function handlePm(data) {
if (account.ignores.indexOf(data.to) > -1) {
return;
}
rph.getPm({
from: data.from,
to: data.to
}, (pm) => {
getUserByName(pm.to.props.name, (user) => {
processPmMsg(user, data, pm);
});
if (pmSettings.notify) {
displayNotification(`${pm.to.props.name} sent a PM to you for ${pm.from.props.name}`);
}
if (awayMessages[data.from] && awayMessages[data.from].enabled) {
awayMessages[data.from].usedPmAwayMsg = true;
socket.emit("pm", {
from: data.from,
to: data.to,
msg: awayMessages[data.from].message,
target: "all",
});
}
if (pmSettings.keepInBackground && !$("#pm-dialog").parent().is(":visible")) {
$("#pm-header").siblings("button").click();
}
});
}
async function handlePmConfirmation(data) {
rph.getPm({
from: data.to,
to: data.from
}, function (pm) {
getUserByName(pm.from.props.name, (user) => {
processPmMsg(user, data, pm);
if (awayMessages[data.to] && awayMessages[data.to].enabled) {
$("#pmNamesDroplist option")
.filter(function () {
return this.value == data.to;
})
.css("background-color", "")
.html(user.props.name);
awayMessages[data.to].enabled = false;
}
});
});
return Promise.resolve(true);
}
function handleAccountUsers() {
setTimeout(() => {
$("#pmNamesDroplist").empty();
let namesToIds = getSortedNames();
for (let name in namesToIds) {
addToDroplist(namesToIds[name], name, "#pmNamesDroplist");
}
}, 3000);
}
function processPmMsg(user, data, pm) {
let pmMsgQuery = pm.$msgs[0].childNodes[pm.$msgs[0].childNodes.length - 1];
const classes = $(pmMsgQuery).attr("class").split(" ");
if (classes[0] === "typing-notify") {
pmMsgQuery = pm.$msgs[0].childNodes[pm.$msgs[0].childNodes.length - 2];
}
let nameQuery = $(pmMsgQuery.childNodes[1].childNodes[1]);
let msgQuery = $(pmMsgQuery.childNodes[1].childNodes[2]);
let pmCommand = parsePostCommand(data.msg);
pmMsgQuery.childNodes[1].childNodes[0].innerHTML = createTimestamp(data.date);
if (pmCommand.includes("rng")) {
msgQuery[0].innerHTML = ` ${generateRngResult(
pmCommand,
data.msg,
data.date
)} <span style="background:#4A4; color: #FFF;"> ☑ </span>`;
nameQuery[0].innerHTML = `${user.props.name}`;
}
else if (pmCommand === "me") {
nameQuery[0].innerHTML = `${user.props.name} `;
}
else {
nameQuery.html(` ${user.props.name}: `);
}
$(msgQuery).removeClass("action");
nameQuery.attr("style", `color: #${user.props.color[0]}`);
if (pmSettings.colorText) {
msgQuery.attr("style", `color: #${user.props.color[0]}`);
}
if (pmSettings.pmEnableImagePreview) {
let contentsChildren = msgQuery[0].children;
let images = [];
for (let i = 0; i < contentsChildren.length; i++) {
const child = contentsChildren[i];
if (child.tagName == "A") {
const url = child.attributes["href"].nodeValue;
if (url.search(`\.png|\.jpe?g|\.gif|\.webm`) > -1) {
images.push(url);
}
}
}
msgQuery.find("div.rpht-images").remove();
let imageArea = `<div class="rpht-images">`;
images.forEach((url) => {
imageArea += `<img src="${url}" width="320px" alt="${url}"> `;
});
imageArea += `</div>`;
msgQuery.append(imageArea);
}
}
/**
* Adds an away status to a character
*/
function setPmAway() {
let userId = $("#pmNamesDroplist option:selected").val();
let name = $("#pmNamesDroplist option:selected").html();
if (!awayMessages[userId]) {
let awayMsgObj = {
usedPmAwayMsg: false,
message: "",
enabled: false,
};
awayMessages[userId] = awayMsgObj;
}
if (!awayMessages[userId].enabled) {
$("#pmNamesDroplist option:selected").html("[Away]" + name);
}
awayMessages[userId].enabled = true;
awayMessages[userId].message = $("input#awayMessageTextbox").val();
$("#pmNamesDroplist option:selected").css("background-color", "#FFD800");
$("#pmNamesDroplist option:selected").prop("selected", false);
console.log(
"RPH Tools[setPmAway]: Setting away message for",
name,
"with message",
awayMessages[userId].message
);
}
/**
* Removes an away status for a character
*/
function removePmAway(userId) {
if (!awayMessages[userId]) {
return;
}
let name = $("#pmNamesDroplist option:selected").html();
if (awayMessages[userId].enabled && name.startsWith("[Away]")) {
awayMessages[userId].enabled = false;
$("#pmNamesDroplist option:selected").html(name.substring(6, name.length));
$("#pmNamesDroplist option:selected").css("background-color", "");
$("input#awayMessageTextbox").val("");
console.log("RPH Tools[removePmAway]: Remove away message for", name);
}
}
/** Forces removal of a friend */
async function forceUnfriend(yourName, friendName) {
const yourUser = await getUserByName(yourName);
const friendUser = await getUserByName(friendName);
socket.emit("deny-friend-request", {
from: yourUser.props.id,
to: friendUser.props.id
});
}
/** Forces removal of a blocked name */
function forceUnblock(otherName) {
getUserByName(otherName, (User) => socket.emit("unblock", {
id: User.props.id
}));
}
function loadSettings() {
let storedSettings = settingsModule.getSettings(localStorageName);
pmSettings = {
colorText: false,
keepInBackground: true,
notify: false,
audioUrl: "https://www.rphaven.com/sounds/imsound.mp3",
sideTabs: false,
pmEnableImagePreview: false,
blockedUsers: [],
};
if (storedSettings) {
pmSettings = Object.assign(pmSettings, storedSettings);
}
$("#receivePmsEnable").prop("checked", true);
$("#pmColorEnable").prop("checked", pmSettings.colorText);
$("#pmSideTabsEnable").prop("checked", pmSettings.sideTabs);
$("#keepInBgEnable").prop("checked", pmSettings.keepInBgEnable);
$("#pmEnableImagePreview").prop("checked", pmSettings.pmEnableImagePreview);
$("#pmNotify").prop("checked", pmSettings.notify);
$("#pmPingURL").val(pmSettings.audioUrl);
rph.sounds.im = new Audio(pmSettings.audioUrl);
if (pmSettings.sideTabs === true) {
let pmTabs = $("div.ul-rows").detach();
$("head").append(`<style>ul.pm-tabs li.tab{display: block; width: auto;}</style>`);
$("#pm-dialog").css("display", "flex");
$("#pm-dialog > div")[0].id = "pm-content";
$("#pm-content").css("width", "75%");
$("#pm-dialog").append(`<div id="pm-tabs" style="width: 25%; background: #303235; overflow: auto"></div>`);
$("#pm-tabs").append(pmTabs);
}
}
function getHtml() {
return html;
}
function toString() {
return "PM Module";
}
return {
init: init,
loadSettings: loadSettings,
getHtml: getHtml,
toString: toString,
};
})();
/**
* This module handles chat modding features. These include an easier way to
* issue kicks, bans, promotions and demotions. It also can set up monitoring
* of certain words and alert the mod.
*/
let moddingModule = (function () {
let settings = {};
let localStorageName = "modSettings";
let html = {
tabId: "modding-module",
tabName: "Modding",
tabContents: "<h3>Moderator Control</h3><br>" +
"<h4>Shortcuts</h4>" +
'<div class="rpht-option-block">' +
" <p><strong>Note:</strong> This must be done with the mods chat tab selected.</p>" +
" <p>General form: <code>/[action] [username],[reason]</code>. The reason is optional.</p>" +
" <p>Example: <code>/kick Alice,Being rude</code></p>" +
" <p>Supported actions: kick, ban, unban, add-mod, remove-mod, add-owner, remove-owner</p>" +
"</div>" +
"<h4>Mod commands</h4>" +
'<div class="rpht-option-block">' +
' <div class="rpht-option-section">' +
' <label class="rpht-label split-input-label">Room-Name pair</label>' +
' <select class="split-input-label" id="roomModSelect"><option value=""><Blank out fields></option></select><br /><br />' +
' <label class="rpht-label split-input-label">Room:</label><input class="split-input-label" type="text" id="modRoomTextInput" placeholder="Room"><br /><br />' +
' <label class="rpht-label split-input-label">Mod name:</label><input class="split-input-label" type="text" id="modFromTextInput" placeholder="Your mod name"><br /><br />' +
' <label class="rpht-label split-input-label">Reason Message:</label><input class="split-input-label" type="text" id="modMessageTextInput" placeholder="Message"><br /><br />' +
" </div>" +
' <div class="rpht-option-section option-section-bottom">' +
" <p>Perform action on these users (comma separated): </p>" +
' <textarea name="modTargetTextInput" id="modTargetTextInput" rows=2 class="rpht_textarea"></textarea>' +
" <br /><br />" +
' <table style="width: 600px;" cellpadding="2">' +
" <tbody>" +
" <tr>" +
' <td valign="top">' +
' <button style="width: 60px;" type="button" id="kickButton">Kick</button>' +
" </td>" +
" <td>" +
' <button style="width: 60px; margin-bottom: 8px;" type="button" id="banButton">Ban</button><br />' +
' <button style="width: 60px;" type="button" id="unbanButton">Unban</button>' +
" </td>" +
" <td>" +
' <button style="width: 60px; margin-bottom: 8px;" type="button" id="modButton">Mod</button><br>' +
' <button style="width: 60px;" type="button" id="unmodButton">Unmod</button>' +
" </td>" +
" <td>" +
' <button style="width: 80px; margin-bottom: 8px;" type="button" id="OwnButton">Owner</button><br>' +
' <button style="width: 80px;" type="button" id="UnownButton">Unowner</button>' +
" </td>" +
" </tr>" +
" </tbody>" +
" </table>" +
" <br><br>" +
' <button type="button" id="resetPwButton">Reset PW</button>' +
" </div>" +
"</div>" +
"<h4>Word Alert</h4>" +
'<div class="rpht-option-block">' +
' <div class="rpht-option-section">' +
' <label class="rpht-label checkbox-label" for="wordAlertEnable">Enable word alerting</label>' +
' <label class="switch"><input type="checkbox" id="wordAlertEnable"><span class="rpht-slider round"></span></label>' +
' <label class="rpht-label descript-label">Highlights words that you want to be pinged on for moderation</label>' +
" </div>" +
' <div class="rpht-option-section option-section-bottom">' +
" <p><strong>Note:</strong> Separate all entries with a pipe character ( | ).</p>" +
' <textarea name="alertTriggers" id="alertTriggers" rows=4 class="rpht_textarea"></textarea>' +
" </div>" +
"</div>",
};
let alertSound = null;
let roomNamePairs = {};
function init() {
loadSettings();
$("#roomModSelect").change(function () {
let roomModeIdx = $("#roomModSelect")[0].selectedIndex;
let roomModVal = $("#roomModSelect")[0].options[roomModeIdx].value;
if (roomNamePairs[roomModVal]) {
$("input#modRoomTextInput").val(roomNamePairs[roomModVal].roomName);
$("input#modFromTextInput").val(roomNamePairs[roomModVal].modName);
}
else {
$("input#modRoomTextInput").val("");
$("input#modFromTextInput").val("");
}
});
$("#resetPwButton").click(function () {
let room = $("input#modRoomTextInput").val();
getUserByName($("input#modFromTextInput").val(), function (user) {
socket.emit("modify", {
room: room,
userid: user.props.id,
props: {
pw: false,
},
});
});
});
$("#kickButton").click(function () {
modAction("kick");
});
$("#banButton").click(function () {
modAction("ban");
});
$("#unbanButton").click(function () {
modAction("unban");
});
$("#modButton").click(function () {
modAction("add-mod");
});
$("#unmodButton").click(function () {
modAction("remove-mod");
});
$("#OwnButton").click(function () {
modAction("add-owner");
});
$("#UnOwnButton").click(function () {
modAction("remove-owner");
});
$("#wordAlertEnable").click(function () {
settings.alertOnWords = $("#wordAlertEnable").is(":checked");
settingsModule.saveSettings(localStorageName, settings);
});
$("#modAlertWords").blur(function () {
settings.alertWords = $("#modAlertWords")
.val()
.replace(/\r?\n|\r/, "");
settingsModule.saveSettings(localStorageName, settings);
});
$("#modAlertUrl").blur(function () {
if (validateSetting("modAlertUrl", "url")) {
settings.alertUrl = $("#modAlertUrl").val();
settingsModule.saveSettings(localStorageName, settings);
alertSound = new Audio(settings.alertUrl);
}
});
$("#alertTriggers").blur(function () {
settings.alertWords = $("#alertTriggers").val();
settingsModule.saveSettings(localStorageName, settings);
});
}
/**
* Performs a modding action. This will look for a user's vanity name first, then act on that.
* @param {string} action Name of the action being performed
*/
function modAction(action) {
let targets = $("#modTargetTextInput")
.val()
.replace(/\r?\n|\r/, "");
let vanityMap = getVanityNamesToIds();
targets = targets.split(",");
console.log("RPH Tools[modAction]: Performing", action, "on", targets);
targets.forEach(function (target) {
if (vanityMap[target]) {
target = messenger.users[vanityMap[target]].props.name;
}
emitModAction(
action,
target,
$("input#modFromTextInput").val(),
$("input#modRoomTextInput").val(),
$("input#modMessageTextInput").val()
);
});
}
/**
* Sends off the mod action to the chat socket
* @param {string} action Name of the action being performed
* @param {string} targetName User name of the recipient of the action
*/
function emitModAction(action, targetName, modName, roomName, reasonMsg) {
getUserByName(targetName, function (target) {
getUserByName(modName, function (user) {
let modMessage = "";
if (action === "kick" || action === "ban" || action === "unban") {
modMessage = reasonMsg;
}
socket.emit(action, {
room: roomName,
userid: user.props.id,
targetid: target.props.id,
msg: modMessage,
});
});
});
}
function findUserAsMod(userObj) {
Object.keys(rph.rooms).forEach((roomname) => {
let roomObj = getRoom(roomname);
if (
roomObj.props.mods.indexOf(userObj.props.id) > -1 ||
roomObj.props.owners.indexOf(userObj.props.id) > -1
) {
addModRoomPair(userObj.props, roomname);
}
});
}
/**
* Adds a key/value pair option to the Room-Name Pair droplist.
* @param {number} userId User ID of the mod
* @param {object} thisRoom Object containing the room data.
*/
function addModRoomPair(userProps, roomName) {
let roomNamePair = roomName + ": " + userProps.name;
let roomNameValue = roomName + "." + userProps.id;
let roomNameObj = {
roomName: roomName,
modName: userProps.name,
modId: userProps.id,
};
if (roomNamePairs[roomNameValue] === undefined) {
roomNamePairs[roomNameValue] = roomNameObj;
$("#roomModSelect").append('<option value="' + roomNameValue + '">' + roomNamePair + "</option>");
}
}
/**
* Plays the alert sound
*/
function playAlert() {
alertSound.play();
}
function loadSettings() {
settings = {
alertOnWords: false,
alertWords: "",
alertUrl: "https://www.rphaven.com/sounds/boop.mp3",
};
let storedSettings = settingsModule.getSettings(localStorageName);
if (storedSettings) {
settings = Object.assign(settings, storedSettings);
}
$("#modAlertUrl").val(settings.alertUrl);
$("#wordAlertEnable").prop("checked", settings.alertOnWords);
$("#modAlertWords").val(settings.alertWords);
alertSound = new Audio(settings.alertUrl);
$("#alertTriggers").val(settings.alertWords);
}
function getAlertWords() {
return settings.alertWords;
}
function getHtml() {
return html;
}
function toString() {
return "Modding Module";
}
return {
init: init,
emitModAction: emitModAction,
findUserAsMod: findUserAsMod,
addModRoomPair: addModRoomPair,
playAlert: playAlert,
loadSettings: loadSettings,
getAlertWords: getAlertWords,
getHtml: getHtml,
toString: toString,
};
})();
let logManagerModule = (function () {
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;
const html = {
tabId: "log-manager-module",
tabName: "Log Manager",
tabContents: `
<h3>Log Manager</h3><br>
<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>`,
};
function init() {
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);
$("#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 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`).val(primaryKey);
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;
}
function getHtml() {
return html;
}
function toString() {
return "Log Manager Module";
}
return {
init: init,
getHtml: getHtml,
toString: toString,
};
})();
/**
* Handles importing, exporting, and deleting of settings.
*/
let settingsModule = (function () {
let html = {
tabId: "settings-module",
tabName: "Settings",
tabContents: "<h3>Script Settings</h3><br>" +
"<h4>Import/Export settings</h4>" +
'<div class="rpht-option-block">' +
' <div class="rpht-option-section">' +
' <label class="rpht-label split-input-label">Export settings to a JSON text file</label>' +
` <a class="split-input-label" id="downloadSettingsLink" download="settings.txt">Download settings</a>` +
" </div>" +
' <div class="rpht-option-section">' +
' <label class="rpht-label split-input-label">Import settings from a JSON text file</label>' +
' <input class="split-input-label" id="importFileInput" type="file" /> <button style="display: none;" id="retryImportButton">Retry</button>' +
' <p id="importSettingsStatus"></p>' +
" </div>" +
' <div class="rpht-option-section option-section-bottom">' +
' <label class="rpht-label checkbox-label">Import/export settings from text</label>' +
' <textarea name="importExportText" id="importExportTextarea" rows=10 class="rpht_textarea"></textarea>' +
" <br /><br />" +
' <button type="button" style="width: 60px;" id="exportButton">Export</button>' +
' <button type="button" style="margin-left: 10px; width: 60px;" id="importButton">Import</button>' +
' <button type="button" style="float: right; background: red;" id="deleteSettingsButton">Delete settings</button>' +
" </div>" +
"</div>",
};
let confirmDelete = false;
let deleteTimer = null;
/**
* Initializes the GUI components of the module.
*/
function init() {
if (!localStorage.getItem(SETTINGS_NAME)) {
localStorage.setItem(SETTINGS_NAME, JSON.stringify({}));
}
$("#importButton").click(() => {
let importSuccess = importSettingsHanlder($("textarea#importExportTextarea").val());
if (importSuccess) {
markProblem("textarea#importExportTextarea", false);
}
else {
markProblem("textarea#importExportTextarea", true);
}
});
$("#exportButton").click(() => {
$("textarea#importExportTextarea").val(exportSettings());
});
$("#downloadSettingsLink").click(() => {
let link = document.getElementById("downloadSettingsLink");
link.href = makeTextFile(localStorage.getItem(SETTINGS_NAME));
$("#downloadSettingsLink").attr("download", `rph-tools-settings.txt`);
});
$("#importFileInput").change(() => {
let file = $("#importFileInput")[0].files[0];
(async () => {
fileContent = await file.text();
let successfulImport = importSettingsHanlder(fileContent);
if (successfulImport === false) {
$("#importSettingsStatus").first().text("There was a problem with the import");
}
else {
$("#importSettingsStatus").first().text("Import successful");
}
})();
});
$("#printSettingsButton").click(() => {
printSettings();
});
$("#deleteSettingsButton").click(() => {
deleteSettingsHanlder();
});
}
/**
* Handles the initial portion of importing settings. This checks the input
* to see if it's a valid JSON formatted string.
*/
function importSettingsHanlder(jsonText) {
let successfulImport = false;
try {
let newSettings = JSON.parse(jsonText);
localStorage.setItem(SETTINGS_NAME, JSON.stringify(newSettings));
rphToolsModule.getAllModules().forEach((module) => {
if (module.loadSettings) {
module.loadSettings();
}
});
successfulImport = true;
}
catch {
console.log("[RPHT.Settings]: There was a problem with importing settings");
}
return successfulImport;
}
/**
* Exports settings into a JSON formatted string
*/
function exportSettings() {
const settings = JSON.parse(localStorage.getItem(SETTINGS_NAME));
delete settings.chatLogs;
markProblem("textarea#importExportTextarea", false);
return JSON.stringify(settings, "\n", 4);
}
/**
* Logic to confirm deleting settings. The button needs to be pressed twice
* within 10 seconds for the settings to be deleted.
*/
function deleteSettingsHanlder() {
if (confirmDelete === false) {
$("#deleteSettingsButton").text("Press again to delete");
confirmDelete = true;
/* Set a timeout to make "confirmDelete" false automatically */
deleteTimer = setTimeout(() => {
confirmDelete = false;
$("#deleteSettingsButton").text("Delete Settings");
}, 10 * 1000);
}
else if (confirmDelete === true) {
clearTimeout(deleteTimer);
console.log("RPH Tools[Settings Module]: Deleting settings");
$("#deleteSettingsButton").text("Delete Settings");
confirmDelete = false;
localStorage.removeItem(SETTINGS_NAME);
localStorage.setItem(SETTINGS_NAME, JSON.stringify({}));
rphToolsModule.getAllModules().forEach((module) => {
if (module.loadSettings) {
console.log(`RPH Tools[Settings Module]: ${module.toString()}`);
module.loadSettings();
}
});
}
}
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;
}
function saveSettings(moduleName, moduleSettings) {
let settings = JSON.parse(localStorage.getItem(SETTINGS_NAME));
settings[moduleName] = {};
settings[moduleName] = moduleSettings;
localStorage.setItem(SETTINGS_NAME, JSON.stringify(settings));
}
function getSettings(moduleName) {
let settings = JSON.parse(localStorage.getItem(SETTINGS_NAME));
let moduleSettings = null;
if (settings[moduleName]) {
moduleSettings = settings[moduleName];
}
return moduleSettings;
}
function getHtml() {
return html;
}
function toString() {
return "Settings Module";
}
/**
* Public members of the module
*/
return {
init: init,
saveSettings: saveSettings,
getSettings: getSettings,
getHtml: getHtml,
toString: toString,
};
})();
/**
* This module handles the "About" section for information on RPH Tools.
*/
let aboutModule = (function () {
let html = {
tabId: "about-module",
tabName: "About",
tabContents: "<h3>RPH Tools</h3><br>" +
"<p><strong>Version: " +
VERSION_STRING +
"</strong>" +
' | <a href="https://openuserjs.org/install/shuffyiosys/RPH_Tools.user.js" target="_blank">Install the latest version</a>' +
' | <a href="https://github.com/shuffyiosys/rph-tools/blob/master/CHANGELOG.md" target="_blank">Version history</a>' +
' | <a href="https://discord.gg/HBEaGjs" target="_blank">Discord channel</a>' +
' | <a href="https://openuserjs.org/scripts/shuffyiosys/RPH_Tools" target="_blank">OpenUserJs page</a>' +
"</p></br>" +
'<p>Created by shuffyiosys. Under MIT License (SPDX: MIT). Feel free to make contributions to <a href="https://github.com/shuffyiosys/rph-tools" target="_blank">the repo</a>!</p><br />' +
'<p><a href="https://github.com/shuffyiosys/rph-tools/blob/master/docs/quick-guide.md" target="_blank">Quick guide to using RPH Tools</a></p></br>',
};
function init() {
return;
}
function getHtml() {
return html;
}
function toString() {
return "About Module";
}
return {
init: init,
getHtml: getHtml,
toString: toString,
};
})();
/**
* Main RPH Tools module
*/
let rphToolsModule = (function () {
let modules = [];
let rpht_css =
"<style>" +
"#settings-dialog .inner > div > div.rpht-option-block{width:640px;border:#888 solid 1px;border-radius:10px;padding:12px;padding-top:16px;padding-bottom:16px;margin-bottom:16px;}" +
".rpht-option-section{border-bottom:#444 solid 1px;padding-bottom:12px;margin-bottom:12px;}" +
".option-section-bottom{border-bottom:none;margin-bottom:0;}" +
".rpht-label{padding-left: 0px;text-align:justify;display:inline-block;cursor:default;}" +
".checkbox-label{font-weight:700;width:542px;cursor:pointer;}" +
".descript-label{width:480px;margin-top:8px;}" +
".text-input-label{width:400px;}" +
".split-input-label {width: 300px;}" +
".rpht_textarea{border:1px solid #000;width:611px;padding:2px;background:#e6e3df;}" +
".rpht_chat_tab{height:54px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;}" +
".rpht-checkbox{height:16px;width:16px;}" +
"input.rpht-short-input{width:200px;}" +
"input.rpht-long-input{max-width:100%;}" +
".msg-padding{line-height: 1.25em}" +
".switch{position:relative;right:12px;width:50px;height:24px;float:right;}" +
".switch input{opacity:0;width:0;height:0;}" +
".rpht-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;-webkit-transition:.4s;transition:.4s}" +
'.rpht-slider:before{position:absolute;content:"";height:16px;width:16px;left:4px;bottom:4px;background-color:#fff;-webkit-transition:.4s;transition:.4s}' +
"input:checked+.rpht-slider{background-color:#2196f3}" +
"input:focus+.rpht-slider{box-shadow:0 0 1px #2196f3}" +
"input:checked+.rpht-slider:before{transform:translateX(26px)}" +
".rpht-slider.round{border-radius:34px}" +
".rpht-slider.round:before{border-radius:50%}" +
".rpht-tooltip-common{position: absolute; bottom: 120px; left: 200px; width: auto; height: auto; color: #dedbd9; background: #303235; opacity: 0.9; padding: 10px;}" +
".rpht-cmd-tooltip{width: 800px; height: auto;}" +
".rpht-cmd-tooltip:hover{opacity: 0;}" +
".rpht-die-label{text-align: right; display: inline-block; width: 74px; margin-right: 7px;}" +
".rpht-die-updown{width: 60px; min-width: 0px;}" +
".rpht-close-btn{margin-left: 40px; width: 24px; cursor: pointer;}" +
".rpht-close-btn:hover{background: #CA7169;}" +
"#diceRollerPopup button{width: 146px;}" +
"</style>";
/**
* Initializes the modules and the HTML elements it handles.
* @param {Array} addonModules Modules to add into the system.
*/
function init(addonModules) {
let $settingsDialog = $("#settings-dialog");
modules = addonModules;
if (Notification.permission !== "denied") {
Notification.requestPermission();
}
$("head").append(rpht_css);
$("#settings-dialog .inner ul.tabs").append("<h3>RPH Tools</h3>");
/* Checks to see if there's a local store for settings and creates one
* if there isn't. */
let settings = localStorage.getItem(SETTINGS_NAME);
if (!settings) {
settings = {};
localStorage.setItem(SETTINGS_NAME, JSON.stringify(settings));
}
modules.forEach(function (module) {
if (module.getHtml) {
html = module.getHtml();
$("#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();
});
module.init();
}
});
}
/**
* Returns a module based on a name passed in.
* @param {string} name Name of the module to get the data
* @returns Returns the module, if found. Otherwise returns null.
*/
let getModule = function (name) {
let module = null;
for (let i = 0; i < modules.length; i++) {
if (modules[i].toString() === name) {
module = modules[i];
break;
}
}
return module;
};
function getAllModules() {
return modules;
}
function getHtml() {
return html;
}
function toString() {
return "RPH Tools Module";
}
return {
init: init,
getModule: getModule,
getAllModules: getAllModules,
getHtml: getHtml,
toString: toString,
};
})();
/****************************************************************************
* Script initializations to execute after the page loads
***************************************************************************/
$(function () {
console.log(`RPH Tools ${VERSION_STRING} start`);
let modules = [chatModule, pmModule, moddingModule, logManagerModule, settingsModule, aboutModule];
rphToolsModule.init(modules);
console.log("RPH Tools initialization complete");
});