[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.1
// @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("[Modmail++] %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("[Modmail++] %cMain function ran!", "color: grey");

	/* SETTINGS */

	//Variables for the responses
	const subTag = $(".ThreadTitle__community").href.slice(23); //Format r/subreddit
	const userTag = "u/" + $(".InfoBar__username").innerText; //Format u/username
	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;

	const placeholderMessage = randItem([
		"Message...",
		"Look, a bird! Message...",
		"What have you been up to today? Message...",
		"Beautiful day, isn't it? Message...",
		"Was the weather nice? Message...",
		"You look good today! Message...",
		"What dreams did you see last night? Message...",
		"What did you do today? Message...",
		"ヽ(o`皿′o)ノ AAAAAAAaaahh, spooked you! Message...",
		"What did you eat today? Message...",
		"Have you drank enough water? Message...",
		"Remember to stretch! Message...",
		"≖‿≖ I live inside of your walls. Message...",
		"(✿◠‿◠) Message...",
		"ಠ╭╮ಠ This modmail isn't going to get a reply just by itself, get back to work! Message..."
	]);

	//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":false,
			"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 Karma Link",
			"replace":false,
			"content":`[karma](https://reddit.zendesk.com/hc/en-us/articles/204511829-What-is-karma-)`
		},
		{
			"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() {
		//Hide real textarea and append a new one (so the text won't get removed by the sync feature)
		$(".ThreadViewerReplyForm__replyText").style.cssText += 'display: none';
		const txtArea = document.createElement("textarea");
		txtArea.setAttribute('class', 'Textarea ThreadViewerReplyForm__replyText ');
		txtArea.setAttribute('name', 'body');
		txtArea.setAttribute('placeholder', `${placeholderMessage}`);
		$(".ThreadViewerReplyForm").insertBefore(txtArea,$(".ThreadViewerReplyForm__replyFooter"));
			
		//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")[1];
			var responses = ${JSON.stringify(responses)};
			var response = responses.find(x => x.content == message);
			response.replace ? messageBox.value = message : messageBox.value += message;
			console.log("[Modmail++] 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);

		$(".ThreadViewer__replyContainer").insertBefore($(".ThreadViewer__typingIndicator"),$(".select"));

		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("[Modmail++] Dark mode detected! Setting colors...");
			textColor = darkModeTextColor;
			titleColor = darkModeTitleColor;
			listBoxColor = darkModeListColor;
		} else {
			console.log("[Modmail++] 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};
		margin-bottom: 15px;
	}
	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%;
	}
	.App__page {
		background: var(--color-tone-8);
	}
	::-webkit-scrollbar {
	  width: 10px;
	}
	::-webkit-scrollbar-track {
	  background: ${listBoxColor}; 
	}
	::-webkit-scrollbar-thumb {
	  background: #888; 
	}
	::-webkit-scrollbar-thumb:hover {
	  background: #555;
	}`;

	//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("[Modmail++] %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);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址