[Reddit] Modmail++

Additional tools and information to Reddit's Modmail

当前为 2021-11-23 提交的版本,查看 最新版本

// ==UserScript==
// @name        [Reddit] Modmail++
// @namespace   HKR
// @match       https://mod.reddit.com/mail/*
// @grant       none
// @version     2.0
// @author      HKR
// @description Additional tools and information to Reddit's Modmail
// @icon        https://www.redditstatic.com/modmail/favicon/favicon-32x32.png
// @supportURL  https://github.com/Hakorr/Userscripts/issues
// ==/UserScript==

console.log("[ModmailExtraInfo] %cScript started!", "color: green");

/* Do not touch */
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
var first = false;
/* Do not touch */

function main() {
	console.log("[ModmailExtraInfo] %cMain function ran!", "color: grey");

	/* SETTINGS */

	/* NOTE (If you want to use the Custom Responses): Reddit's sync feature removes the script's added text.
	- If you block "https://oauth.reddit.com/api/mod/conversations/*****?markRead=false&redditWebClient=modmail", the added text will stay. */

	//Variables for the responses
	const subTag = $(".ThreadTitle__community").href.slice(23);
	const userTag = "u/" + $(".InfoBar__username").innerText;
	const modmail = `[modmail](https://www.reddit.com/message/compose?to=/${subTag})`;
	const rules = `https://www.reddit.com/${subTag}/about/rules`;
	const randItem = itemArr => itemArr[Math.floor(Math.random() * itemArr.length)];

	//Text color settings
	var textColor = null, lightModeTextColor = "#6e6e6e", darkModeTextColor = "#757575";

	//Title color settings
	var titleColor = null, lightModeTitleColor = "#2c2c2c", darkModeTitleColor = "#a7a7a7";

	//Listbox color settings
	var listBoxColor = null, lightModeListColor = "#fff", darkModeListColor = "#242424";

	//Data (Such as numbers) color settings
	const dataColor = "#0079d3";

	//No response list is created if false
	const enableCustomResponses = true;

	//No chat profile icons are added if false
	const chatProfileIcons = true;

	//Feel free to edit and add more responses suitable for you! Replace means if to replace all text or just to add the text.
	const responses = [
		{
			"name":"Select a template",
			"replace":true,
			"content":``
		},
		{
			"name":"Default approved",
			"replace":true,
			"content":`Hey, approved the post!`
		},
		{
			"name":"Default rule broken",
			"replace":true,
			"content":`Your post broke our [rules](${rules}).\n\nThe action will not be reverted.`
		},
		{
			"name":"Add greetings",
			"replace":true,
			"content":`${randItem(["Greetings","Hello","Hi"])} ${userTag},\n\n`
		},
		{
			"name":"Add thanks",
			"replace":false,
			"content":`\n\nThank you!`
		},
		{
			"name":"Add subreddit mention",
			"replace":false,
			"content":`${subTag}`
		},
		{
			"name":"Add user mention",
			"replace":false,
			"content":`${userTag}`
		},
		{
			"name":"Add Modmail link",
			"replace":false,
			"content":`${modmail}`
		},
		{
			"name":"Add Content Policy",
			"replace":false,
			"content":`[Content Policy](https://www.redditinc.com/policies/content-policy)`
		},
		{
			"name":"Add User Agreement",
			"replace":false,
			"content":`[User Agreement](https://www.redditinc.com/policies/user-agreement)`
		},
		{
			"name":"Add Rickroll",
			"replace":false,
			"content":`[link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)`
		}
	];

	/* ---------- JS & HTML ---------- */
	
	function time(UNIX_timestamp){
		//Get UNIX time
		var d = new Date(UNIX_timestamp * 1000);
		const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

		//Get year, month, date, hour, min & sec variables
		var year = d.getFullYear(), 
		monthNum = d.getMonth() + 1,
		month = months[d.getMonth()],
		date = d.getDate(), 
		hour = fixnumber(d.getHours()), 
		min = fixnumber(d.getMinutes()), 
		sec = fixnumber(d.getSeconds());

		//Construct the time (DD/MM/YY HH/MM/SS) and return it
		var time = `${date}.${monthNum}.${year} ${hour}:${min}:${sec}`;
		return time;
	}
	
	//Adds a zero suffix if x < 10
	const fixnumber = number => number < 10 ? "0" + number : number;

	//Removes the u/ prefix
	const removePrefix = username => username.includes("u/") ? username.slice(2) : username;

	//Adds the u/ prefix if nonexistant
	const keepPrefix = username => username.includes("u/") ? username : "u/" + username;

	//Function to avoid XSS
	function sanitize(evilstring) {
		const decoder = document.createElement('div')
		decoder.innerHTML = evilstring;
		return decoder.textContent;
	}

	//Appends the info (main, karma, links) to the page
	function addInfo(){
		//Load and parse username
		var username = removePrefix($(".InfoBar__username").innerText);
		var about = "https://www.reddit.com/user/" + username + "/about.json";
		const xhr = new XMLHttpRequest();
	
		//Once the user info JSON has been fetched
		xhr.onload = () => {
			var user = JSON.parse(xhr.responseText);
			//Separator HTML element
			var seperator = document.createElement('div');
			seperator.innerHTML = '<div class="InfoBar__modActions"></div>';
	
			//HTML element that contains all the data
			var userDetails = document.createElement('div');
			userDetails.classList.add("InfoBar__age");
			userDetails.innerHTML = `<img class="profileIcon" src="${user.data.icon_img}" width="25">
			<a class="InfoBar__username" href="https://www.reddit.com/user/${user.data.name}">${user.data.subreddit.display_name_prefixed}</a>
			<h1 style="color: ${textColor} ; font-size: 11px; margin-top: 17px; margin-bottom: 10px;">${sanitize(user.data.subreddit.public_description)}</h1>
			<h1 class="dataTitle">Main</h1>
			<div class="dataText">
				<p>Created: <span class="value">${time(user.data.created)}</span></p>
				<p>UserID: <span class="value">${user.data.id}</span></p>
				<p>Verified: <span class="value">${user.data.verified}</span></p>
				<p>Employee: <span class="value">${user.data.is_employee}</span></p>
				<p>NSFW Profile: <span class="value">${user.data.subreddit.over_18}</span></p>
			</div>
			<h1 class="dataTitle">Karma</h1>
			<div class="dataText">
				<p>Post: <span class="value">${user.data.link_karma}</span></p>
				<p>Comment: <span class="value">${user.data.comment_karma}</span></p>
				<p>Total: <span class="value">${user.data.total_karma}</span></p>
				<p>Awardee: <span class="value">${user.data.awardee_karma}</span></p>
				<p>Awarder: <span class="value">${user.data.awarder_karma}</span></p>
			</div>
			<h1 class="dataTitle">Links</h1>
				<div style="padding-left: 10px;">
				<a class="InfoBar__recent" href="https://redditmetis.com/user/${user.data.name}" target="_blank">Redditmetis</a>
				<a class="InfoBar__recent" href="https://www.reddit.com/search?q=${user.data.name}" target="_blank">Reddit Search</a>
				<a class="InfoBar__recent" href="https://www.google.com/search?q=%22${user.data.name}%22" target="_blank">Google Search</a>
			</div>`;

			//Add profile pictures
			if(chatProfileIcons) {
				//Icon element
				var chatProfileIcon = document.createElement('div');
				chatProfileIcon.innerHTML = `<img class="chatProfileIcon" src="${user.data.icon_img}" width="25">`;

				//Loop trough every username on chat
				for(var i = 0; i < $$(".ThreadPreview__author").length; i++) {
					//Get username (u/xxxxxx)
					let name = $$(".Author__text")[i].innerText;
					//Check if there is an icon appended already
					let exists = $$(".ThreadPreview__author")[i].childNodes.length == 1 ? false : true;
					//If the username is the user (non-mod)
					if(removePrefix(name) == username && !exists) {
						//Append the icon next to the username -> [icon] u/username
						$$(".ThreadPreview__author")[i].insertBefore(chatProfileIcon.cloneNode(true), $$(".ThreadPreview__author")[i].firstChild);
					}
				}
			}

			//Append the elements
			$(".ThreadViewer__infobar").appendChild(seperator);
			$(".ThreadViewer__infobar").appendChild(seperator);
			$(".ThreadViewer__infobar").appendChild(userDetails);
			$(".ThreadViewer__infobar").appendChild($(".ThreadViewer__infobar").firstChild);
			$(".InfoBar").appendChild($(".InfoBar__modActions"));
			$(".InfoBar").insertBefore($(".InfoBar__modActions"),$(".InfoBar").firstChild);
			if($(".InfoBar__banText"))
				$(".ThreadViewer__infobar").insertBefore($(".InfoBar__banText"),$(".ThreadViewer__infobar").firstChild);
			
			//Remove certain elements
			$$(".InfoBar__username")[1].outerHTML = "";
			$$(".InfoBar__age")[1].outerHTML = "";
			$$(".InfoBar__modActions")[1].outerHTML = "";
		};
		
		//Get user details
		xhr.open('GET', about);
		xhr.send();
	}
	
	//Appends the response template listbox to the page
	function addResponseBox() {
		//Listbox element
		var responseBox = document.createElement('div');
		responseBox.classList.add("select");
		responseBox.innerHTML = `<h2 class="dataTitle">Response templates</h2>
		<select id="responseListbox" onchange="listBoxChanged(this.value);" onfocus="this.selectedIndex = -1;"/>
		<span class="focus"></span>`;
	
		//Script element to head
		var headJS = document.createElement('script');
		headJS.innerHTML = `function listBoxChanged(message) {
			var messageBox = document.getElementsByClassName("Textarea ThreadViewerReplyForm__replyText")[0];
			var responses = ${JSON.stringify(responses)};
			var response = responses.find(x => x.content == message);
			response.replace ? messageBox.value = message : messageBox.value += message;
			console.log("[ModmailExtraInfo] New messageBox value: %c" + messageBox.value,"color: orange");
		}`;
	
		function populate() {
			var select = $("#responseListbox");
			for(var i = 0; i < responses.length; i++) {
				select.options[select.options.length] = new Option(responses[i].name, responses[i].content);
			}
		}
	
		$(".ThreadViewer__replyContainer").prepend(responseBox);
		var head = $("head");
		head.appendChild(headJS);
	
		populate();
	}

	//Detects the current theme (dark/light) and applies the correct color (for the added elements)
	function themeColors() {
		var darkTheme = $$(".theme-dark").length ? true : false;
		if(darkTheme) {
			console.log("[ModmailExtraInfo] Dark mode detected! Setting colors...");
			textColor = darkModeTextColor;
			titleColor = darkModeTitleColor;
			listBoxColor = darkModeListColor;
		} else {
			console.log("[ModmailExtraInfo] Light mode detected! Setting colors...");
			textColor = lightModeTextColor;
			titleColor = lightModeTitleColor;
			listBoxColor = lightModeListColor;
		}
	}

	themeColors();

	//Took advice for the listbox CSS from moderncss.dev/custom-select-styles-with-pure-css, thanks!
	var css = `.profileIcon:hover {
		-ms-transform: scale(6);
		-webkit-transform: scale(6);
		transform: scale(6);
	}
	.profileIcon {
		position: relative;
		bottom: 4px;
		margin-bottom: 10px;
		float: left; border-radius: 50%;
		transition: transform .1s;
	}
	.InfoBar__recentsNone {
		color: #6e6e6e;
	}
	.InfoBar__metadata, .InfoBar__recents {
		margin: 6px 0; 
		margin-left: 10px;
	}
	.value {
		color: ${dataColor};
	}
	.InfoBar__banText {
		padding-bottom: 15px;
	}
	.InfoBar__username, .InfoBar__username:visited {
		padding-left: 10px;
	}
	.ThreadViewer__infobarContainer {
		display: table;
	}
	.dataText {
		color: ${textColor}; 
		font-size: 13px;
		padding-left: 10px;
	}
	.dataTitle {
		color: ${titleColor};
		font-size: 15px; 
		margin-bottom: 3px; 
		margin-top: 5px;
	}
	.responseListbox {
		width: 50%;
		cursor: pointer;
	}
	:root {
		--select-border: #0079d3;
		--select-focus: blue;
		--select-arrow: var(--select-border);
	}
	*,
	*::before,
	*::after {
	box-sizing: border-box;
	}
	select {
		appearance: none;
		background-color: ${listBoxColor};
		color: ${textColor};
		border: none;
		padding: 0 1em 0 0;
		margin: 0;
		width: 100%;
		cursor: pointer;
		font-family: inherit;
		font-size: inherit;
		line-height: inherit;
		outline: none;
		position: relative;
	}
	.select {
		width: 100%;
		min-width: 15ch;
		max-width: 30ch;
		border: 1px solid var(--select-border);
		border-radius: 0.25em;
		padding: 0.3em 0.4em;
		font-size: 0.9rem;
		line-height: 1.1;
		background-color: ${listBoxColor};
	}
	select::-ms-expand {
		display: none;
	}
	option {
		white-space: normal;
		outline-color: var(--select-focus);
	}
	select:focus + .focus {
		position: absolute;
		top: -1px;
		left: -1px;
		right: -1px;
		bottom: -1px;
		border: 2px solid var(--select-focus);
		border-radius: inherit;
	}
	.Author__text {
		padding: 6px 0;
	}
	.chatProfileIcon {
		margin-right: 7px;
		transition: transform .1s;
		border-radius: 50%;
	}`;

	//Apply the custom css
	var styleSheet = document.createElement("style");
	styleSheet.type = "text/css";
	styleSheet.innerText = css;
	document.head.appendChild(styleSheet);

	addInfo();
	if(enableCustomResponses && $("#responseListbox") == null) addResponseBox();
	console.log("[ModmailExtraInfo] %cLoaded!", "color: lime");

} /* End of Main function */

/* Start Main function when visiting new modmail */
var pageURLCheckTimer = setInterval (function () {
	if (this.lastPathStr !== location.pathname) 
	{
		this.lastPathStr = location.pathname;

		first = true;

		let startInterval = setInterval (function () {
			if($(".InfoBar__username")) {
				if(first) main();
				first = false;
				clearInterval(startInterval);
			}
		}, 5);
	}
}, 100);