NoPaste

quickmod.user.js (v1.5)

von TRex
SNIPPET_DESC:
https://wiki.debianforum.de/Userscripts
SNIPPET_CREATION_TIME:
27.03.2023 17:04:09
SNIPPET_PRUNE_TIME:
Unendlich

SNIPPET_TEXT:
  1. // ==UserScript==
  2. // @name          debianforum.de-quickmod-additions
  3. // @namespace     org.free.for.all
  4. // @include       https://debianforum.de/forum/viewtopic.php*
  5. // @match         https://debianforum.de/forum/viewtopic.php*
  6. // @author        Thorsten Sperber
  7. // @author        JTH
  8. // @version       1.5
  9. // ==/UserScript==
  10.  
  11. const ARCHIVFORUMID = 35;
  12.  
  13. async function archiveThread(firstPage, reason) {
  14.     const moveProm = (async () => {
  15.         const moveLink = firstPage.querySelector("#quickmod .dropdown-contents a[href*='action=move']");
  16.  
  17.         let form, formData;
  18.         try {
  19.             [form, formData] = await openForm(toAbsoluteURL(moveLink.href), "form#confirm");
  20.         } catch (err) {
  21.             throw `Konnte Formular zum Verschieben des Themas nicht öffnen: ${err}`;
  22.         }
  23.  
  24.         formData.set("to_forum_id", ARCHIVFORUMID);
  25.         try {
  26.             /* Unlike splitting a thread, moving does not have a second
  27.              * confirmation step.
  28.              */
  29.             await postForm(form, formData, "confirm");
  30.         } catch (err) {
  31.             throw `Konnte Thema nicht verschieben: ${err}`;
  32.         }
  33.     })();
  34.  
  35.     const editProm = (async () => {
  36.         const editLink = firstPage.querySelector(".post .post-buttons a[href*='mode=edit']");
  37.  
  38.         let form, formData;
  39.         try {
  40.             [form, formData] = await openForm(toAbsoluteURL(editLink.href), "form#postform");
  41.         } catch (err) {
  42.             throw `Konnte Formular zum Bearbeiten des ersten Beitrags nicht öffnen: ${err}`;
  43.         }
  44.  
  45.         formData.set("subject", prefixSubject(form.elements["subject"], reason));
  46.  
  47.         /* All "altering actions not secured by confirm_box" require a non-zero
  48.          * time difference between opening and submitting a form. See
  49.          * check_form_key() in phpBB/includes/functions.php.
  50.          *
  51.          * So we artificially delay the postForm() for a second.
  52.          */
  53.         await new Promise((resolve) => {
  54.             setTimeout(async () => {
  55.                 try {
  56.                     await postForm(form, formData, "post");
  57.                 } catch (err) {
  58.                     throw `Konnte Thema nicht umbenennen: ${err}`;
  59.                 }
  60.  
  61.                 resolve();
  62.             }, 1001);
  63.         });
  64.     })();
  65.  
  66.     /* An mcp action and a post edit can actually be done concurrently! :-) */
  67.     await Promise.all([moveProm, editProm]);
  68. }
  69.  
  70. async function archiveThreadQuickmod() {
  71.     const canonicalURL = new URL(document.querySelector("link[rel='canonical']").href);
  72.     const firstPage = await openDoc(canonicalURL);
  73.     const firstPost = firstPage.querySelector(".post");
  74.     const usernameElem = firstPost.querySelector(".author .username,.author .username-coloured");
  75.     const username = usernameElem.textContent;
  76.     const thread_title = firstPage.querySelector('.topic-title a').text;
  77.  
  78.     const archiveReason = await asyncPrompt(`Thema „${ellipsify(thread_title, 100)}“ eröffnet von „${username}“ wirklich als Spam archivieren?\n\nGrund:`, "Spam");
  79.     if (archiveReason === null) {
  80.         /* Don't do any of the other actions if moving was cancelled. */
  81.         return;
  82.     }
  83.  
  84.     const archivingThread = archiveThread(firstPage, archiveReason);
  85.  
  86.     /* Prompting for a separate ban reason in case there is something more
  87.      * specific to note here.
  88.      */
  89.     const userStillExists = usernameElem.nodeName === "A";
  90.     const banReasonPrompt = userStillExists &&
  91.         asyncPrompt(`Benutzer „${username}, der das Thema eröffnet hat, sperren?\n\nGrund:`, "Spam");
  92.  
  93.     /* Mod actions via mcp.php involve a confirm_key which is stored in the
  94.      * database when an action is requested until it is confirmed. There can only
  95.      * be one confirm_key stored at a time---meaning there cannot be multiple mcp
  96.      * actions executed concurrently. See confirm_box() in
  97.      * phpBB/includes/functions.php.
  98.      *
  99.      * This means we cannot really execute the actions concurrently here,
  100.      * unfortunately. User interaction is still done in parallel to one action at
  101.      * a time, though.
  102.      */
  103.     const errors = [];
  104.     try {
  105.         await archivingThread;
  106.     } catch (err) {
  107.         errors.push(err);
  108.     }
  109.  
  110.     let banningUser;
  111.     const banReason = await banReasonPrompt;
  112.     if (banReason) {
  113.         banningUser = banUser(username, banReason);
  114.     } else if (!userStillExists) {
  115.         await asyncAlert(`Benutzer „${username}“ wurde schon gelöscht und kann nicht mehr gesperrt werden.`);
  116.     }
  117.  
  118.     const shouldCloseReport = isPostReported(firstPost) &&
  119.         asyncConfirm("Meldung zum ersten Beitrag schließen?");
  120.  
  121.     try {
  122.         await banningUser;
  123.     } catch (err) {
  124.         errors.push(err);
  125.     }
  126.  
  127.     if (await shouldCloseReport) {
  128.         try {
  129.             await closeReport(firstPost);
  130.         } catch (err) {
  131.             errors.push(err);
  132.         }
  133.     }
  134.  
  135.     for (const error of errors) {
  136.         console.log(error);
  137.         window.alert(`ACHTUNG!\n\n${error}`);
  138.     }
  139.  
  140.     if (errors.length === 0) {
  141.         redirectToArchive();
  142.     }
  143. }
  144.  
  145. async function asyncAlert(message) {
  146.     return showDialog(message);
  147. }
  148.  
  149. async function asyncConfirm(message) {
  150.     return showDialog(message, (dialog) => dialog.returnValue === "OK", true);
  151. }
  152.  
  153. async function asyncPrompt(message, defaultValue) {
  154.     return showDialog(message, (dialog) =>
  155.         dialog.returnValue === "OK" ? dialog.firstChild.elements["value"].value : null,
  156.         true, defaultValue);
  157. }
  158.  
  159. async function banUser(username, reason) {
  160.     /* The URL to the ban form does not need any IDs or hidden inputs. We
  161.      * hardcode it here.
  162.      */
  163.     let form, formData;
  164.     try {
  165.         [form, formData] = await openForm(toAbsoluteURL("./mcp.php?i=ban&mode=user"),
  166.             "form#mcp_ban");
  167.     } catch (err) {
  168.         throw `Konnte Formular zum Sperren von Benutzern nicht öffnen: ${err}`;
  169.     }
  170.  
  171.     formData.set("ban", username);
  172.     formData.set("banreason", reason);
  173.     //formData.set("bangivereason", reason);
  174.     try {
  175.         await postForm(form, formData, "bansubmit", true);
  176.     } catch (err) {
  177.         throw `Konnte Benutzer nicht sperren: ${err}`;
  178.     }
  179. }
  180.  
  181. async function closeReport(post) {
  182.     const reportLink = post.querySelector(".post-notice.reported a");
  183.  
  184.     let form, formData;
  185.     try {
  186.         [form, formData] = await openForm(toAbsoluteURL(reportLink.href),
  187.             "form#mcp_report");
  188.     } catch (err) {
  189.         throw `Konnte Formular zum Schließen der Meldung nicht öffnen: ${err}`;
  190.     }
  191.  
  192.     try {
  193.         await postForm(form, formData, "action[close]", true);
  194.     } catch (err) {
  195.         throw `Konnte Meldung nicht schließen: ${err}`;
  196.     }
  197. }
  198.  
  199. async function confirmAction(response) {
  200.     const [form, formData] = await openForm(response, "form#confirm");
  201.     await postForm(form, formData, "confirm");
  202. }
  203.  
  204. function ellipsify(str, maxlen) {
  205.     const ell = str.length > maxlen ? " […]" : "";
  206.     return str.substring(0, maxlen - ell.length) + ell;
  207. }
  208.  
  209. function isPostReported(post) {
  210.     return post.querySelector(".post-notice.reported a") !== null;
  211. }
  212.  
  213. async function openForm(urlOrResponse, selector) {
  214.     const doc = await openDoc(urlOrResponse);
  215.     const form = doc.querySelector(selector);
  216.     return [form, new FormData(form)];
  217. }
  218.  
  219. async function openDoc(urlOrResponse) {
  220.     const resp = urlOrResponse instanceof Response ? urlOrResponse :
  221.         await fetch(urlOrResponse);
  222.     if (!resp.ok) {
  223.         throw `${resp.url}: ${resp.status}`;
  224.     }
  225.  
  226.     const parser = new DOMParser();
  227.     const txt = await resp.text();
  228.     return parser.parseFromString(txt, "text/html");
  229. }
  230.  
  231. async function postForm(form, formData, submitName, requiresConfirmation = false) {
  232.     /* "Press" the right submit button. */
  233.     const submitBtn = form.elements[submitName];
  234.     formData.set(submitBtn.name, submitBtn.value);
  235.  
  236.     /* Have to use explicit getAttribute() below since there is an input with
  237.      * name="action" which would be accessed with `form.action` :-/
  238.      */
  239.     const resp = await fetch(toAbsoluteURL(form.getAttribute("action")),
  240.         { body: new URLSearchParams(formData), method: "POST" });
  241.     if (!resp.ok) {
  242.         throw `${resp.url}: ${resp.status}`;
  243.     }
  244.  
  245.     if (requiresConfirmation) {
  246.         await confirmAction(resp);
  247.     }
  248. }
  249.  
  250. function prefixSubject(input, reason) {
  251.     let subject = input.value;
  252.  
  253.     if (!reason) {
  254.         return subject;
  255.     }
  256.  
  257.     const prefix = `[${reason}]`;
  258.     if (subject.toLowerCase().includes(prefix.toLowerCase())) {
  259.         return subject;
  260.     }
  261.  
  262.     subject = `${prefix} ${subject}`;
  263.     const maxLen = input.getAttribute("maxlength") ?? subject.length;
  264.     return subject.slice(0, maxLen);
  265. }
  266.  
  267. function redirectToArchive() {
  268.     /* TODO: Make the location configurable, redirect to homepage, "Aktive
  269.      * Themen", "Neue Beiträge", or other?
  270.      */
  271.     window.location = `./viewforum.php?f=${ARCHIVFORUMID}`;
  272. }
  273.  
  274. async function remove_post_handler(event) {
  275.     const post = event.currentTarget.closest('.post');
  276.     const usernameElem = post.querySelector(".author .username,.author .username-coloured");
  277.     const username = usernameElem.textContent;
  278.     const thread_title = document.querySelector('.topic-title a').text;
  279.     const content = ellipsify(post.querySelector(".content").innerText, 250);
  280.  
  281.     const splitReason = await asyncPrompt(`Folgenden Beitrag von „${username}“ im Thema „${ellipsify(thread_title, 100)}“ wirklich als Spam archivieren?\n\n„${content}“\n\nGrund:`, "Spam");
  282.     if (splitReason === null) {
  283.         /* Don't do any of the other actions if splitting was cancelled. */
  284.         return;
  285.     }
  286.  
  287.     const archivingPost = send_mcp_request_archival(post, splitReason);
  288.  
  289.     /* Prompting for a separate ban reason in case there is something more
  290.      * specific to note here.
  291.      */
  292.     const userStillExists = usernameElem.nodeName === "A";
  293.     const banReasonPrompt = userStillExists &&
  294.         asyncPrompt(`Benutzer „${username}“ sperren?\n\nGrund:`, "Spam");
  295.  
  296.     /* Mod actions via mcp.php involve a confirm_key which is stored in the
  297.      * database when an action is requested until it is confirmed. There can only
  298.      * be one confirm_key stored at a time---meaning there cannot be multiple mcp
  299.      * actions executed concurrently. See confirm_box() in
  300.      * phpBB/includes/functions.php.
  301.      *
  302.      * This means we cannot really execute the actions concurrently here,
  303.      * unfortunately. User interaction is still done in parallel to one action at
  304.      * a time, though.
  305.      */
  306.     const errors = [];
  307.     try {
  308.         await archivingPost;
  309.     } catch (err) {
  310.         errors.push(err);
  311.     }
  312.  
  313.     let banningUser;
  314.     const banReason = await banReasonPrompt;
  315.     if (banReason) {
  316.         banningUser = banUser(username, banReason);
  317.     } else if (!userStillExists) {
  318.         await asyncAlert(`Benutzer „${username}“ wurde schon gelöscht und kann nicht mehr gesperrt werden.`);
  319.     }
  320.  
  321.     const shouldCloseReport = isPostReported(post) &&
  322.         asyncConfirm("Meldung zum Beitrag schließen?");
  323.  
  324.     try {
  325.         await banningUser;
  326.     } catch (err) {
  327.         errors.push(err);
  328.     }
  329.  
  330.     if (await shouldCloseReport) {
  331.         try {
  332.             await closeReport(post);
  333.         } catch (err) {
  334.             errors.push(err);
  335.         }
  336.     }
  337.  
  338.     for (const error of errors) {
  339.         console.log(error);
  340.         window.alert(`ACHTUNG!\n\n${error}`);
  341.     }
  342.  
  343.     if (errors.length === 0) {
  344.         updatePageAfterSplit(post);
  345.     }
  346. }
  347.  
  348. async function send_mcp_request_archival(post, reason) {
  349.     const splitLink = document.querySelector("#quickmod .dropdown-contents a[href*='action=split']");
  350.  
  351.     let form, formData;
  352.     try {
  353.         [form, formData] = await openForm(toAbsoluteURL(splitLink.href), "form#mcp");
  354.     } catch (err) {
  355.         throw `Konnte Formular zum Aufteilen des Themas nicht öffnen: ${err}`;
  356.     }
  357.  
  358.     const post_id = post.id.slice(1);
  359.     formData.set("post_id_list[]", post_id);
  360.     formData.set("subject", prefixSubject(form.elements["subject"], reason));
  361.     formData.set("to_forum_id", ARCHIVFORUMID);
  362.  
  363.     try {
  364.         await postForm(form, formData, "mcp_topic_submit", true);
  365.     } catch (err) {
  366.         throw `Konnte Thema nicht aufteilen: ${err}`;
  367.     }
  368. }
  369.  
  370. async function showDialog(message, returnFunc = null, abortable = false, defaultValue = null) {
  371.     const dialog = document.body.appendChild(document.createElement("dialog"));
  372.     dialog.className = "quickmod-dialog";
  373.     dialog.style.borderColor = "#D31141";
  374.     dialog.style.maxWidth = "60em";
  375.  
  376.     const form = dialog.appendChild(document.createElement("form"));
  377.     form.method = "dialog";
  378.  
  379.     const p = form.appendChild(document.createElement("p"));
  380.     p.style.whiteSpace = "pre-line";
  381.     p.textContent = message;
  382.  
  383.     if (defaultValue !== null) {
  384.         const inputP = form.appendChild(document.createElement("p"));
  385.         inputP.innerHTML = `<input class="inputbox" name="value" type="text" value="${defaultValue}">`;
  386.     }
  387.  
  388.     const submitButtons = form.appendChild(document.createElement("fieldset"));
  389.     submitButtons.className = "submit-buttons";
  390.     submitButtons.innerHTML = '<input class="button1" type="submit" value="OK"> ';
  391.     if (abortable) {
  392.         const abortBtn = submitButtons.appendChild(document.createElement("input"));
  393.         abortBtn.className = "button2";
  394.         abortBtn.type = "submit";
  395.         abortBtn.value = "Abbrechen";
  396.     }
  397.  
  398.     dialog.showModal();
  399.  
  400.     return new Promise((resolve) => {
  401.         dialog.addEventListener("close", (event) => {
  402.             event.currentTarget.remove();
  403.             resolve(returnFunc?.(event.currentTarget));
  404.         });
  405.     });
  406. }
  407.  
  408. function toAbsoluteURL(relativeOrAbsoluteURL) {
  409.     return new URL(relativeOrAbsoluteURL, window.location);
  410. }
  411.  
  412. function updatePageAfterSplit(post) {
  413.     if (document.querySelectorAll(".post").length > 1) {
  414.         post.parentNode.removeChild(post);
  415.     } else {
  416.         redirectToArchive();
  417.     }
  418. }
  419.  
  420. function add_buttons() {
  421.     const del_post_btn_outer = document.createElement('li');
  422.     const del_post_btn = document.createElement('a');
  423.     del_post_btn.className = 'button button-icon-only';
  424.     del_post_btn.innerHTML = '<i class="icon fa-fire-extinguisher fa-fw" aria-hidden="true"></i><span class="sr-only">Abfall</span>';
  425.     del_post_btn.addEventListener("click", remove_post_handler);
  426.     del_post_btn.title = "Als Spam archivieren";
  427.     del_post_btn_outer.append(del_post_btn);
  428.  
  429.     for (const postButtons of document.querySelectorAll(".post-buttons")) {
  430.         const del_post_btn_outer_clone = del_post_btn_outer.cloneNode(true);
  431.         del_post_btn_outer_clone.addEventListener("click", remove_post_handler);
  432.         postButtons.appendChild(del_post_btn_outer_clone);
  433.     }
  434.  
  435.     const quickmodLinks = document.querySelector("#quickmod .dropdown-contents");
  436.     const archiveThreadLink = quickmodLinks
  437.         .insertBefore(document.createElement("li"), quickmodLinks.firstChild)
  438.         .appendChild(document.createElement("a"));
  439.     archiveThreadLink.addEventListener("click", archiveThreadQuickmod);
  440.     archiveThreadLink.innerText = "Thema als Spam archivieren";
  441.     archiveThreadLink.style.cursor = "pointer";
  442.  
  443.     const stylesheet = document.head.appendChild(document.createElement("style")).sheet;
  444.     /* The pseudo element ::backdrop can only be styled through a rule. */
  445.     stylesheet.insertRule(".quickmod-dialog::backdrop { background: #333C }");
  446. }
  447.  
  448. add_buttons();

Quellcode

Hier kannst du den Code kopieren und ihn in deinen bevorzugten Editor einfügen. PASTEBIN_DOWNLOAD_SNIPPET_EXPLAIN