]> git.aero2k.de Git - dfde/quickmods.git/blob - quickmod.user.js
Bump version to 1.4
[dfde/quickmods.git] / quickmod.user.js
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.4
9 // ==/UserScript==
10
11 const ARCHIVFORUMID = 35;
12
13 async function banUser(username, reason) {
14     /* The URL to the ban form does not need any IDs or hidden inputs. We
15      * hardcode it here.
16      */
17     let resp = await fetch(toAbsoluteURL("./mcp.php?i=ban&mode=user"));
18     if (!resp.ok) {
19         throw "Konnte Formular zum Sperren von Benutzern nicht laden.";
20     }
21
22     let form, formData;
23     try {
24         [form, formData] = await openForm(resp, "form#mcp_ban");
25     } catch {
26         throw "Konnte Formular zum Sperren von Benutzern nicht öffnen.";
27     }
28
29     formData.set("ban", username);
30     formData.set("banreason", reason);
31     //formData.set("bangivereason", reason);
32     resp = await postForm(form, formData, "bansubmit");
33     if (!resp.ok) {
34         throw "Konnte Sperrung des Benutzers nicht anfragen.";
35     }
36
37     try {
38         await confirmAction(resp);
39     } catch (err) {
40         throw `Konnte Sperrung des Benutzers nicht bestätigen: ${err}`;
41     }
42 }
43
44 async function closeReport(post) {
45     const reportLink = post.querySelector(".post-notice.reported a");
46
47     let resp = await fetch(toAbsoluteURL(reportLink.href));
48     if (!resp.ok) {
49         throw "Konnte Meldung nicht öffnen.";
50     }
51
52     let form, formData;
53     try {
54         [form, formData] = await openForm(resp, "form#mcp_report");
55     } catch {
56         throw "Konnte Formular zum Schließen der Meldung nicht öffnen.";
57     }
58
59     resp = await postForm(form, formData, "action[close]");
60     if (!resp.ok) {
61         throw "Konnte Schließen der Meldung nicht anfragen.";
62     }
63
64     try {
65         await confirmAction(resp);
66     } catch (err) {
67         throw `Konnte Schließen der Meldung nicht bestätigen: ${err}`;
68     }
69 }
70
71 async function confirmAction(response) {
72     const [form, formData] = await openForm(response, "form#confirm");
73
74     /* Have to use explicit getAttribute() below since there is an input with
75      * name="action" which would be accessed with `form.action` :-/
76      */
77     const resp = await postForm(form, formData, "confirm");
78     if (!resp.ok) {
79         throw `${resp.url}: ${resp.status}`;
80     }
81     return await resp.text();
82 }
83
84 function ellipsify(str, maxlen) {
85     const ell = str.length > maxlen ? " […]" : "";
86     return str.substring(0, maxlen - ell.length) + ell;
87 }
88
89 function isPostReported(post) {
90     return post.querySelector(".post-notice.reported a") !== null;
91 }
92
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)];
99 }
100
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);
105
106     /* Have to use explicit getAttribute() below since there is an input with
107      * name="action" which would be accessed with `form.action` :-/
108      */
109     return fetch(toAbsoluteURL(form.getAttribute("action")),
110         { body: new URLSearchParams(formData), method: "POST" });
111 }
112
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);
119
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. */
123         return;
124     }
125
126     /* Prompting for a separate ban reason in case there is something more
127      * specific to note here.
128      */
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?");
133
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).
140      *
141      * So essentially, we do not have any asynchronous execution here,
142      * unfortunately.
143      */
144     const errors = new Array();
145     try {
146         await send_mcp_request_archival(post, splitReason);
147     } catch (err) {
148         errors.push(err);
149     }
150
151     if (banReason) {
152         try {
153             await banUser(username, banReason);
154         } catch (err) {
155             errors.push(err);
156         }
157     } else if (!userStillExists) {
158         window.alert(`Benutzer „${username}“ wurde schon gelöscht und kann nicht mehr gesperrt werden.`);
159     }
160
161     if (shouldCloseReport) {
162         try {
163             await closeReport(post);
164         } catch (err) {
165             errors.push(err);
166         }
167     }
168
169     for (const error of errors) {
170         console.log(error);
171         window.alert(`ACHTUNG!\n\n${error}`);
172     }
173
174     if (errors.length === 0) {
175         updatePageAfterSplit(post);
176     }
177 }
178
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));
182     if (!resp.ok) {
183         throw "Konnte Formular zum Aufteilen des Themas nicht laden.";
184     }
185
186     let form, formData;
187     try {
188         [form, formData] = await openForm(resp, "form#mcp");
189     } catch {
190         throw "Konnte Formular zum Aufteilen des Themas nicht öffnen.";
191     }
192
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}`;
199         }
200         return title.slice(0, form.elements["subject"].maxLength);
201     })();
202     formData.set("post_id_list[]", post_id);
203     formData.set("subject", thread_title);
204     formData.set("to_forum_id", ARCHIVFORUMID);
205
206     resp = await postForm(form, formData, "mcp_topic_submit");
207     if (!resp.ok) {
208         throw "Konnte Aufteilen des Themas nicht anfragen.";
209     }
210
211     try {
212         await confirmAction(resp);
213     } catch (err) {
214         throw `Konnte Aufteilen des Themas nicht bestätigen: ${err}`;
215     }
216 }
217
218 function toAbsoluteURL(relativeOrAbsoluteURL) {
219     return new URL(relativeOrAbsoluteURL, window.location);
220 }
221
222 function updatePageAfterSplit(post) {
223     if (document.querySelectorAll(".post").length > 1) {
224         post.parentNode.removeChild(post);
225     } else {
226         /* TODO: Make the location configurable, redirect to homepage, "Aktive
227          * Themen", "Neue Beiträge", or other?
228          */
229         window.location = "/";
230     }
231 }
232
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);
241
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);
246     }
247 }
248
249 add_buttons();