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
11 const ARCHIVFORUMID = 35;
13 async function asyncAlert(message) {
14 window.alert(message);
17 async function asyncConfirm(message) {
18 return window.confirm(message);
21 async function asyncPrompt(message, defaultValue) {
22 return window.prompt(message, defaultValue);
25 async function banUser(username, reason) {
26 /* The URL to the ban form does not need any IDs or hidden inputs. We
31 [form, formData] = await openForm(toAbsoluteURL("./mcp.php?i=ban&mode=user"),
34 throw `Konnte Formular zum Sperren von Benutzern nicht öffnen: ${err}`;
37 formData.set("ban", username);
38 formData.set("banreason", reason);
39 //formData.set("bangivereason", reason);
41 await postForm(form, formData, "bansubmit", true);
43 throw `Konnte Benutzer nicht sperren: ${err}`;
47 async function closeReport(post) {
48 const reportLink = post.querySelector(".post-notice.reported a");
52 [form, formData] = await openForm(toAbsoluteURL(reportLink.href),
55 throw `Konnte Formular zum Schließen der Meldung nicht öffnen: ${err}`;
59 await postForm(form, formData, "action[close]", true);
61 throw `Konnte Meldung nicht schließen: ${err}`;
65 async function confirmAction(response) {
66 const [form, formData] = await openForm(response, "form#confirm");
67 await postForm(form, formData, "confirm");
70 function ellipsify(str, maxlen) {
71 const ell = str.length > maxlen ? " […]" : "";
72 return str.substring(0, maxlen - ell.length) + ell;
75 function isPostReported(post) {
76 return post.querySelector(".post-notice.reported a") !== null;
79 async function openForm(urlOrResponse, selector) {
80 const resp = urlOrResponse instanceof Response ? urlOrResponse :
81 await fetch(urlOrResponse);
83 throw `${resp.url}: ${resp.status}`;
86 const parser = new DOMParser();
87 const txt = await resp.text();
88 const doc = parser.parseFromString(txt, "text/html");
89 const form = doc.querySelector(selector);
90 return [form, new FormData(form)];
93 async function postForm(form, formData, submitName, requiresConfirmation = false) {
94 /* "Press" the right submit button. */
95 const submitBtn = form.elements[submitName];
96 formData.set(submitBtn.name, submitBtn.value);
98 /* Have to use explicit getAttribute() below since there is an input with
99 * name="action" which would be accessed with `form.action` :-/
101 const resp = await fetch(toAbsoluteURL(form.getAttribute("action")),
102 { body: new URLSearchParams(formData), method: "POST" });
104 throw `${resp.url}: ${resp.status}`;
107 if (requiresConfirmation) {
108 await confirmAction(resp);
112 function prefixSubject(input, reason) {
113 let subject = input.value;
119 const prefix = `[${reason}]`;
120 if (subject.toLowerCase().includes(prefix.toLowerCase())) {
124 subject = `${prefix} ${subject}`;
125 const maxLen = input.getAttribute("maxlength") ?? subject.length;
126 return subject.slice(0, maxLen);
129 function redirectToArchive() {
130 /* TODO: Make the location configurable, redirect to homepage, "Aktive
131 * Themen", "Neue Beiträge", or other?
133 window.location = `./viewforum.php?f=${ARCHIVFORUMID}`;
136 async function remove_post_handler(event) {
137 const post = event.currentTarget.closest('.post');
138 const usernameElem = post.querySelector(".author .username,.author .username-coloured");
139 const username = usernameElem.textContent;
140 const thread_title = document.querySelector('.topic-title a').text;
141 const content = ellipsify(post.querySelector(".content").innerText, 250);
143 const splitReason = await asyncPrompt(`Folgenden Beitrag von „${username}“ im Thema „${ellipsify(thread_title, 100)}“ wirklich als Spam archivieren?\n\n„${content}“\n\nGrund:`, "Spam");
144 if (splitReason === null) {
145 /* Don't do any of the other actions if splitting was cancelled. */
149 const archivingPost = send_mcp_request_archival(post, splitReason);
151 /* Prompting for a separate ban reason in case there is something more
152 * specific to note here.
154 const userStillExists = usernameElem.nodeName === "A";
155 const banReasonPrompt = userStillExists &&
156 asyncPrompt(`Benutzer „${username}“ sperren?\n\nGrund:`, "Spam");
158 /* Mod actions via mcp.php involve a confirm_key which is stored in the
159 * database when an action is requested until it is confirmed. There can only
160 * be one confirm_key stored at a time---meaning there cannot be multiple mcp
161 * actions executed concurrently. See confirm_box() in
162 * phpBB/includes/functions.php.
164 * This means we cannot really execute the actions concurrently here,
165 * unfortunately. User interaction is still done in parallel to one action at
176 const banReason = await banReasonPrompt;
178 banningUser = banUser(username, banReason);
179 } else if (!userStillExists) {
180 await asyncAlert(`Benutzer „${username}“ wurde schon gelöscht und kann nicht mehr gesperrt werden.`);
183 const shouldCloseReport = isPostReported(post) &&
184 asyncConfirm("Meldung zum Beitrag schließen?");
192 if (await shouldCloseReport) {
194 await closeReport(post);
200 for (const error of errors) {
202 window.alert(`ACHTUNG!\n\n${error}`);
205 if (errors.length === 0) {
206 updatePageAfterSplit(post);
210 async function send_mcp_request_archival(post, reason) {
211 const splitLink = document.querySelector("#quickmod .dropdown-contents a[href*='action=split']");
215 [form, formData] = await openForm(toAbsoluteURL(splitLink.href), "form#mcp");
217 throw `Konnte Formular zum Aufteilen des Themas nicht öffnen: ${err}`;
220 const post_id = post.id.slice(1);
221 formData.set("post_id_list[]", post_id);
222 formData.set("subject", prefixSubject(form.elements["subject"], reason));
223 formData.set("to_forum_id", ARCHIVFORUMID);
226 await postForm(form, formData, "mcp_topic_submit", true);
228 throw `Konnte Thema nicht aufteilen: ${err}`;
232 function toAbsoluteURL(relativeOrAbsoluteURL) {
233 return new URL(relativeOrAbsoluteURL, window.location);
236 function updatePageAfterSplit(post) {
237 if (document.querySelectorAll(".post").length > 1) {
238 post.parentNode.removeChild(post);
244 function add_buttons() {
245 const del_post_btn_outer = document.createElement('li');
246 const del_post_btn = document.createElement('a');
247 del_post_btn.className = 'button button-icon-only';
248 del_post_btn.innerHTML = '<i class="icon fa-fire-extinguisher fa-fw" aria-hidden="true"></i><span class="sr-only">Abfall</span>';
249 del_post_btn.addEventListener("click", remove_post_handler);
250 del_post_btn.title = "Als Spam archivieren";
251 del_post_btn_outer.append(del_post_btn);
253 for (const postButtons of document.querySelectorAll(".post-buttons")) {
254 const del_post_btn_outer_clone = del_post_btn_outer.cloneNode(true);
255 del_post_btn_outer_clone.addEventListener("click", remove_post_handler);
256 postButtons.appendChild(del_post_btn_outer_clone);