Discord custom nicknames

Assign custom nicknames to Discord usernames client-side

  1. // ==UserScript==
  2. // @name Discord custom nicknames
  3. // @namespace https://github.com/aspiers/Discord-custom-nicks-userscript
  4. // @version 0.3.3
  5. // @description Assign custom nicknames to Discord usernames client-side
  6. // @author Adam Spiers
  7. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
  8. // @match https://discord.com/channels/*
  9. // @icon https://www.google.com/s2/favicons?domain=discord.com
  10. // @require https://code.jquery.com/jquery-3.6.0.min.js
  11. // @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
  12. // @require https://greasyfork.org/scripts/5392-waitforkeyelements/code/WaitForKeyElements.js?version=115012
  13. // @resource jQueryUI-css https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/vader/jquery-ui.min.css
  14. // @resource jQueryUI-icon1 https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/vader/images/ui-icons_666666_256x240.png
  15. // @resource jQueryUI-icon2 https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/vader/images/ui-icons_bbbbbb_256x240.png
  16. // @resource jqueryUI-icon3 https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/vader/images/ui-icons_c98000_256x240.png
  17. // @grant GM_getValue
  18. // @grant GM_setValue
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_getResourceText
  21. // @grant GM_getResourceURL
  22. // @grant GM_info
  23. // @grant GM_addStyle
  24. // @run-at document-end
  25. // ==/UserScript==
  26. //
  27. // Browser userscript to assign custom names to Discord nicknames
  28. // Copyright (C) 2021 Adam Spiers <userscripts@adamspiers.org>
  29. //
  30. // This program is free software: you can redistribute it and/or modify
  31. // it under the terms of the GNU General Public License as published by
  32. // the Free Software Foundation, either version 3 of the License, or
  33. // (at your option) any later version.
  34. //
  35. // This program is distributed in the hope that it will be useful,
  36. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  37. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  38. // GNU General Public License for more details.
  39. //
  40. // You should have received a copy of the GNU General Public License
  41. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  42.  
  43. // Stop JSHint in Tampermonkey's CodeMirror editor from complaining
  44. // about globals imported via @require:
  45. // https://jshint.com/docs/#inline-configuration
  46. /* globals jQuery waitForKeyElements */
  47.  
  48. (function() {
  49. 'use strict';
  50. let $ = jQuery;
  51. unsafeWindow.jQuery = jQuery;
  52.  
  53. // Don't replace more often than this number of milliseconds.
  54. const DEBOUNCE_MS = 2000;
  55.  
  56. const ELEMENT_PREFIX = "Discord-custom-nicknames-";
  57. const DIALOG_ID = ELEMENT_PREFIX + "dialog";
  58. const TEXTAREA_ID = ELEMENT_PREFIX + "textarea";
  59. const DIALOG_SELECTOR = "#" + DIALOG_ID;
  60. const TEXTAREA_SELECTOR = "#" + TEXTAREA_ID;
  61.  
  62. const ORIG_ATTR = "data-Discord-orig-nickname";
  63. const STORAGE = "Discord_custom_nicknames_mapping";
  64.  
  65. function get_nick_map_str() {
  66. let map_str = GM_getValue(STORAGE);
  67. return typeof(map_str) == "string" ? map_str : "";
  68. }
  69. unsafeWindow.get_nick_map_str = get_nick_map_str;
  70.  
  71. function set_nick_map_str(new_value) {
  72. GM_setValue(STORAGE, new_value);
  73. }
  74. unsafeWindow.set_nick_map_str = set_nick_map_str;
  75.  
  76. function get_nick_map() {
  77. return parse_map(get_nick_map_str());
  78. }
  79. unsafeWindow.get_nick_map = get_nick_map;
  80.  
  81. // function serialise_map(map_obj) {
  82. // return Object.entries(map_obj).map(e => e[0] + "=" + e[1]).join("\n");
  83. // }
  84.  
  85. function parse_map(map_str) {
  86. let map_obj = {};
  87. for (const pair of map_str.split("\n")) {
  88. if (pair.indexOf("=") != -1) {
  89. let [k, v] = pair.split("=");
  90. map_obj[k] = v;
  91. }
  92. }
  93. return map_obj;
  94. }
  95. window.parse_map = parse_map;
  96.  
  97. const PREFIX = "[Discord custom nicknames]";
  98.  
  99. function debug(...args) {
  100. console.debug(PREFIX, ...args);
  101. }
  102.  
  103. function log(...args) {
  104. console.log(PREFIX, ...args);
  105. }
  106.  
  107. function replace_nick(nick_map, element) {
  108. // debug("replace", element);
  109. let orig_nick = element.getAttribute(ORIG_ATTR);
  110. let Discord_nick = orig_nick || element.innerText;
  111. let at = "";
  112. if (Discord_nick.startsWith("@")) {
  113. at = "@";
  114. Discord_nick = Discord_nick.slice(1);
  115. }
  116. let mapped_name = nick_map[Discord_nick];
  117. if (mapped_name) {
  118. mapped_name = at + mapped_name;
  119. debug(`${at}${Discord_nick} -> ${mapped_name}`);
  120. if (!orig_nick && element.tagName !== "TITLE") {
  121. // Back up the original to an attribute so that we can remap later
  122. // without reloading the page.
  123. //
  124. // FIXME: Figure out a way to make this work
  125. // flawlessly for <title>. Currently it's slightly
  126. // broken because <title> can change values when
  127. // switching between DM pages, so we can't back up
  128. // the original username to an attribute on it.
  129. element.setAttribute(ORIG_ATTR, element.innerText)
  130. }
  131. element.innerText = mapped_name;
  132. }
  133. else {
  134. // debug(`no mapping found for ${element.innerText}`);
  135. // This is required in case a nick mapping is removed:
  136. if (orig_nick) {
  137. element.innerText = orig_nick;
  138. }
  139. }
  140. }
  141.  
  142. function replace_css_elements(nick_map, query) {
  143. let matches = jQuery(query);
  144. // debug(`replacing ${query}`, matches);
  145. if (matches && matches.each) {
  146. matches.each((i, elt) => replace_nick(nick_map, elt));
  147. }
  148. }
  149.  
  150. function replace_all() {
  151. debug("replace_all()");
  152. let nick_map = get_nick_map();
  153. debug("parsed:", nick_map);
  154.  
  155. for (let selector of CSS_SELECTORS) {
  156. replace_css_elements(nick_map, selector);
  157. }
  158. }
  159.  
  160. function dialog_html() {
  161. return `
  162. <div id="${DIALOG_ID}" title="Discord custom nicknames">
  163. <p>
  164. Enter your mappings here, one on each line.
  165. </p>
  166. <textarea rows="10" cols="50" id="${TEXTAREA_ID}"
  167. placeholder="nickname=Real Name"></textarea>
  168. <p>
  169. Each mapping should look something like
  170. </p>
  171. <pre><code>nickname=Firstname Lastname</code></pre>
  172. <p>
  173. where the left-hand side of the <code>=</code>
  174. sign is the normal Discord nickname (excluding
  175. the <code>#1234</code> suffix), and the
  176. right-hand side is what you want to see instead.
  177. </p>
  178. </div>
  179. `;
  180. }
  181.  
  182. function handle_dialog_save(dialog) {
  183. let map_str = $(TEXTAREA_SELECTOR).val();
  184. debug(`${TEXTAREA_SELECTOR} dialog save:`, map_str);
  185. GM_setValue(STORAGE, map_str || "");
  186. replace_all();
  187. $(dialog).dialog("close");
  188. }
  189.  
  190. function handle_dialog_open(dialog) {
  191. let orig = get_nick_map_str();
  192. debug(`restoring ${TEXTAREA_SELECTOR} to`, orig);
  193. $(TEXTAREA_SELECTOR).val(orig);
  194. }
  195.  
  196. unsafeWindow.GM_info = GM_info;
  197.  
  198. function insert_CSS() {
  199. let CSS = GM_getResourceText("jQueryUI-css");
  200. for (let resource of GM_info.script.resources) {
  201. let image = resource.url.match(/images\/.+\.png/);
  202. if (!image) {
  203. continue;
  204. }
  205. let URL = GM_getResourceURL(resource.name);
  206. let rel_path = image[0];
  207. CSS = CSS.replaceAll(
  208. `url("${rel_path}")`,
  209. `url("${URL}")`,
  210. );
  211. }
  212. GM_addStyle(CSS);
  213. }
  214.  
  215. function insert_dialog() {
  216. $("body").append(dialog_html());
  217. $(TEXTAREA_SELECTOR).val(get_nick_map_str());
  218.  
  219. $(DIALOG_SELECTOR).dialog({
  220. minWidth: 300,
  221. width: 700,
  222. maxWidth: 300,
  223. buttons: [
  224. {
  225. text: "Save",
  226. click: function() {
  227. handle_dialog_save(this);
  228. }
  229. },
  230. {
  231. text: "Cancel",
  232. click: function() {
  233. $(this).dialog("close");
  234. }
  235. }
  236. ],
  237. open: handle_dialog_open,
  238. });
  239. }
  240.  
  241. function display_dialog() {
  242. if ($(DIALOG_SELECTOR).length == 0) {
  243. insert_CSS();
  244. insert_dialog();
  245. }
  246. $(DIALOG_SELECTOR).dialog("open");
  247. }
  248.  
  249. GM_registerMenuCommand("Nickname mapping", display_dialog);
  250.  
  251. const CSS_SELECTORS = [
  252. "title",
  253.  
  254. /////////////////////////////////////////////////////////
  255. // Channel pages
  256.  
  257. // User list on right-hand side
  258. "div[class^=membersWrap] span[class^=roleColor]",
  259.  
  260. // Attributions in main chat pane
  261. "span[class^=headerText] span[class^=username]",
  262.  
  263. // Mentions within messages
  264. "div[class*=messageContent] span.mention",
  265.  
  266. // When replying, name of user we're replying to
  267. "div[class^=replyBar] span[class^=name]",
  268.  
  269. /////////////////////////////////////////////////////////
  270. // DM pages
  271.  
  272. // DM list in left bar
  273. "div#private-channels div[class^=nameAndDecorators]",
  274.  
  275. // Main friends list when "Friends" is clicked on
  276. "div[class^=peopleList] div[class^=userInfo] span[class^=username]",
  277.  
  278. // Top of individual DM page
  279. "div[class^=chat] section[class^=title] h3[class*=title]",
  280.  
  281. // h3 under individual DM large avatar
  282. "div[id^=chat-messages] h3[class^=header]"
  283.  
  284. // N.B. deliberately not replacing
  285. //
  286. // "This is the beginning of your direct message history with"
  287. //
  288. // because that's a useful place to show the mapping with
  289. // the original username.
  290. ];
  291.  
  292. function init() {
  293. let lastWaited = {};
  294. let nick_map = get_nick_map();
  295. for (let selector of CSS_SELECTORS) {
  296. waitForKeyElements(
  297. selector,
  298. () => {
  299. debug("waitForKeyElements triggered for", selector);
  300. let last = lastWaited[selector];
  301. if (!last || (new Date() - last > DEBOUNCE_MS)) {
  302. replace_css_elements(nick_map, selector);
  303. lastWaited[selector] = new Date();
  304. }
  305. }
  306. );
  307. }
  308. setInterval(replace_all, 5000);
  309. }
  310.  
  311. init();
  312. })();