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 banUser(username, reason) {
14 /* The URL to the ban form does not need any IDs or hidden inputs. We
17 let resp = await fetch(toAbsoluteURL("./mcp.php?i=ban&mode=user"));
19 throw "Konnte Formular zum Sperren von Benutzern nicht laden.";
24 [form, formData] = await openForm(resp, "form#mcp_ban");
26 throw "Konnte Formular zum Sperren von Benutzern nicht öffnen.";
29 formData.set("ban", username);
30 formData.set("banreason", reason);
31 //formData.set("bangivereason", reason);
32 resp = await postForm(form, formData, "bansubmit");
34 throw "Konnte Sperrung des Benutzers nicht anfragen.";
38 await confirmAction(resp);
40 throw `Konnte Sperrung des Benutzers nicht bestätigen: ${err}`;
44 async function closeReport(post) {
45 const reportLink = post.querySelector(".post-notice.reported a");
47 let resp = await fetch(toAbsoluteURL(reportLink.href));
49 throw "Konnte Meldung nicht öffnen.";
54 [form, formData] = await openForm(resp, "form#mcp_report");
56 throw "Konnte Formular zum Schließen der Meldung nicht öffnen.";
59 resp = await postForm(form, formData, "action[close]");
61 throw "Konnte Schließen der Meldung nicht anfragen.";
65 await confirmAction(resp);
67 throw `Konnte Schließen der Meldung nicht bestätigen: ${err}`;
71 async function confirmAction(response) {
72 const [form, formData] = await openForm(response, "form#confirm");
74 /* Have to use explicit getAttribute() below since there is an input with
75 * name="action" which would be accessed with `form.action` :-/
77 const resp = await postForm(form, formData, "confirm");
79 throw `${resp.url}: ${resp.status}`;
81 return await resp.text();
84 function ellipsify(str, maxlen) {
85 const ell = str.length > maxlen ? " […]" : "";
86 return str.substring(0, maxlen - ell.length) + ell;
89 function isPostReported(post) {
90 return post.querySelector(".post-notice.reported a") !== null;
93 async function openForm(response, selector) {
94 const parser = new DOMParser();
95 const txt = await response.text();
96 const doc = parser.parseFromString(txt, "text/html");
97 const form = doc.querySelector(selector);
98 return [form, new FormData(form)];
101 function postForm(form, formData, submitName) {
102 /* "Press" the right submit button. */
103 const submitBtn = form.elements[submitName];
104 formData.set(submitBtn.name, submitBtn.value);
106 /* Have to use explicit getAttribute() below since there is an input with
107 * name="action" which would be accessed with `form.action` :-/
109 return fetch(toAbsoluteURL(form.getAttribute("action")),
110 { body: new URLSearchParams(formData), method: "POST" });
113 async function remove_post_handler(event) {
114 const post = event.currentTarget.closest('.post');
115 const usernameElem = post.querySelector(".author .username,.author .username-coloured");
116 const username = usernameElem.textContent;
117 const thread_title = document.querySelector('.topic-title a').text;
118 const content = ellipsify(post.querySelector(".content").innerText, 250);
120 const splitReason = window.prompt(`Folgenden Beitrag von „${username}“ im Thema „${ellipsify(thread_title, 100)}“ wirklich als Spam archivieren?\n\n„${content}“\n\nGrund:`, "Spam");
121 if (splitReason === null) {
122 /* Don't do any of the other actions if splitting was cancelled. */
126 /* Prompting for a separate ban reason in case there is something more
127 * specific to note here.
129 const userStillExists = usernameElem.nodeName === "A";
130 const banReason = userStillExists &&
131 window.prompt(`Benutzer „${username}“ sperren?\n\nGrund:`, "Spam");
132 const shouldCloseReport = isPostReported(post) && window.confirm("Meldung zum Beitrag schließen?");
134 /* Initially, I wanted to use Promise.allSettled() below to trigger and wait
135 * for all actions in parallel. But that made at least one of them fail in
136 * most cases. Not sure, if there was a mistake in here somewhere (totally
137 * not impossible) or if phpBB just does not cope well with parallel mod
138 * actions (there are some session IDs and confirm_keys involved when a mod
139 * action is executed and confirmed).
141 * So essentially, we do not have any asynchronous execution here,
144 const errors = new Array();
146 await send_mcp_request_archival(post, splitReason);
153 await banUser(username, banReason);
157 } else if (!userStillExists) {
158 window.alert(`Benutzer „${username}“ wurde schon gelöscht und kann nicht mehr gesperrt werden.`);
161 if (shouldCloseReport) {
163 await closeReport(post);
169 for (const error of errors) {
171 window.alert(`ACHTUNG!\n\n${error}`);
174 if (errors.length === 0) {
175 updatePageAfterSplit(post);
179 async function send_mcp_request_archival(post, reason) {
180 const splitLink = document.querySelector("#quickmod .dropdown-contents a[href*='action=split']");
181 let resp = await fetch(toAbsoluteURL(splitLink.href));
183 throw "Konnte Formular zum Aufteilen des Themas nicht laden.";
188 [form, formData] = await openForm(resp, "form#mcp");
190 throw "Konnte Formular zum Aufteilen des Themas nicht öffnen.";
193 const post_id = post.id.slice(1);
194 const thread_title = (() => {
195 const prefix = `[${reason}]`;
196 let title = formData.get("subject");
197 if (reason && !title.toLowerCase().includes(prefix.toLowerCase())) {
198 title = `${prefix} ${title}`;
200 return title.slice(0, form.elements["subject"].maxLength);
202 formData.set("post_id_list[]", post_id);
203 formData.set("subject", thread_title);
204 formData.set("to_forum_id", ARCHIVFORUMID);
206 resp = await postForm(form, formData, "mcp_topic_submit");
208 throw "Konnte Aufteilen des Themas nicht anfragen.";
212 await confirmAction(resp);
214 throw `Konnte Aufteilen des Themas nicht bestätigen: ${err}`;
218 function toAbsoluteURL(relativeOrAbsoluteURL) {
219 return new URL(relativeOrAbsoluteURL, window.location);
222 function updatePageAfterSplit(post) {
223 if (document.querySelectorAll(".post").length > 1) {
224 post.parentNode.removeChild(post);
226 /* TODO: Make the location configurable, redirect to homepage, "Aktive
227 * Themen", "Neue Beiträge", or other?
229 window.location = "/";
233 function add_buttons() {
234 const del_post_btn_outer = document.createElement('li');
235 const del_post_btn = document.createElement('a');
236 del_post_btn.className = 'button button-icon-only';
237 del_post_btn.innerHTML = '<i class="icon fa-fire-extinguisher fa-fw" aria-hidden="true"></i><span class="sr-only">Abfall</span>';
238 del_post_btn.addEventListener("click", remove_post_handler);
239 del_post_btn.title = "Als Spam archivieren";
240 del_post_btn_outer.append(del_post_btn);
242 for (const postButtons of document.querySelectorAll(".post-buttons")) {
243 const del_post_btn_outer_clone = del_post_btn_outer.cloneNode(true);
244 del_post_btn_outer_clone.addEventListener("click", remove_post_handler);
245 postButtons.appendChild(del_post_btn_outer_clone);