[Reddit] Modmail++

Additional tools and information to Reddit's Modmail

目前為 2021-11-23 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name [Reddit] Modmail++
  3. // @namespace HKR
  4. // @match https://mod.reddit.com/mail/*
  5. // @grant none
  6. // @version 2.0
  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("[ModmailExtraInfo] %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("[ModmailExtraInfo] %cMain function ran!", "color: grey");
  23.  
  24. /* SETTINGS */
  25.  
  26. /* NOTE (If you want to use the Custom Responses): Reddit's sync feature removes the script's added text.
  27. - If you block "https://oauth.reddit.com/api/mod/conversations/*****?markRead=false&redditWebClient=modmail", the added text will stay. */
  28.  
  29. //Variables for the responses
  30. const subTag = $(".ThreadTitle__community").href.slice(23);
  31. const userTag = "u/" + $(".InfoBar__username").innerText;
  32. const modmail = `[modmail](https://www.reddit.com/message/compose?to=/${subTag})`;
  33. const rules = `https://www.reddit.com/${subTag}/about/rules`;
  34. const randItem = itemArr => itemArr[Math.floor(Math.random() * itemArr.length)];
  35.  
  36. //Text color settings
  37. var textColor = null, lightModeTextColor = "#6e6e6e", darkModeTextColor = "#757575";
  38.  
  39. //Title color settings
  40. var titleColor = null, lightModeTitleColor = "#2c2c2c", darkModeTitleColor = "#a7a7a7";
  41.  
  42. //Listbox color settings
  43. var listBoxColor = null, lightModeListColor = "#fff", darkModeListColor = "#242424";
  44.  
  45. //Data (Such as numbers) color settings
  46. const dataColor = "#0079d3";
  47.  
  48. //No response list is created if false
  49. const enableCustomResponses = true;
  50.  
  51. //No chat profile icons are added if false
  52. const chatProfileIcons = true;
  53.  
  54. //Feel free to edit and add more responses suitable for you! Replace means if to replace all text or just to add the text.
  55. const responses = [
  56. {
  57. "name":"Select a template",
  58. "replace":true,
  59. "content":``
  60. },
  61. {
  62. "name":"Default approved",
  63. "replace":true,
  64. "content":`Hey, approved the post!`
  65. },
  66. {
  67. "name":"Default rule broken",
  68. "replace":true,
  69. "content":`Your post broke our [rules](${rules}).\n\nThe action will not be reverted.`
  70. },
  71. {
  72. "name":"Add greetings",
  73. "replace":true,
  74. "content":`${randItem(["Greetings","Hello","Hi"])} ${userTag},\n\n`
  75. },
  76. {
  77. "name":"Add thanks",
  78. "replace":false,
  79. "content":`\n\nThank you!`
  80. },
  81. {
  82. "name":"Add subreddit mention",
  83. "replace":false,
  84. "content":`${subTag}`
  85. },
  86. {
  87. "name":"Add user mention",
  88. "replace":false,
  89. "content":`${userTag}`
  90. },
  91. {
  92. "name":"Add Modmail link",
  93. "replace":false,
  94. "content":`${modmail}`
  95. },
  96. {
  97. "name":"Add Content Policy",
  98. "replace":false,
  99. "content":`[Content Policy](https://www.redditinc.com/policies/content-policy)`
  100. },
  101. {
  102. "name":"Add User Agreement",
  103. "replace":false,
  104. "content":`[User Agreement](https://www.redditinc.com/policies/user-agreement)`
  105. },
  106. {
  107. "name":"Add Rickroll",
  108. "replace":false,
  109. "content":`[link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)`
  110. }
  111. ];
  112.  
  113. /* ---------- JS & HTML ---------- */
  114. function time(UNIX_timestamp){
  115. //Get UNIX time
  116. var d = new Date(UNIX_timestamp * 1000);
  117. const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  118.  
  119. //Get year, month, date, hour, min & sec variables
  120. var year = d.getFullYear(),
  121. monthNum = d.getMonth() + 1,
  122. month = months[d.getMonth()],
  123. date = d.getDate(),
  124. hour = fixnumber(d.getHours()),
  125. min = fixnumber(d.getMinutes()),
  126. sec = fixnumber(d.getSeconds());
  127.  
  128. //Construct the time (DD/MM/YY HH/MM/SS) and return it
  129. var time = `${date}.${monthNum}.${year} ${hour}:${min}:${sec}`;
  130. return time;
  131. }
  132. //Adds a zero suffix if x < 10
  133. const fixnumber = number => number < 10 ? "0" + number : number;
  134.  
  135. //Removes the u/ prefix
  136. const removePrefix = username => username.includes("u/") ? username.slice(2) : username;
  137.  
  138. //Adds the u/ prefix if nonexistant
  139. const keepPrefix = username => username.includes("u/") ? username : "u/" + username;
  140.  
  141. //Function to avoid XSS
  142. function sanitize(evilstring) {
  143. const decoder = document.createElement('div')
  144. decoder.innerHTML = evilstring;
  145. return decoder.textContent;
  146. }
  147.  
  148. //Appends the info (main, karma, links) to the page
  149. function addInfo(){
  150. //Load and parse username
  151. var username = removePrefix($(".InfoBar__username").innerText);
  152. var about = "https://www.reddit.com/user/" + username + "/about.json";
  153. const xhr = new XMLHttpRequest();
  154. //Once the user info JSON has been fetched
  155. xhr.onload = () => {
  156. var user = JSON.parse(xhr.responseText);
  157. //Separator HTML element
  158. var seperator = document.createElement('div');
  159. seperator.innerHTML = '<div class="InfoBar__modActions"></div>';
  160. //HTML element that contains all the data
  161. var userDetails = document.createElement('div');
  162. userDetails.classList.add("InfoBar__age");
  163. userDetails.innerHTML = `<img class="profileIcon" src="${user.data.icon_img}" width="25">
  164. <a class="InfoBar__username" href="https://www.reddit.com/user/${user.data.name}">${user.data.subreddit.display_name_prefixed}</a>
  165. <h1 style="color: ${textColor} ; font-size: 11px; margin-top: 17px; margin-bottom: 10px;">${sanitize(user.data.subreddit.public_description)}</h1>
  166. <h1 class="dataTitle">Main</h1>
  167. <div class="dataText">
  168. <p>Created: <span class="value">${time(user.data.created)}</span></p>
  169. <p>UserID: <span class="value">${user.data.id}</span></p>
  170. <p>Verified: <span class="value">${user.data.verified}</span></p>
  171. <p>Employee: <span class="value">${user.data.is_employee}</span></p>
  172. <p>NSFW Profile: <span class="value">${user.data.subreddit.over_18}</span></p>
  173. </div>
  174. <h1 class="dataTitle">Karma</h1>
  175. <div class="dataText">
  176. <p>Post: <span class="value">${user.data.link_karma}</span></p>
  177. <p>Comment: <span class="value">${user.data.comment_karma}</span></p>
  178. <p>Total: <span class="value">${user.data.total_karma}</span></p>
  179. <p>Awardee: <span class="value">${user.data.awardee_karma}</span></p>
  180. <p>Awarder: <span class="value">${user.data.awarder_karma}</span></p>
  181. </div>
  182. <h1 class="dataTitle">Links</h1>
  183. <div style="padding-left: 10px;">
  184. <a class="InfoBar__recent" href="https://redditmetis.com/user/${user.data.name}" target="_blank">Redditmetis</a>
  185. <a class="InfoBar__recent" href="https://www.reddit.com/search?q=${user.data.name}" target="_blank">Reddit Search</a>
  186. <a class="InfoBar__recent" href="https://www.google.com/search?q=%22${user.data.name}%22" target="_blank">Google Search</a>
  187. </div>`;
  188.  
  189. //Add profile pictures
  190. if(chatProfileIcons) {
  191. //Icon element
  192. var chatProfileIcon = document.createElement('div');
  193. chatProfileIcon.innerHTML = `<img class="chatProfileIcon" src="${user.data.icon_img}" width="25">`;
  194.  
  195. //Loop trough every username on chat
  196. for(var i = 0; i < $$(".ThreadPreview__author").length; i++) {
  197. //Get username (u/xxxxxx)
  198. let name = $$(".Author__text")[i].innerText;
  199. //Check if there is an icon appended already
  200. let exists = $$(".ThreadPreview__author")[i].childNodes.length == 1 ? false : true;
  201. //If the username is the user (non-mod)
  202. if(removePrefix(name) == username && !exists) {
  203. //Append the icon next to the username -> [icon] u/username
  204. $$(".ThreadPreview__author")[i].insertBefore(chatProfileIcon.cloneNode(true), $$(".ThreadPreview__author")[i].firstChild);
  205. }
  206. }
  207. }
  208.  
  209. //Append the elements
  210. $(".ThreadViewer__infobar").appendChild(seperator);
  211. $(".ThreadViewer__infobar").appendChild(seperator);
  212. $(".ThreadViewer__infobar").appendChild(userDetails);
  213. $(".ThreadViewer__infobar").appendChild($(".ThreadViewer__infobar").firstChild);
  214. $(".InfoBar").appendChild($(".InfoBar__modActions"));
  215. $(".InfoBar").insertBefore($(".InfoBar__modActions"),$(".InfoBar").firstChild);
  216. if($(".InfoBar__banText"))
  217. $(".ThreadViewer__infobar").insertBefore($(".InfoBar__banText"),$(".ThreadViewer__infobar").firstChild);
  218. //Remove certain elements
  219. $$(".InfoBar__username")[1].outerHTML = "";
  220. $$(".InfoBar__age")[1].outerHTML = "";
  221. $$(".InfoBar__modActions")[1].outerHTML = "";
  222. };
  223. //Get user details
  224. xhr.open('GET', about);
  225. xhr.send();
  226. }
  227. //Appends the response template listbox to the page
  228. function addResponseBox() {
  229. //Listbox element
  230. var responseBox = document.createElement('div');
  231. responseBox.classList.add("select");
  232. responseBox.innerHTML = `<h2 class="dataTitle">Response templates</h2>
  233. <select id="responseListbox" onchange="listBoxChanged(this.value);" onfocus="this.selectedIndex = -1;"/>
  234. <span class="focus"></span>`;
  235. //Script element to head
  236. var headJS = document.createElement('script');
  237. headJS.innerHTML = `function listBoxChanged(message) {
  238. var messageBox = document.getElementsByClassName("Textarea ThreadViewerReplyForm__replyText")[0];
  239. var responses = ${JSON.stringify(responses)};
  240. var response = responses.find(x => x.content == message);
  241. response.replace ? messageBox.value = message : messageBox.value += message;
  242. console.log("[ModmailExtraInfo] New messageBox value: %c" + messageBox.value,"color: orange");
  243. }`;
  244. function populate() {
  245. var select = $("#responseListbox");
  246. for(var i = 0; i < responses.length; i++) {
  247. select.options[select.options.length] = new Option(responses[i].name, responses[i].content);
  248. }
  249. }
  250. $(".ThreadViewer__replyContainer").prepend(responseBox);
  251. var head = $("head");
  252. head.appendChild(headJS);
  253. populate();
  254. }
  255.  
  256. //Detects the current theme (dark/light) and applies the correct color (for the added elements)
  257. function themeColors() {
  258. var darkTheme = $$(".theme-dark").length ? true : false;
  259. if(darkTheme) {
  260. console.log("[ModmailExtraInfo] Dark mode detected! Setting colors...");
  261. textColor = darkModeTextColor;
  262. titleColor = darkModeTitleColor;
  263. listBoxColor = darkModeListColor;
  264. } else {
  265. console.log("[ModmailExtraInfo] Light mode detected! Setting colors...");
  266. textColor = lightModeTextColor;
  267. titleColor = lightModeTitleColor;
  268. listBoxColor = lightModeListColor;
  269. }
  270. }
  271.  
  272. themeColors();
  273.  
  274. //Took advice for the listbox CSS from moderncss.dev/custom-select-styles-with-pure-css, thanks!
  275. var css = `.profileIcon:hover {
  276. -ms-transform: scale(6);
  277. -webkit-transform: scale(6);
  278. transform: scale(6);
  279. }
  280. .profileIcon {
  281. position: relative;
  282. bottom: 4px;
  283. margin-bottom: 10px;
  284. float: left; border-radius: 50%;
  285. transition: transform .1s;
  286. }
  287. .InfoBar__recentsNone {
  288. color: #6e6e6e;
  289. }
  290. .InfoBar__metadata, .InfoBar__recents {
  291. margin: 6px 0;
  292. margin-left: 10px;
  293. }
  294. .value {
  295. color: ${dataColor};
  296. }
  297. .InfoBar__banText {
  298. padding-bottom: 15px;
  299. }
  300. .InfoBar__username, .InfoBar__username:visited {
  301. padding-left: 10px;
  302. }
  303. .ThreadViewer__infobarContainer {
  304. display: table;
  305. }
  306. .dataText {
  307. color: ${textColor};
  308. font-size: 13px;
  309. padding-left: 10px;
  310. }
  311. .dataTitle {
  312. color: ${titleColor};
  313. font-size: 15px;
  314. margin-bottom: 3px;
  315. margin-top: 5px;
  316. }
  317. .responseListbox {
  318. width: 50%;
  319. cursor: pointer;
  320. }
  321. :root {
  322. --select-border: #0079d3;
  323. --select-focus: blue;
  324. --select-arrow: var(--select-border);
  325. }
  326. *,
  327. *::before,
  328. *::after {
  329. box-sizing: border-box;
  330. }
  331. select {
  332. appearance: none;
  333. background-color: ${listBoxColor};
  334. color: ${textColor};
  335. border: none;
  336. padding: 0 1em 0 0;
  337. margin: 0;
  338. width: 100%;
  339. cursor: pointer;
  340. font-family: inherit;
  341. font-size: inherit;
  342. line-height: inherit;
  343. outline: none;
  344. position: relative;
  345. }
  346. .select {
  347. width: 100%;
  348. min-width: 15ch;
  349. max-width: 30ch;
  350. border: 1px solid var(--select-border);
  351. border-radius: 0.25em;
  352. padding: 0.3em 0.4em;
  353. font-size: 0.9rem;
  354. line-height: 1.1;
  355. background-color: ${listBoxColor};
  356. }
  357. select::-ms-expand {
  358. display: none;
  359. }
  360. option {
  361. white-space: normal;
  362. outline-color: var(--select-focus);
  363. }
  364. select:focus + .focus {
  365. position: absolute;
  366. top: -1px;
  367. left: -1px;
  368. right: -1px;
  369. bottom: -1px;
  370. border: 2px solid var(--select-focus);
  371. border-radius: inherit;
  372. }
  373. .Author__text {
  374. padding: 6px 0;
  375. }
  376. .chatProfileIcon {
  377. margin-right: 7px;
  378. transition: transform .1s;
  379. border-radius: 50%;
  380. }`;
  381.  
  382. //Apply the custom css
  383. var styleSheet = document.createElement("style");
  384. styleSheet.type = "text/css";
  385. styleSheet.innerText = css;
  386. document.head.appendChild(styleSheet);
  387.  
  388. addInfo();
  389. if(enableCustomResponses && $("#responseListbox") == null) addResponseBox();
  390. console.log("[ModmailExtraInfo] %cLoaded!", "color: lime");
  391.  
  392. } /* End of Main function */
  393.  
  394. /* Start Main function when visiting new modmail */
  395. var pageURLCheckTimer = setInterval (function () {
  396. if (this.lastPathStr !== location.pathname)
  397. {
  398. this.lastPathStr = location.pathname;
  399.  
  400. first = true;
  401.  
  402. let startInterval = setInterval (function () {
  403. if($(".InfoBar__username")) {
  404. if(first) main();
  405. first = false;
  406. clearInterval(startInterval);
  407. }
  408. }, 5);
  409. }
  410. }, 100);