From 853920831609c732bba3803ccae33dcf106ce2bb Mon Sep 17 00:00:00 2001 From: Thorsten Date: Sat, 3 Sep 2022 12:30:08 +0200 Subject: [PATCH] JTH: ban user, close mod notification --- quickmod.user.js | 278 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 197 insertions(+), 81 deletions(-) diff --git a/quickmod.user.js b/quickmod.user.js index d5c53e4..8905d6a 100644 --- a/quickmod.user.js +++ b/quickmod.user.js @@ -5,120 +5,236 @@ // @match https://debianforum.de/forum/viewtopic.php* // @author Thorsten Sperber // @author JTH -// @version 1.2 +// @version 1.3 // ==/UserScript== const ARCHIVFORUMID = 35; -const boardurl = "/forum/"; -let mod_user_id; +async function banUser(username, reason) { + /* The URL to the ban form does not need any IDs or hidden inputs. We + * hardcode it here. + */ + let resp = await fetch("./mcp.php?i=ban&mode=user"); + if (!resp.ok) { + throw "Konnte Formular zum Sperren von Benutzern nicht laden."; + } + + let form, formData; + try { + [form, formData] = await openForm(resp, "form#mcp_ban"); + } catch { + throw "Konnte Formular zum Sperren von Benutzern nicht öffnen."; + } + + formData.set("ban", username); + formData.set("banreason", reason); + //formData.set("bangivereason", reason); + resp = await postForm(form, formData, "bansubmit"); + if (!resp.ok) { + throw "Konnte Sperrung des Benutzers nicht anfragen."; + } + + try { + await confirmAction(resp); + } catch (err) { + throw `Konnte Sperrung des Benutzers nicht bestätigen: ${err}`; + } +} + +async function closeReport(post) { + const reportLink = post.querySelector(".post-notice.reported a"); + + let resp = await fetch(reportLink.href); + if (!resp.ok) { + throw "Konnte Meldung nicht öffnen."; + } + + let form, formData; + try { + [form, formData] = await openForm(resp, "form#mcp_report"); + } catch { + throw "Konnte Formular zum Schließen der Meldung nicht öffnen."; + } + + resp = await postForm(form, formData, "action[close]"); + if (!resp.ok) { + throw "Konnte Schließen der Meldung nicht anfragen."; + } + + try { + await confirmAction(resp); + } catch (err) { + throw `Konnte Schließen der Meldung nicht bestätigen: ${err}`; + } +} + +async function confirmAction(response) { + const [form, formData] = await openForm(response, "form#confirm"); + + /* Have to use explicit getAttribute() below since there is an input with + * name="action" which would be accessed with `form.action` :-/ + */ + const resp = await postForm(form, formData, "confirm"); + if (!resp.ok) { + throw `${resp.url}: ${resp.status}`; + } + return await resp.text(); +} function ellipsify(str, maxlen) { - const ell = str.length > maxlen ? " […]" : ""; + const ell = str.length > maxlen ? " […]" : ""; return str.substring(0, maxlen - ell.length) + ell; } -function get_thread_id() { - return new URL(document.head.querySelector("link[rel=canonical]").href).searchParams.get("t"); +function isPostReported(post) { + return post.querySelector(".post-notice.reported a") !== null; } -function get_forum_id() { - return new URLSearchParams(document.querySelector('a[href*="f="]').search).get("f"); +async function openForm(response, selector) { + const parser = new DOMParser(); + const txt = await response.text(); + const doc = parser.parseFromString(txt, "text/html"); + const form = doc.querySelector(selector); + return [form, new FormData(form)]; } -function redirect_to_new_topic(redirDoc) { - const links = redirDoc.querySelectorAll("#message a"); - window.location = links[links.length-1].href; +function postForm(form, formData, submitName) { + /* "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` :-/ + */ + return fetch(form.getAttribute("action"), + { body: new URLSearchParams(formData), method: "POST" }); } -function remove_post_handler(event) { +async function remove_post_handler(event) { const post = event.currentTarget.closest('.post'); - const post_id = post.id.slice(1); const username = post.querySelector(".author .username,.author .username-coloured").text; const thread_title = document.querySelector('.topic-title a').text; const content = ellipsify(post.querySelector(".content").innerText, 250); - if (localStorage.getItem("deleteWithoutConfirmation") ?? window.confirm(`Folgenden Beitrag von „${username}“ im Thema „${ellipsify(thread_title, 100)}“ wirklich als Spam archivieren?\n\n„${content}“`)) { - send_mcp_request_archival(post_id, get_thread_id(), thread_title); + + const splitReason = window.prompt(`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; } -} -async function send_mcp_request_confirmation(post_id, confirmDoc) { - const form = confirmDoc.querySelector("form#confirm"); - const data = new FormData(form); - const confirmInput = form.elements["confirm"]; - data.set(confirmInput.name, confirmInput.value); // Press "Yes" - /* Have to use explicit getAttribute() below since there is an input with - * name="action" which would be accessed with `form.action` :-/ + /* Prompting for a separate ban reason in case there is something more + * specific to note here. + */ + const banReason = window.prompt(`Benutzer „${username}“ sperren?\n\nGrund:`, "Spam"); + const shouldCloseReport = isPostReported(post) && window.confirm("Meldung zum Beitrag schließen?"); + + /* Initially, I wanted to use Promise.allSettled() below to trigger and wait + * for all actions in parallel. But that made at least one of them fail in + * most cases. Not sure, if there was a mistake in here somewhere (totally + * not impossible) or if phpBB just does not cope well with parallel mod + * actions (there are some session IDs and confirm_keys involved when a mod + * action is executed and confirmed). + * + * So essentially, we do not have any asynchronous execution here, + * unfortunately. */ - const resp = await fetch(form.getAttribute("action"), - { body: new URLSearchParams(data), method: "POST" }); - if (resp.ok) { - if (localStorage.getItem("redirectToNewTopic") ?? true) { - const respTxt = await resp.text(); - const respDoc = new DOMParser().parseFromString(respTxt, "text/html"); - if (!respDoc.querySelector("parsererror")) { - redirect_to_new_topic(respDoc); - } - } else { - // hide the freshly archived post - document.querySelector('.post#p' + post_id).style.display = "none"; + const errors = new Array(); + try { + await send_mcp_request_archival(post, splitReason); + } catch (err) { + errors.push(err); + } + + if (banReason !== null) { + try { + await banUser(username, banReason); + } catch (err) { + errors.push(err); } } -} -async function send_mcp_request_archival(post_id, thread_id, thread_title) { - const url = boardurl + "mcp.php?f=" + get_forum_id() + "&t=" + thread_id + "&i=main&mode=topic_view&action=split&start=0"; - const data = new URLSearchParams({ - "action": "split_all", - "mcp_topic_submit": "Absenden", - "post_id_list[]": post_id, - "posts_per_page": "15", - "st": "0", - "sk": "t", - "sd": "a", - "icon": "1", - "subject": thread_title.toLowerCase().includes("spam") ? thread_title : "[spam] " + thread_title, - "to_forum_id": ARCHIVFORUMID, - "to_topic_id": "0", - "st_old": "0", - "post_ids[0]": post_id - }); - const resp = await fetch(url, { body: data, method: "POST" }); - if (resp.ok) { - const respTxt = await resp.text(); - const respDoc = new DOMParser().parseFromString(respTxt, "text/html"); - if (!respDoc.querySelector("parsererror")) { - send_mcp_request_confirmation(post_id, respDoc); + if (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); + } } -function add_buttons() { - const postbuttons = document.querySelectorAll(".postbody .post-buttons"); - postbuttons.forEach(function(el) { - // well... - el.style.maxWidth = '41%'; +async function send_mcp_request_archival(post, reason) { + const splitLink = document.querySelector("#quickmod .dropdown-contents a[href*='action=split']"); + let resp = await fetch(splitLink.href); + if (!resp.ok) { + throw "Konnte Formular zum Aufteilen des Themas nicht laden."; + } + + let form, formData; + try { + [form, formData] = await openForm(resp, "form#mcp"); + } catch { + throw "Konnte Formular zum Aufteilen des Themas nicht öffnen."; + } + + const post_id = post.id.slice(1); + const thread_title = (() => { + const prefix = `[${reason}]`; + let title = formData.get("subject"); + if (reason && !title.toLowerCase().includes(prefix.toLowerCase())) { + title = `${prefix} ${title}`; + } + return title.slice(0, form.elements["subject"].maxLength); + })(); + formData.set("post_id_list[]", post_id); + formData.set("subject", thread_title); + formData.set("to_forum_id", ARCHIVFORUMID); - 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 = 'Abfall'; + resp = await postForm(form, formData, "mcp_topic_submit"); + if (!resp.ok) { + throw "Konnte Aufteilen des Themas nicht anfragen."; + } - del_post_btn.addEventListener("click", remove_post_handler); + try { + await confirmAction(resp); + } catch (err) { + throw `Konnte Aufteilen des Themas nicht bestätigen: ${err}`; + } +} - del_post_btn_outer.append(del_post_btn); - el.insertBefore(del_post_btn_outer, el.querySelector('.dropdown-container')); - }); +function updatePageAfterSplit(post) { + if (document.querySelectorAll(".post").length > 1) { + post.parentNode.removeChild(post); + } else { + /* TODO: Make the location configurable, redirect to homepage, "Aktive + * Themen", "Neue Beiträge", or other? + */ + window.location = "/"; + } } -function find_mod_user_id() { - const profile_link = document.querySelector("#username_logged_in a[title^='Profil']"); - return Number.parseInt(new URLSearchParams(profile_link?.search).get("u")); +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 = 'Abfall'; + 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); + } } -mod_user_id ??= find_mod_user_id(); -if (Number.isInteger(mod_user_id)) { - add_buttons(); -} else { - console.error("mod_user_id unset and could not find user ID"); -} \ No newline at end of file +add_buttons(); \ No newline at end of file -- 2.39.2