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:
-
- // ==UserScript==
- // @name debianforum.de-quickmod-additions
- // @namespace org.free.for.all
- // @include https://debianforum.de/forum/viewtopic.php*
- // @match https://debianforum.de/forum/viewtopic.php*
- // @author Thorsten Sperber
- // @author JTH
- // @version 1.5
- // ==/UserScript==
- const ARCHIVFORUMID = 35;
- async function archiveThread(firstPage, reason) {
- const moveProm = (async () => {
- const moveLink = firstPage.querySelector("#quickmod .dropdown-contents a[href*='action=move']");
- let form, formData;
- try {
- [form, formData] = await openForm(toAbsoluteURL(moveLink.href), "form#confirm");
- } catch (err) {
- throw `Konnte Formular zum Verschieben des Themas nicht öffnen: ${err}`;
- }
- formData.set("to_forum_id", ARCHIVFORUMID);
- try {
- /* Unlike splitting a thread, moving does not have a second
- * confirmation step.
- */
- await postForm(form, formData, "confirm");
- } catch (err) {
- throw `Konnte Thema nicht verschieben: ${err}`;
- }
- })();
- const editProm = (async () => {
- const editLink = firstPage.querySelector(".post .post-buttons a[href*='mode=edit']");
- let form, formData;
- try {
- [form, formData] = await openForm(toAbsoluteURL(editLink.href), "form#postform");
- } catch (err) {
- throw `Konnte Formular zum Bearbeiten des ersten Beitrags nicht öffnen: ${err}`;
- }
- formData.set("subject", prefixSubject(form.elements["subject"], reason));
- /* All "altering actions not secured by confirm_box" require a non-zero
- * time difference between opening and submitting a form. See
- * check_form_key() in phpBB/includes/functions.php.
- *
- * So we artificially delay the postForm() for a second.
- */
- await new Promise((resolve) => {
- setTimeout(async () => {
- try {
- await postForm(form, formData, "post");
- } catch (err) {
- throw `Konnte Thema nicht umbenennen: ${err}`;
- }
- resolve();
- }, 1001);
- });
- })();
- /* An mcp action and a post edit can actually be done concurrently! :-) */
- await Promise.all([moveProm, editProm]);
- }
- async function archiveThreadQuickmod() {
- const canonicalURL = new URL(document.querySelector("link[rel='canonical']").href);
- const firstPage = await openDoc(canonicalURL);
- const firstPost = firstPage.querySelector(".post");
- const usernameElem = firstPost.querySelector(".author .username,.author .username-coloured");
- const username = usernameElem.textContent;
- const thread_title = firstPage.querySelector('.topic-title a').text;
- const archiveReason = await asyncPrompt(`Thema „${ellipsify(thread_title, 100)}“ eröffnet von „${username}“ wirklich als Spam archivieren?\n\nGrund:`, "Spam");
- if (archiveReason === null) {
- /* Don't do any of the other actions if moving was cancelled. */
- return;
- }
- const archivingThread = archiveThread(firstPage, archiveReason);
- /* Prompting for a separate ban reason in case there is something more
- * specific to note here.
- */
- const userStillExists = usernameElem.nodeName === "A";
- const banReasonPrompt = userStillExists &&
- asyncPrompt(`Benutzer „${username}“, der das Thema eröffnet hat, sperren?\n\nGrund:`, "Spam");
- /* Mod actions via mcp.php involve a confirm_key which is stored in the
- * database when an action is requested until it is confirmed. There can only
- * be one confirm_key stored at a time---meaning there cannot be multiple mcp
- * actions executed concurrently. See confirm_box() in
- * phpBB/includes/functions.php.
- *
- * This means we cannot really execute the actions concurrently here,
- * unfortunately. User interaction is still done in parallel to one action at
- * a time, though.
- */
- const errors = [];
- try {
- await archivingThread;
- } catch (err) {
- errors.push(err);
- }
- let banningUser;
- const banReason = await banReasonPrompt;
- if (banReason) {
- banningUser = banUser(username, banReason);
- } else if (!userStillExists) {
- await asyncAlert(`Benutzer „${username}“ wurde schon gelöscht und kann nicht mehr gesperrt werden.`);
- }
- const shouldCloseReport = isPostReported(firstPost) &&
- asyncConfirm("Meldung zum ersten Beitrag schließen?");
- try {
- await banningUser;
- } catch (err) {
- errors.push(err);
- }
- if (await shouldCloseReport) {
- try {
- await closeReport(firstPost);
- } catch (err) {
- errors.push(err);
- }
- }
- for (const error of errors) {
- console.log(error);
- window.alert(`ACHTUNG!\n\n${error}`);
- }
- if (errors.length === 0) {
- redirectToArchive();
- }
- }
- async function asyncAlert(message) {
- return showDialog(message);
- }
- async function asyncConfirm(message) {
- return showDialog(message, (dialog) => dialog.returnValue === "OK", true);
- }
- async function asyncPrompt(message, defaultValue) {
- return showDialog(message, (dialog) =>
- dialog.returnValue === "OK" ? dialog.firstChild.elements["value"].value : null,
- true, defaultValue);
- }
- async function banUser(username, reason) {
- /* The URL to the ban form does not need any IDs or hidden inputs. We
- * hardcode it here.
- */
- let form, formData;
- try {
- [form, formData] = await openForm(toAbsoluteURL("./mcp.php?i=ban&mode=user"),
- "form#mcp_ban");
- } catch (err) {
- throw `Konnte Formular zum Sperren von Benutzern nicht öffnen: ${err}`;
- }
- formData.set("ban", username);
- formData.set("banreason", reason);
- //formData.set("bangivereason", reason);
- try {
- await postForm(form, formData, "bansubmit", true);
- } catch (err) {
- throw `Konnte Benutzer nicht sperren: ${err}`;
- }
- }
- async function closeReport(post) {
- const reportLink = post.querySelector(".post-notice.reported a");
- let form, formData;
- try {
- [form, formData] = await openForm(toAbsoluteURL(reportLink.href),
- "form#mcp_report");
- } catch (err) {
- throw `Konnte Formular zum Schließen der Meldung nicht öffnen: ${err}`;
- }
- try {
- await postForm(form, formData, "action[close]", true);
- } catch (err) {
- throw `Konnte Meldung nicht schließen: ${err}`;
- }
- }
- async function confirmAction(response) {
- const [form, formData] = await openForm(response, "form#confirm");
- await postForm(form, formData, "confirm");
- }
- function ellipsify(str, maxlen) {
- const ell = str.length > maxlen ? " […]" : "";
- return str.substring(0, maxlen - ell.length) + ell;
- }
- function isPostReported(post) {
- return post.querySelector(".post-notice.reported a") !== null;
- }
- async function openForm(urlOrResponse, selector) {
- const doc = await openDoc(urlOrResponse);
- const form = doc.querySelector(selector);
- return [form, new FormData(form)];
- }
- async function openDoc(urlOrResponse) {
- const resp = urlOrResponse instanceof Response ? urlOrResponse :
- await fetch(urlOrResponse);
- if (!resp.ok) {
- throw `${resp.url}: ${resp.status}`;
- }
- const parser = new DOMParser();
- const txt = await resp.text();
- return parser.parseFromString(txt, "text/html");
- }
- async function postForm(form, formData, submitName, requiresConfirmation = false) {
- /* "Press" the right submit button. */
- const submitBtn = form.elements[submitName];
- formData.set(submitBtn.name, submitBtn.value);
- /* Have to use explicit getAttribute() below since there is an input with
- * name="action" which would be accessed with `form.action` :-/
- */
- const resp = await fetch(toAbsoluteURL(form.getAttribute("action")),
- { body: new URLSearchParams(formData), method: "POST" });
- if (!resp.ok) {
- throw `${resp.url}: ${resp.status}`;
- }
- if (requiresConfirmation) {
- await confirmAction(resp);
- }
- }
- function prefixSubject(input, reason) {
- let subject = input.value;
- if (!reason) {
- return subject;
- }
- const prefix = `[${reason}]`;
- if (subject.toLowerCase().includes(prefix.toLowerCase())) {
- return subject;
- }
- subject = `${prefix} ${subject}`;
- const maxLen = input.getAttribute("maxlength") ?? subject.length;
- return subject.slice(0, maxLen);
- }
- function redirectToArchive() {
- /* TODO: Make the location configurable, redirect to homepage, "Aktive
- * Themen", "Neue Beiträge", or other?
- */
- window.location = `./viewforum.php?f=${ARCHIVFORUMID}`;
- }
- async function remove_post_handler(event) {
- const post = event.currentTarget.closest('.post');
- const usernameElem = post.querySelector(".author .username,.author .username-coloured");
- const username = usernameElem.textContent;
- const thread_title = document.querySelector('.topic-title a').text;
- const content = ellipsify(post.querySelector(".content").innerText, 250);
- const splitReason = await asyncPrompt(`Folgenden Beitrag von „${username}“ im Thema „${ellipsify(thread_title, 100)}“ wirklich als Spam archivieren?\n\n„${content}“\n\nGrund:`, "Spam");
- if (splitReason === null) {
- /* Don't do any of the other actions if splitting was cancelled. */
- return;
- }
- const archivingPost = send_mcp_request_archival(post, splitReason);
- /* Prompting for a separate ban reason in case there is something more
- * specific to note here.
- */
- const userStillExists = usernameElem.nodeName === "A";
- const banReasonPrompt = userStillExists &&
- asyncPrompt(`Benutzer „${username}“ sperren?\n\nGrund:`, "Spam");
- /* Mod actions via mcp.php involve a confirm_key which is stored in the
- * database when an action is requested until it is confirmed. There can only
- * be one confirm_key stored at a time---meaning there cannot be multiple mcp
- * actions executed concurrently. See confirm_box() in
- * phpBB/includes/functions.php.
- *
- * This means we cannot really execute the actions concurrently here,
- * unfortunately. User interaction is still done in parallel to one action at
- * a time, though.
- */
- const errors = [];
- try {
- await archivingPost;
- } catch (err) {
- errors.push(err);
- }
- let banningUser;
- const banReason = await banReasonPrompt;
- if (banReason) {
- banningUser = banUser(username, banReason);
- } else if (!userStillExists) {
- await asyncAlert(`Benutzer „${username}“ wurde schon gelöscht und kann nicht mehr gesperrt werden.`);
- }
- const shouldCloseReport = isPostReported(post) &&
- asyncConfirm("Meldung zum Beitrag schließen?");
- try {
- await banningUser;
- } catch (err) {
- errors.push(err);
- }
- if (await shouldCloseReport) {
- try {
- await closeReport(post);
- } catch (err) {
- errors.push(err);
- }
- }
- for (const error of errors) {
- console.log(error);
- window.alert(`ACHTUNG!\n\n${error}`);
- }
- if (errors.length === 0) {
- updatePageAfterSplit(post);
- }
- }
- async function send_mcp_request_archival(post, reason) {
- const splitLink = document.querySelector("#quickmod .dropdown-contents a[href*='action=split']");
- let form, formData;
- try {
- [form, formData] = await openForm(toAbsoluteURL(splitLink.href), "form#mcp");
- } catch (err) {
- throw `Konnte Formular zum Aufteilen des Themas nicht öffnen: ${err}`;
- }
- const post_id = post.id.slice(1);
- formData.set("post_id_list[]", post_id);
- formData.set("subject", prefixSubject(form.elements["subject"], reason));
- formData.set("to_forum_id", ARCHIVFORUMID);
- try {
- await postForm(form, formData, "mcp_topic_submit", true);
- } catch (err) {
- throw `Konnte Thema nicht aufteilen: ${err}`;
- }
- }
- async function showDialog(message, returnFunc = null, abortable = false, defaultValue = null) {
- const dialog = document.body.appendChild(document.createElement("dialog"));
- dialog.className = "quickmod-dialog";
- dialog.style.borderColor = "#D31141";
- dialog.style.maxWidth = "60em";
- const form = dialog.appendChild(document.createElement("form"));
- form.method = "dialog";
- const p = form.appendChild(document.createElement("p"));
- p.style.whiteSpace = "pre-line";
- p.textContent = message;
- if (defaultValue !== null) {
- const inputP = form.appendChild(document.createElement("p"));
- inputP.innerHTML = `<input class="inputbox" name="value" type="text" value="${defaultValue}">`;
- }
- const submitButtons = form.appendChild(document.createElement("fieldset"));
- submitButtons.className = "submit-buttons";
- submitButtons.innerHTML = '<input class="button1" type="submit" value="OK"> ';
- if (abortable) {
- const abortBtn = submitButtons.appendChild(document.createElement("input"));
- abortBtn.className = "button2";
- abortBtn.type = "submit";
- abortBtn.value = "Abbrechen";
- }
- dialog.showModal();
- return new Promise((resolve) => {
- dialog.addEventListener("close", (event) => {
- event.currentTarget.remove();
- resolve(returnFunc?.(event.currentTarget));
- });
- });
- }
- function toAbsoluteURL(relativeOrAbsoluteURL) {
- return new URL(relativeOrAbsoluteURL, window.location);
- }
- function updatePageAfterSplit(post) {
- if (document.querySelectorAll(".post").length > 1) {
- post.parentNode.removeChild(post);
- } else {
- redirectToArchive();
- }
- }
- function add_buttons() {
- const del_post_btn_outer = document.createElement('li');
- const del_post_btn = document.createElement('a');
- del_post_btn.className = 'button button-icon-only';
- del_post_btn.innerHTML = '<i class="icon fa-fire-extinguisher fa-fw" aria-hidden="true"></i><span class="sr-only">Abfall</span>';
- del_post_btn.addEventListener("click", remove_post_handler);
- del_post_btn.title = "Als Spam archivieren";
- del_post_btn_outer.append(del_post_btn);
- for (const postButtons of document.querySelectorAll(".post-buttons")) {
- const del_post_btn_outer_clone = del_post_btn_outer.cloneNode(true);
- del_post_btn_outer_clone.addEventListener("click", remove_post_handler);
- postButtons.appendChild(del_post_btn_outer_clone);
- }
- const quickmodLinks = document.querySelector("#quickmod .dropdown-contents");
- const archiveThreadLink = quickmodLinks
- .insertBefore(document.createElement("li"), quickmodLinks.firstChild)
- .appendChild(document.createElement("a"));
- archiveThreadLink.addEventListener("click", archiveThreadQuickmod);
- archiveThreadLink.innerText = "Thema als Spam archivieren";
- archiveThreadLink.style.cursor = "pointer";
- const stylesheet = document.head.appendChild(document.createElement("style")).sheet;
- /* The pseudo element ::backdrop can only be styled through a rule. */
- stylesheet.insertRule(".quickmod-dialog::backdrop { background: #333C }");
- }
- add_buttons();
Quellcode
Hier kannst du den Code kopieren und ihn in deinen bevorzugten Editor einfügen. PASTEBIN_DOWNLOAD_SNIPPET_EXPLAIN