[Reddit] Modmail++

Additional tools and information to Reddit's Modmail

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

  1. // ==UserScript==
  2. // @name [Reddit] Modmail++
  3. // @namespace HKR
  4. // @match https://mod.reddit.com/mail/*
  5. // @grant none
  6. // @version 2.2
  7. // @author HKR
  8. // @description Additional tools and information to Reddit's Modmail
  9. // @icon https://www.redditstatic.com/modmail/favicon/favicon-32x32.png
  10. // @supportURL https://github.com/Hakorr/Userscripts/issues
  11. // ==/UserScript==
  12.  
  13. console.log("[Modmail++] %cScript started!", "color: green");
  14.  
  15. /* Do not touch */
  16. const $ = document.querySelector.bind(document);
  17. const $$ = document.querySelectorAll.bind(document);
  18. var first = false;
  19. /* Do not touch */
  20.  
  21. function main() {
  22. console.log("[Modmail++] %cMain function ran!", "color: grey");
  23.  
  24. /* SETTINGS */
  25.  
  26. //Variables for the responses
  27. const subTag = $(".ThreadTitle__community").href.slice(23); //Format r/subreddit
  28. const userTag = "u/" + $(".InfoBar__username").innerText; //Format u/username
  29. const modmail = `[modmail](https://www.reddit.com/message/compose?to=/${subTag})`;
  30. const rules = `https://www.reddit.com/${subTag}/about/rules`;
  31. const randItem = itemArr => itemArr[Math.floor(Math.random() * itemArr.length)];
  32.  
  33. //Text color settings
  34. var textColor = null, lightModeTextColor = "#6e6e6e", darkModeTextColor = "#757575";
  35.  
  36. //Title color settings
  37. var titleColor = null, lightModeTitleColor = "#2c2c2c", darkModeTitleColor = "#a7a7a7";
  38.  
  39. //Listbox color settings
  40. var listBoxColor = null, lightModeListColor = "#fff", darkModeListColor = "#242424";
  41.  
  42. //Data (Such as numbers) color settings
  43. const dataColor = "#0079d3";
  44.  
  45. //No response list is created if false
  46. const enableCustomResponses = true;
  47.  
  48. //No chat profile icons are added if false
  49. const chatProfileIcons = true;
  50.  
  51. const placeholderMessage = randItem([
  52. "Message...",
  53. "Look, a bird! Message...",
  54. "What have you been up to today? Message...",
  55. "Beautiful day, isn't it? Message...",
  56. "Was the weather nice? Message...",
  57. "You look good today! Message...",
  58. "What dreams did you see last night? Message...",
  59. "What did you do today? Message...",
  60. "ヽ(o`皿′o)ノ AAAAAAAaaahh, spooked you! Message...",
  61. "What did you eat today? Message...",
  62. "Have you drank enough water? Message...",
  63. "Remember to stretch! Message...",
  64. "≖‿≖ I live inside of your walls. Message...",
  65. "(✿◠‿◠) Message...",
  66. "ಠ╭╮ಠ This modmail isn't going to get a reply just by itself, get back to work! Message..."
  67. ]);
  68.  
  69. /*
  70. Responses - Edit to your own liking, remove whatever you don't like!
  71.  
  72. name | The name of the response that will show on the listbox. (Example value: "Hello!")
  73. replace | Replace all messagebox text if true, otherwise just add. (Example value: true)
  74. subreddit | Visible only while on this subreddit's modmail. (Example value: "r/subreddit")
  75. content | This text will be added to the messagebox once selected (Example value: "Hello world!")
  76. */
  77. const responses = [
  78. {
  79. "name":"Select a template",
  80. "replace":true,
  81. "subreddit":"",
  82. "content":``
  83. },
  84. {
  85. "name":"Default Approved",
  86. "replace":true,
  87. "subreddit":"",
  88. "content":`Hey, approved the post!`
  89. },
  90. {
  91. "name":"Default Rule Broken",
  92. "replace":true,
  93. "subreddit":"",
  94. "content":`Your post broke our [rules](${rules}).\n\nThe action will not be reverted.`
  95. },
  96. {
  97. "name":"Add Greetings",
  98. "replace":false,
  99. "subreddit":"",
  100. "content":`${randItem(["Greetings","Hello","Hi"])} ${userTag},\n\n`
  101. },
  102. {
  103. "name":"Add Thanks",
  104. "replace":false,
  105. "subreddit":"",
  106. "content":`\n\nThank you!`
  107. },
  108. {
  109. "name":"Add Subreddit Mention",
  110. "replace":false,
  111. "subreddit":"",
  112. "content":`${subTag}`
  113. },
  114. {
  115. "name":"Add User Mention",
  116. "replace":false,
  117. "subreddit":"",
  118. "content":`${userTag}`
  119. },
  120. {
  121. "name":"Add Modmail Link",
  122. "replace":false,
  123. "subreddit":"",
  124. "content":`${modmail}`
  125. },
  126. {
  127. "name":"Add Karma Link",
  128. "replace":false,
  129. "subreddit":"",
  130. "content":`[karma](https://reddit.zendesk.com/hc/en-us/articles/204511829-What-is-karma-)`
  131. },
  132. {
  133. "name":"Add Content Policy",
  134. "replace":false,
  135. "subreddit":"",
  136. "content":`[Content Policy](https://www.redditinc.com/policies/content-policy)`
  137. },
  138. {
  139. "name":"Add User Agreement",
  140. "replace":false,
  141. "subreddit":"",
  142. "content":`[User Agreement](https://www.redditinc.com/policies/user-agreement)`
  143. },
  144. {
  145. "name":"Add Rickroll",
  146. "replace":false,
  147. "subreddit":"",
  148. "content":`[link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)`
  149. }
  150. ];
  151.  
  152. /* ---------- JS & HTML ---------- */
  153. function time(UNIX_timestamp){
  154. //Get UNIX time
  155. var d = new Date(UNIX_timestamp * 1000);
  156. const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  157.  
  158. //Get year, month, date, hour, min & sec variables
  159. var year = d.getFullYear(),
  160. monthNum = d.getMonth() + 1,
  161. month = months[d.getMonth()],
  162. date = d.getDate(),
  163. hour = fixnumber(d.getHours()),
  164. min = fixnumber(d.getMinutes()),
  165. sec = fixnumber(d.getSeconds());
  166.  
  167. //Construct the time (DD/MM/YY HH/MM/SS) and return it
  168. var time = `${date}.${monthNum}.${year} ${hour}:${min}:${sec}`;
  169. return time;
  170. }
  171. //Adds a zero suffix if x < 10
  172. const fixnumber = number => number < 10 ? "0" + number : number;
  173.  
  174. //Removes the Reddit prefix
  175. const removePrefix = (username) => ["r/","u/"].some(tag => username.includes(tag)) ? username.slice(2) : username;
  176.  
  177. //Adds the Reddit prefix if nonexistant
  178. const keepPrefix = (username, subreddit) => ["r/","u/"].some(tag => username.includes(tag)) ? username : subreddit ? `r/${username}` : `u/${username}`;
  179.  
  180. //Function to avoid XSS
  181. function sanitize(evilstring) {
  182. const decoder = document.createElement('div')
  183. decoder.innerHTML = evilstring;
  184. return decoder.textContent;
  185. }
  186.  
  187. //Appends the info (main, karma, links) to the page
  188. function addInfo(){
  189. //Load and parse username
  190. var username = removePrefix($(".InfoBar__username").innerText);
  191. var about = `https://www.reddit.com/user/${username}/about.json`;
  192. const xhr = new XMLHttpRequest();
  193. //Once the user info JSON has been fetched
  194. xhr.onload = () => {
  195. var user = JSON.parse(xhr.responseText);
  196. //Separator HTML element
  197. var seperator = document.createElement('div');
  198. seperator.innerHTML = '<div class="InfoBar__modActions"></div>';
  199. //HTML element that contains all the data
  200. var userDetails = document.createElement('div');
  201. userDetails.classList.add("InfoBar__age");
  202. userDetails.innerHTML = `<img class="profileIcon" src="${user.data.icon_img}" width="25">
  203. <a class="InfoBar__username" href="https://www.reddit.com/user/${user.data.name}">${user.data.subreddit.display_name_prefixed}</a>
  204. <h1 style="color: ${textColor} ; font-size: 11px; margin-top: 17px; margin-bottom: 10px;">${sanitize(user.data.subreddit.public_description)}</h1>
  205. <h1 class="dataTitle">Main</h1>
  206. <div class="dataText">
  207. <p>Created: <span class="value">${time(user.data.created)}</span></p>
  208. <p>UserID: <span class="value">${user.data.id}</span></p>
  209. <p>Verified: <span class="value">${user.data.verified}</span></p>
  210. <p>Employee: <span class="value">${user.data.is_employee}</span></p>
  211. <p>NSFW Profile: <span class="value">${user.data.subreddit.over_18}</span></p>
  212. </div>
  213. <h1 class="dataTitle">Karma</h1>
  214. <div class="dataText">
  215. <p>Post: <span class="value">${user.data.link_karma}</span></p>
  216. <p>Comment: <span class="value">${user.data.comment_karma}</span></p>
  217. <p>Total: <span class="value">${user.data.total_karma}</span></p>
  218. <p>Awardee: <span class="value">${user.data.awardee_karma}</span></p>
  219. <p>Awarder: <span class="value">${user.data.awarder_karma}</span></p>
  220. </div>
  221. <h1 class="dataTitle">Links</h1>
  222. <div style="padding-left: 10px;">
  223. <a class="InfoBar__recent" href="https://redditmetis.com/user/${user.data.name}" target="_blank">Redditmetis</a>
  224. <a class="InfoBar__recent" href="https://www.reddit.com/search?q=${user.data.name}" target="_blank">Reddit Search</a>
  225. <a class="InfoBar__recent" href="https://www.google.com/search?q=%22${user.data.name}%22" target="_blank">Google Search</a>
  226. </div>`;
  227.  
  228. //Add profile pictures
  229. if(chatProfileIcons) {
  230. //Icon element
  231. var chatProfileIcon = document.createElement('div');
  232. chatProfileIcon.innerHTML = `<img class="chatProfileIcon" src="${user.data.icon_img}" width="25">`;
  233.  
  234. //Loop trough every username on chat
  235. for(var i = 0; i < $$(".ThreadPreview__author").length; i++) {
  236. //Get username (u/xxxxxx)
  237. let name = $$(".Author__text")[i].innerText;
  238. //Check if there is an icon appended already
  239. let exists = $$(".ThreadPreview__author")[i].childNodes.length == 1 ? false : true;
  240. //If the username is the user (non-mod)
  241. if(removePrefix(name) == username && !exists) {
  242. //Append the icon next to the username -> [icon] u/username
  243. $$(".ThreadPreview__author")[i].insertBefore(chatProfileIcon.cloneNode(true), $$(".ThreadPreview__author")[i].firstChild);
  244. }
  245. }
  246. }
  247.  
  248. //Append the elements
  249. $(".ThreadViewer__infobar").appendChild(seperator);
  250. $(".ThreadViewer__infobar").appendChild(seperator);
  251. $(".ThreadViewer__infobar").appendChild(userDetails);
  252. $(".ThreadViewer__infobar").appendChild($(".ThreadViewer__infobar").firstChild);
  253. $(".InfoBar").appendChild($(".InfoBar__modActions"));
  254. $(".InfoBar").insertBefore($(".InfoBar__modActions"),$(".InfoBar").firstChild);
  255. if($(".InfoBar__banText"))
  256. $(".ThreadViewer__infobar").insertBefore($(".InfoBar__banText"),$(".ThreadViewer__infobar").firstChild);
  257. //Remove certain elements
  258. $$(".InfoBar__username")[1].outerHTML = "";
  259. $$(".InfoBar__age")[1].outerHTML = "";
  260. $$(".InfoBar__modActions")[1].outerHTML = "";
  261. };
  262. //Get user details
  263. xhr.open('GET', about);
  264. xhr.send();
  265. }
  266. //Appends the response template listbox to the page
  267. function addResponseBox() {
  268. //Hide real textarea and append a new one (so the text won't get removed by the sync feature)
  269. $(".ThreadViewerReplyForm__replyText").style.cssText += 'display: none';
  270. const txtArea = document.createElement("textarea");
  271. txtArea.setAttribute('class', 'Textarea ThreadViewerReplyForm__replyText ');
  272. txtArea.setAttribute('name', 'body');
  273. txtArea.setAttribute('placeholder', `${placeholderMessage}`);
  274. $(".ThreadViewerReplyForm").insertBefore(txtArea,$(".ThreadViewerReplyForm__replyFooter"));
  275. //Fix clear textarea - will not clear it if the moderator stays to send another message
  276. var replyButton = document.getElementsByClassName("Button ThreadViewerReplyForm__replyButton m-internal ")[0];
  277. const clearBoxJS = `setTimeout(function(){document.getElementsByClassName("Textarea ThreadViewerReplyForm__replyText")[1].value = ""; console.log("[Modmail++] Cleared the textarea!");},500)`;
  278. replyButton.setAttribute("onclick", clearBoxJS);
  279.  
  280. //Listbox element
  281. var responseBox = document.createElement('div');
  282. responseBox.classList.add("select");
  283. responseBox.innerHTML = `<h2 class="dataTitle">Response Templates</h2>
  284. <select id="responseListbox" onchange="listBoxChanged(this.value);" onfocus="this.selectedIndex = -1;"/>
  285. <span class="focus"></span>`;
  286. //Script element to head
  287. var headJS = document.createElement('script');
  288. headJS.innerHTML = `function listBoxChanged(message) {
  289. var messageBox = document.getElementsByClassName("Textarea ThreadViewerReplyForm__replyText")[1];
  290. var responses = ${JSON.stringify(responses)};
  291. var response = responses.find(x => x.content == message);
  292. response.replace ? messageBox.value = message : messageBox.value += message;
  293. console.log("[Modmail++] New messageBox value: %c" + messageBox.value,"color: orange");
  294. }`;
  295. //Add all the responses to the listbox
  296. function populate() {
  297. var select = $("#responseListbox");
  298. for(var i = 0; i < responses.length; i++) {
  299. //Sorry if it looks a bit complicated
  300. if(keepPrefix(responses[i].subreddit.toLowerCase(), true) == keepPrefix(subTag.toLowerCase(), true) || responses[i].subreddit == "")
  301. select.options[select.options.length] = new Option(responses[i].name, responses[i].content);
  302. }
  303. }
  304. $(".ThreadViewer__replyContainer").prepend(responseBox);
  305. var head = $("head");
  306. head.appendChild(headJS);
  307.  
  308. $(".ThreadViewer__replyContainer").insertBefore($(".ThreadViewer__typingIndicator"),$(".select"));
  309.  
  310. populate();
  311. }
  312.  
  313. //Detects the current theme (dark/light) and applies the correct color (for the added elements)
  314. function themeColors() {
  315. var darkTheme = $$(".theme-dark").length ? true : false;
  316. if(darkTheme) {
  317. console.log("[Modmail++] Dark mode detected! Setting colors...");
  318. textColor = darkModeTextColor;
  319. titleColor = darkModeTitleColor;
  320. listBoxColor = darkModeListColor;
  321. } else {
  322. console.log("[Modmail++] Light mode detected! Setting colors...");
  323. textColor = lightModeTextColor;
  324. titleColor = lightModeTitleColor;
  325. listBoxColor = lightModeListColor;
  326. }
  327. }
  328.  
  329. themeColors();
  330.  
  331. //Took advice for the listbox CSS from moderncss.dev/custom-select-styles-with-pure-css, thanks!
  332. var css = `.profileIcon:hover {
  333. -ms-transform: scale(6);
  334. -webkit-transform: scale(6);
  335. transform: scale(6);
  336. }
  337. .profileIcon {
  338. position: relative;
  339. bottom: 4px;
  340. margin-bottom: 10px;
  341. float: left; border-radius: 50%;
  342. transition: transform .1s;
  343. }
  344. .InfoBar__recentsNone {
  345. color: #6e6e6e;
  346. }
  347. .InfoBar__metadata, .InfoBar__recents {
  348. margin: 6px 0;
  349. margin-left: 10px;
  350. }
  351. .value {
  352. color: ${dataColor};
  353. }
  354. .InfoBar__banText {
  355. padding-bottom: 15px;
  356. }
  357. .InfoBar__username, .InfoBar__username:visited {
  358. padding-left: 10px;
  359. }
  360. .ThreadViewer__infobarContainer {
  361. display: table;
  362. }
  363. .dataText {
  364. color: ${textColor};
  365. font-size: 13px;
  366. padding-left: 10px;
  367. }
  368. .dataTitle {
  369. color: ${titleColor};
  370. font-size: 15px;
  371. margin-bottom: 3px;
  372. margin-top: 5px;
  373. }
  374. .responseListbox {
  375. width: 50%;
  376. cursor: pointer;
  377. }
  378. :root {
  379. --select-border: #0079d3;
  380. --select-focus: blue;
  381. --select-arrow: var(--select-border);
  382. }
  383. *,
  384. *::before,
  385. *::after {
  386. box-sizing: border-box;
  387. }
  388. select {
  389. appearance: none;
  390. background-color: ${listBoxColor};
  391. color: ${textColor};
  392. border: none;
  393. padding: 0 1em 0 0;
  394. margin: 0;
  395. width: 100%;
  396. cursor: pointer;
  397. font-family: inherit;
  398. font-size: inherit;
  399. line-height: inherit;
  400. outline: none;
  401. position: relative;
  402. }
  403. .select {
  404. width: 100%;
  405. min-width: 15ch;
  406. max-width: 30ch;
  407. border: 1px solid var(--select-border);
  408. border-radius: 0.25em;
  409. padding: 0.3em 0.4em;
  410. font-size: 0.9rem;
  411. line-height: 1.1;
  412. background-color: ${listBoxColor};
  413. margin-bottom: 15px;
  414. }
  415. select::-ms-expand {
  416. display: none;
  417. }
  418. option {
  419. white-space: normal;
  420. outline-color: var(--select-focus);
  421. }
  422. select:focus + .focus {
  423. position: absolute;
  424. top: -1px;
  425. left: -1px;
  426. right: -1px;
  427. bottom: -1px;
  428. border: 2px solid var(--select-focus);
  429. border-radius: inherit;
  430. }
  431. .Author__text {
  432. padding: 6px 0;
  433. }
  434. .chatProfileIcon {
  435. margin-right: 7px;
  436. transition: transform .1s;
  437. border-radius: 50%;
  438. }
  439. .App__page {
  440. background: var(--color-tone-8);
  441. }
  442. ::-webkit-scrollbar {
  443. width: 10px;
  444. }
  445. ::-webkit-scrollbar-track {
  446. background: ${listBoxColor};
  447. }
  448. ::-webkit-scrollbar-thumb {
  449. background: #888;
  450. }
  451. ::-webkit-scrollbar-thumb:hover {
  452. background: #555;
  453. }`;
  454.  
  455. //Apply the custom css
  456. var styleSheet = document.createElement("style");
  457. styleSheet.type = "text/css";
  458. styleSheet.innerText = css;
  459. document.head.appendChild(styleSheet);
  460.  
  461. addInfo();
  462. if(enableCustomResponses && $("#responseListbox") == null) addResponseBox();
  463. console.log("[Modmail++] %cLoaded!", "color: lime");
  464.  
  465. } /* End of Main function */
  466.  
  467. /* Start Main function when visiting new modmail */
  468. var pageURLCheckTimer = setInterval (function () {
  469. if (this.lastPathStr !== location.pathname)
  470. {
  471. this.lastPathStr = location.pathname;
  472.  
  473. first = true;
  474.  
  475. let startInterval = setInterval (function () {
  476. if($(".InfoBar__username")) {
  477. if(first) main();
  478. first = false;
  479. clearInterval(startInterval);
  480. }
  481. }, 5);
  482. }
  483. }, 100);