Ask for Permission

Helps you ask for permission to repost something from Fur Affinity

  1. // ==UserScript==
  2. // @name Ask for Permission
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.1
  5. // @description Helps you ask for permission to repost something from Fur Affinity
  6. // @author Mantikor
  7. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
  8. // @match https://www.furaffinity.net/*
  9. // @icon https://www.google.com/s2/favicons?domain=tampermonkey.net
  10. // @grant GM.addValueChangeListener
  11. // @grant GM.deleteValue
  12. // @grant GM.getValue
  13. // @grant GM.log
  14. // @grant GM.notification
  15. // @grant GM.openInTab
  16. // @grant GM.registerMenuCommand
  17. // @grant GM.setValue
  18. // ==/UserScript==
  19.  
  20. // Notes about the header
  21. // The match pattern https://www.furaffinity.net/* matches all pages on Fur Affinity
  22.  
  23. (async function() {
  24. 'use strict';
  25.  
  26. // --------------------------------------------------------------------------------
  27. // Notifications: Error, Success
  28. // --------------------------------------------------------------------------------
  29. const error_extracting_name_post = "Couldn't extract Post Title.";
  30. const error_extracting_name_recipient = "Couldn't extract Recipient Name.";
  31. const error_extracting_name_sender = "Couldn't extract Sender Name.\nYou are probably not logged in to Fur Affinity.";
  32. const error_extracting_url_newpm = "Couldn't extract Note URL.";
  33. const error_finding_message = "Couldn't find Message field.";
  34. const error_finding_subject = "Couldn't find Subject field.";
  35. const error_table_name_recipient_empty = "Recipient Name in row <row> is empty";
  36. const error_table_url_newpm_exists = "Table row <row> already contains Note URL:\n<url_newpm>";
  37. const error_table_url_newpm_invalid = "Note URL in row <row> is not valid:\n<url_newpm>";
  38. const success_settings_reset = "Settings reset.";
  39. const success_settings_saved = "Settings saved.";
  40. const success_user_information_copied = "User information copied to 'Ask User and Others' tab.";
  41.  
  42. // --------------------------------------------------------------------------------
  43. // FA: newpm
  44. // --------------------------------------------------------------------------------
  45. // Extract the "message" textarea from a newpm page
  46. const newpm_extract_message = {css: "textarea[name='message']", func: e => e, min: 1, max: 1, index: 0, re: null};
  47. // Extract the "subject" input from a newpm page
  48. const newpm_extract_subject = {css: "input[name='subject']", func: e => e, min: 1, max: 1, index: 0, re: null};
  49. // Extract the system message
  50. // User Fender has declined to participate in the note system. You may attempt to find alternate means of contact listed on their userpage or in one of their journals.
  51. const newpm_extract_system_message = {css: "div.redirect-message", func: e => e, min: 1, max: 1, index: 0, re: null};
  52. // Check if we're on a newpm page; the trailing [^$]+ matches the query string (must be present to trigger AfP)
  53. // https://www.furaffinity.net/newpm/fender/
  54. const newpm_regexp = /^https:\/\/www\.furaffinity\.net\/newpm\/[^\/]+\/[^$]+$/;
  55.  
  56. // --------------------------------------------------------------------------------
  57. // FA: user
  58. // --------------------------------------------------------------------------------
  59. // Extract name_recipient from a user page
  60. // <meta property="og:title" content="Userpage of Fender -- Fur Affinity [dot] net" />
  61. const user_extract_name_recipient = {css: "meta[property='og:title']", func: e => e.content, min: 1, max: 1, index: 0, re: /^Userpage of (?<value>[^$]+) -- Fur Affinity \[dot\] net$/};
  62. // Extract url_newpm from a user page
  63. // <a class="button usernav-watch-flex-button hideondesktop" href="/newpm/fender/">Note</a>
  64. const user_extract_url_newpm = {css: "a[href*='newpm']", func: e => e.href, min: 1, max: 1, index: 0, re: null};
  65. // Check if we're on a user page; the trailing slash isn't required, because some links to user pages omit it
  66. // https://www.furaffinity.net/user/fender/
  67. const user_regexp_url = /^https:\/\/www\.furaffinity.net\/user\/[^$]+$/;
  68.  
  69. // --------------------------------------------------------------------------------
  70. // FA: view
  71. // --------------------------------------------------------------------------------
  72. // Extract name_sender from a view page
  73. // <a href="/user/mantikor"><img class="loggedin_user_avatar menubar-icon-resize avatar" style="cursor:pointer" alt="Mantikor" src="//a.furaffinity.net/9999999999/mantikor.gif"/></a>
  74. const view_extract_name_sender = {css: "img.loggedin_user_avatar", func: e => e.alt, min: 2, max: 2, index: 0, re: null};
  75. // Extract name_post from a view page
  76. // <meta property="og:title" content="Fender (Character Sheet) by Fender" />
  77. const view_extract_name_post = {css: "meta[property='og:title']", func: e => e.content, min: 1, max: 1, index: 0, re: /^(?<value>[^$]+) by [^$]+$/};
  78. // Extract name_recipient from a view page
  79. // <meta property="og:title" content="Fender (Character Sheet) by Fender" />
  80. const view_extract_name_recipient = {css: "meta[property='og:title']", func: e => e.content, min: 1, max: 1, index: 0, re: /^[^$]+ by (?<value>[^$]+)$/};
  81. // Extract url_newpm from a view page
  82. // <div class="note"><a href="/newpm/fender/">Note</a></div>
  83. const view_extract_url_newpm = {css: "a[href*='newpm']", func: e => e.href, min: 2, max: 2, index: 0, re: null};
  84. // Check if we're on a view page; the trailing slash isn't required, because some links to view pages omit it; the trailing [^$]* matches optional URL fragments (i.e. links to comments)
  85. // https://www.furaffinity.net/view/4483888/
  86. const view_regexp_url = /^(?<url>https:\/\/www\.furaffinity\.net\/view\/\d+)[^$]*$/;
  87.  
  88. // --------------------------------------------------------------------------------
  89. // AfP: Ask User and Others
  90. // --------------------------------------------------------------------------------
  91. // Used to check if we're on the "Ask User and Others" page => Replace the 404 page; the trailing [^$]+ matches the query string (must be present to trigger AfP)
  92. // AfP uses a regexp literal instead of a constructed regexp, because the latter doesn't escape periods in the URL:
  93. // const regexp_ask_user_and_others = new RegExp(url_ask_user_and_others + "[^$]+");
  94. const ask_user_and_others_regexp_url = /^https:\/\/www\.furaffinity\.net\/userscripts\/ask-for-permission\/ask-user-and-others\/[^$]+$/;
  95. // Opened when the user clicks "Ask for Permission -> Ask User and Others"
  96. const ask_user_and_others_url = "https://www.furaffinity.net/userscripts/ask-for-permission/ask-user-and-others/";
  97.  
  98. // --------------------------------------------------------------------------------
  99. // AfP: Settings
  100. // --------------------------------------------------------------------------------
  101. // Opened when the user clicks "Ask for Permission -> Settings"
  102. // Also: Used to check if we're on the "Settings" page => Replace the 404 page
  103. const settings_url = "https://www.furaffinity.net/userscripts/ask-for-permission/config/";
  104.  
  105. // --------------------------------------------------------------------------------
  106. // Other constants
  107. // --------------------------------------------------------------------------------
  108. // AfP passes name_userscript in all query strings, so other userscripts that use query strings don't trigger AfP
  109. const name_userscript = "Ask for Permission";
  110. // Separates the last and second-to-last other recipient in message_ask_user_and_others
  111. const separator_and = " and ";
  112. // Separates the other recipients (except the last one) in message_ask_user_and_others
  113. const separator_comma = ", ";
  114.  
  115. const template_settings = `
  116. <!DOCTYPE html>
  117. <html>
  118. <head>
  119. <title>Ask for Permission: Settings</title>
  120. </head>
  121. <body>
  122. <h1>Ask for Permission: Settings</h1>
  123. <p>
  124. <label for="subject">Subject</label>
  125. <br>
  126. <input type="text" id="subject" size="75">
  127. </textarea>
  128. </p>
  129. <p>
  130. <label for="message_ask_user_only">Message (Ask User Only)</label>
  131. <br>
  132. <textarea id="message_ask_user_only" rows=10 cols=75>
  133. </textarea>
  134. </p>
  135. <p>
  136. <label for="message_ask_user_and_others">Message (Ask User and Others)</label>
  137. <br>
  138. <textarea id="message_ask_user_and_others" rows=10 cols=75>
  139. </textarea>
  140. </p>
  141. <p>
  142. <button id="save_settings_button">Save Settings</button>
  143. </p>
  144. <p>
  145. <button id="reset_settings_button" disabled>Reset Settings</button>
  146. <input type="checkbox" id="reset_settings_checkbox">
  147. <label for="reset_settings_checkbox">Enable the "Reset Settings" button</label>
  148. </p>
  149. </body>
  150. </html>`
  151. .trim()
  152.  
  153. const template_ask_user_and_others = `
  154. <!DOCTYPE html>
  155. <html>
  156. <head>
  157. <title>Ask for Permission: Ask User and Others</title>
  158. </head>
  159. <body>
  160. <h1>Ask for Permission: Ask User and Others</h1>
  161. <table id="name_table">
  162. <thead>
  163. <tr>
  164. <th scope="col">Note URL</th>
  165. <th scope="col">Recipient Name</th>
  166. <th scope="col"></th>
  167. <th scope="col"></th>
  168. </tr>
  169. </thead>
  170. <tbody>
  171. </tbody>
  172. </table>
  173. <p>
  174. <button id="create_messages_button">Create Messages</button>
  175. </p>
  176. <template id="table_row">
  177. <tr>
  178. <td>
  179. <input type="text" name="url_newpm" size="50">
  180. </td>
  181. <td>
  182. <input type="text" name="name_recipient" size="50">
  183. </td>
  184. <td>
  185. <button name="row_add">Add Row</button>
  186. </td>
  187. <td>
  188. <button name="row_delete">Delete Row</button>
  189. </td>
  190. </tr>
  191. </template>
  192. </body>
  193. </html>
  194. `.trim()
  195.  
  196. const template_subject = "Uploading \"<name_post>\" to e621?";
  197.  
  198. const template_message_ask_user_only = `
  199. Hi <name_recipient>,
  200.  
  201. may I upload <url_post> to https://e621.net/posts ?
  202.  
  203. --
  204. <name_sender>
  205. `.trim();
  206.  
  207. const template_message_ask_user_and_others = `
  208. Hi <name_recipient>,
  209.  
  210. may I upload <url_post> to https://e621.net/posts ?
  211. I'll also ask <names_recipients_other>.
  212.  
  213. --
  214. <name_sender>
  215. `.trim();
  216.  
  217. function capitalizeFirstLetter(text) {
  218. return text.substr(0, 1).toUpperCase() + text.substr(1);
  219. }
  220.  
  221. function createElement(tag_name, attributes, text_content) {
  222. let element = document.createElement(tag_name);
  223.  
  224. for (const key in attributes) {
  225. element.setAttribute(key, attributes[key]);
  226. }
  227.  
  228. if (text_content) {
  229. element.appendChild(document.createTextNode(text_content));
  230. }
  231.  
  232. return element;
  233. }
  234.  
  235. function extract(doc, info) {
  236. let elements = doc.querySelectorAll(info.css);
  237.  
  238. if ((elements.length < info.min) || (elements.length > info.max)) {
  239. return null;
  240. }
  241.  
  242. let text = info.func(elements[info.index]);
  243. if (! info.re) {
  244. return text;
  245. }
  246. if (! info.re.test(text)) {
  247. return null;
  248. }
  249.  
  250. return text.match(info.re).groups.value;
  251. }
  252.  
  253. function queryString(url, data) {
  254. let query_string = new URL(url);
  255.  
  256. for (const [key, value] of Object.entries(data)) {
  257. query_string.searchParams.set(key, value);
  258. }
  259.  
  260. return query_string.href;
  261. }
  262.  
  263. function replaceDocument(doc, html) {
  264. let newDoc = new DOMParser().parseFromString(html, "text/html");
  265. ["head", "body"].forEach(element_name => {
  266. doc[element_name].remove();
  267. doc.documentElement.append(newDoc[element_name]);
  268. })
  269. }
  270.  
  271. // Ask User and Others:
  272. // Add a new row to the table
  273. // Returns true if the new row was successfully added to the table, false otherwise
  274. function tableAddRow(table, index, url_newpm, name_recipient, row_delete) {
  275. let tbody = table.tBodies[0];
  276.  
  277. if (url_newpm) {
  278. // Check if there's already a row containing url_newpm
  279. for (const row of tbody.rows) {
  280. let inputs = row.querySelectorAll("input");
  281. if (inputs[0].value == url_newpm) {
  282. GM.notification({text: error_table_url_newpm_exists.replaceAll("<row>", row.rowIndex).replaceAll("<url_newpm>", url_newpm)});
  283. return false;
  284. }
  285. }
  286. }
  287.  
  288. // Create the new row
  289. let table_row = document.getElementById("table_row").content.firstElementChild.cloneNode(true);
  290. let inputs = table_row.querySelectorAll("input");
  291. inputs[0].value = url_newpm ? url_newpm : "";
  292. inputs[1].value = name_recipient ? name_recipient: "";
  293. if (! row_delete) {
  294. // Remove the "Delete Row" button
  295. let buttons = table_row.querySelectorAll("button");
  296. buttons[1].remove();
  297. }
  298. tbody.insertBefore(table_row, tbody.rows[index]);
  299.  
  300. return true;
  301. }
  302.  
  303. function ask(ask_others) {
  304. let name_post;
  305. let name_recipient;
  306. let name_sender;
  307. let url_newpm;
  308. let url_post;
  309. let url_tab;
  310.  
  311. let elements;
  312.  
  313. // url_post
  314. url_post = document.URL.match(view_regexp_url).groups.url;
  315.  
  316. // name_sender
  317. name_sender = extract(document, view_extract_name_sender);
  318. if (! name_sender) {
  319. GM.notification({text: error_extracting_name_sender});
  320. return;
  321. }
  322. name_sender = capitalizeFirstLetter(name_sender);
  323.  
  324. // name_post
  325. name_post = extract(document, view_extract_name_post);
  326. if (! name_post) {
  327. GM.notification({text: error_extracting_name_post});
  328. return;
  329. }
  330. // name_recipient
  331. name_recipient = extract(document, view_extract_name_recipient);
  332. if (! name_recipient) {
  333. GM.notification({text: error_extracting_name_recipient});
  334. return;
  335. }
  336. name_recipient = capitalizeFirstLetter(name_recipient);
  337.  
  338. // url_newpm
  339. url_newpm = extract(document, view_extract_url_newpm);
  340. if (! url_newpm) {
  341. GM.notification({text: error_extracting_url_newpm});
  342. return;
  343. }
  344.  
  345. if (ask_others) {
  346. url_tab = queryString(ask_user_and_others_url, {name_userscript: name_userscript, name_post: name_post, name_recipient: name_recipient, url_post: url_post, url_newpm: url_newpm, name_sender: name_sender});
  347. }
  348. else {
  349. url_tab = queryString(url_newpm, {name_userscript: name_userscript, name_post: name_post, name_recipient: name_recipient, url_post: url_post, names_recipients_other: "", name_sender: name_sender});
  350. }
  351.  
  352. GM.openInTab(url_tab, {active: true, insert: true, setParent: true});
  353. }
  354.  
  355. function askUserOnly() {
  356. ask(false);
  357. }
  358.  
  359. function askUserAndOthers() {
  360. ask(true);
  361. }
  362.  
  363. async function copyUserInformation() {
  364. let name_recipient;
  365. let url_newpm;
  366. let elements;
  367.  
  368. // Extract name_recipient
  369. name_recipient = extract(document, user_extract_name_recipient);
  370. if (! name_recipient) {
  371. GM.notification({text: error_extracting_name_recipient});
  372. return;
  373. }
  374. name_recipient = capitalizeFirstLetter(name_recipient);
  375.  
  376. // Extract url_newpm
  377. url_newpm = extract(document, user_extract_url_newpm);
  378. if (! url_newpm) {
  379. GM.notification({text: error_extracting_url_newpm});
  380. return;
  381. }
  382.  
  383. // Send url_newpm and name_recipient to the "Ask User and Others" page
  384. //await GM.deleteValue("data_ask_user_and_others");
  385. await GM.setValue("data_ask_user_and_others", {url_newpm: url_newpm, name_recipient: name_recipient});
  386.  
  387. // Close active tab:
  388. // Not possible with an XMonkey userscript
  389. }
  390.  
  391. async function fillSettingsFields() {
  392. let [subject, message_ask_user_only, message_ask_user_and_others] = await Promise.all([
  393. GM.getValue("subject", template_subject),
  394. GM.getValue("message_ask_user_only", template_message_ask_user_only),
  395. GM.getValue("message_ask_user_and_others", template_message_ask_user_and_others)
  396. ]);
  397. document.getElementById("subject").value = subject;
  398. document.getElementById("message_ask_user_only").value = message_ask_user_only;
  399. document.getElementById("message_ask_user_and_others").value = message_ask_user_and_others;
  400. }
  401.  
  402. function settings() {
  403. GM.openInTab(settings_url, {active: true, insert: true, setParent: true});
  404. }
  405.  
  406. // --------------------------------------------------------------------------------
  407. // Settings
  408. // --------------------------------------------------------------------------------
  409. if (document.URL == settings_url) {
  410. // Replace the 404 page with the "Settings" page
  411. replaceDocument(document, template_settings);
  412. // Fill Subject and Messages
  413. fillSettingsFields();
  414. // Add event listeners
  415. // -- Save Settings button
  416. document.getElementById("save_settings_button").addEventListener("click", async function(event) {
  417. await Promise.all([
  418. GM.setValue("subject", document.getElementById("subject").value),
  419. GM.setValue("message_ask_user_only", document.getElementById("message_ask_user_only").value),
  420. GM.setValue("message_ask_user_and_others", document.getElementById("message_ask_user_and_others").value)
  421. ]);
  422. GM.notification({text: success_settings_saved});
  423. });
  424. // -- Reset Settings button
  425. document.getElementById("reset_settings_button").addEventListener("click", async function(event) {
  426. await Promise.all(["subject", "message_ask_user_only", "message_ask_user_and_others"].map(key => GM.deleteValue(key)));
  427. fillSettingsFields();
  428. GM.notification({text: success_settings_reset});
  429.  
  430. document.getElementById("reset_settings_checkbox").checked = false;
  431. document.getElementById("reset_settings_button").disabled = true;
  432. });
  433. // -- Reset Settings checkbox
  434. document.getElementById("reset_settings_checkbox").addEventListener("change", async function(event) {
  435. document.getElementById("reset_settings_button").disabled = !event.target.checked;
  436. });
  437. }
  438. // --------------------------------------------------------------------------------
  439. // Ask User and Others
  440. // --------------------------------------------------------------------------------
  441. else if (ask_user_and_others_regexp_url.test(document.URL)) {
  442. GM.registerMenuCommand("Settings", settings);
  443.  
  444. // Replace the 404 page with the "Ask User and Others" page
  445. replaceDocument(document, template_ask_user_and_others);
  446.  
  447. // Extract parameters
  448. let params = new URL(document.URL).searchParams;
  449.  
  450. // Fill the first row in the table
  451. let table = document.getElementById("name_table");
  452. tableAddRow(table, 0, params.get("url_newpm"), params.get("name_recipient"), false);
  453.  
  454. // Add event listeners
  455. // -- GM Value Change
  456. GM.addValueChangeListener("data_ask_user_and_others", async function(name, old_value, new_value, remote) {
  457. if (remote) {
  458. // Add a new row at the bottom of the table
  459. let row_added = tableAddRow(table, -1, new_value.url_newpm, new_value.name_recipient, true);
  460. if (row_added) {
  461. GM.notification({text: success_user_information_copied});
  462. }
  463. await GM.deleteValue("data_ask_user_and_others");
  464. }
  465. });
  466. // -- Table
  467. table.addEventListener("click", function(event) {
  468. switch (event.target.name) {
  469. case "row_add":
  470. tableAddRow(table, event.target.parentElement.parentElement.rowIndex, null, null, true);
  471. break;
  472. case "row_delete":
  473. table.deleteRow(event.target.parentElement.parentElement.rowIndex);
  474. break;
  475. }
  476. });
  477. // -- Create Messages button
  478. document.getElementById("create_messages_button").addEventListener("click", function(event) {
  479. let recipient_info = [];
  480.  
  481. // Create a list of recipient information
  482. for (const row of table.tBodies[0].rows) {
  483. let inputs = row.querySelectorAll("input");
  484. recipient_info.push({url_newpm: inputs[0].value, name_recipient: inputs[1].value});
  485. }
  486.  
  487. // Check if every recipient information is valid
  488. for (let i = 0; i < recipient_info.length; i++) {
  489. // Check url_newpm
  490. try {
  491. let url = new URL(recipient_info[i].url_newpm);
  492. }
  493. catch (error) {
  494. GM.notification({text: error_table_url_newpm_invalid.replaceAll("<row>", i + 1).replaceAll("<url_newpm>", recipient_info[i].url_newpm)});
  495. return;
  496. }
  497. // Check name_recipient
  498. if (recipient_info[i].name_recipient == "") {
  499. GM.notification({text: error_table_name_recipient_empty.replaceAll("<row>", i + 1)});
  500. return;
  501. }
  502. }
  503.  
  504. // Create Notes
  505. recipient_info.forEach(function(item, index, array) {
  506. let names_recipients_other = recipient_info.map(x => x.name_recipient).filter((filter_item, filter_index) => filter_index != index);
  507. let url_tab;
  508.  
  509. if (names_recipients_other.length == 0) {
  510. url_tab = queryString(item.url_newpm, {name_userscript: params.get("name_userscript"), name_post: params.get("name_post"), name_recipient: item.name_recipient, url_post: params.get("url_post"), names_recipients_other: "", name_sender: params.get("name_sender")});
  511. }
  512. else {
  513. let last_element = names_recipients_other.pop();
  514. if (names_recipients_other.length == 0) {
  515. names_recipients_other = last_element;
  516. }
  517. else {
  518. names_recipients_other = names_recipients_other.join(separator_comma) + separator_and + last_element;
  519. }
  520. url_tab = queryString(item.url_newpm, {name_userscript: params.get("name_userscript"), name_post: params.get("name_post"), name_recipient: item.name_recipient, url_post: params.get("url_post"), names_recipients_other: names_recipients_other, name_sender: params.get("name_sender")});
  521. }
  522.  
  523. GM.openInTab(url_tab, {active: true, insert: true, setParent: true});
  524. });
  525. });
  526. }
  527. // --------------------------------------------------------------------------------
  528. // User
  529. // --------------------------------------------------------------------------------
  530. else if (user_regexp_url.test(document.URL)) {
  531. GM.registerMenuCommand("Copy User Information", copyUserInformation);
  532. GM.registerMenuCommand("Settings", settings);
  533. }
  534. // --------------------------------------------------------------------------------
  535. // View
  536. // --------------------------------------------------------------------------------
  537. else if (view_regexp_url.test(document.URL)) {
  538. GM.registerMenuCommand("Ask User Only", askUserOnly);
  539. GM.registerMenuCommand("Ask User and Others", askUserAndOthers);
  540. GM.registerMenuCommand("Settings", settings);
  541. }
  542. // --------------------------------------------------------------------------------
  543. // New PM
  544. // --------------------------------------------------------------------------------
  545. else if (newpm_regexp.test(document.URL)) {
  546. GM.registerMenuCommand("Settings", settings);
  547.  
  548. // Check if the user has disabled notes
  549. let system_message = extract(document, newpm_extract_system_message);
  550. if (system_message) {
  551. return;
  552. }
  553.  
  554. // Extract parameters
  555. let params = new URL(document.URL).searchParams;
  556. if (params.get("name_userscript") != name_userscript) {
  557. // AfP didn't open this newpm page
  558. return;
  559. }
  560.  
  561. // Subject
  562. let subject = await GM.getValue("subject", template_subject);
  563. subject = subject.replaceAll("<name_post>", params.get("name_post"));
  564.  
  565. let element_subject = extract(document, newpm_extract_subject);
  566. if (! element_subject) {
  567. GM.notification({text: error_finding_subject});
  568. return;
  569. }
  570. element_subject.value = subject;
  571.  
  572. // Message
  573. let message;
  574. if (params.get("names_recipients_other") == "") {
  575. message = await GM.getValue("message_ask_user_only", template_message_ask_user_only);
  576. }
  577. else {
  578. message = await GM.getValue("message_ask_user_and_others", template_message_ask_user_and_others);
  579. }
  580.  
  581. for (const element of ["name_recipient", "url_post", "names_recipients_other", "name_sender"]) {
  582. message = message.replaceAll("<" + element + ">", params.get(element));
  583. }
  584.  
  585. let element_message = extract(document, newpm_extract_message);
  586. if (! element_message) {
  587. GM.notification({text: error_finding_message});
  588. return;
  589. }
  590. element_message.value = message;
  591. }
  592. else {
  593. GM.registerMenuCommand("Settings", settings);
  594. }
  595. })();