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.3 // @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.3"; 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); /* 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("\u200b") === -1) { continue; } const SEED = newMsgLines[msgIdx].split("\u200b")[1]; const MSG_CHUNKS = newMsgLines[msgIdx].split(/ /g); let validResult = true; newMsgLines[msgIdx] = newMsgLines[msgIdx].substring(0, newMsgLines[msgIdx].indexOf(" @\u200b")); /* 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="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, keeps 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> <p>Pops a desktop notification when you get a PM</p> </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>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 = {}; function init() { loadSettings(); $("#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); }); $("#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); } } 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, }; if (storedSettings) { pmSettings = Object.assign(pmSettings, storedSettings); } $("#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: ` <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"); });