]> git.aero2k.de Git - dfde/quickmods.git/blob - quickmod.user.js
5eb4defbc6cf3171a371cb195ee375b83f7e012f
[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 form, formData;
18     try {
19         [form, formData] = await openForm(toAbsoluteURL("./mcp.php?i=ban&mode=user"),
20             "form#mcp_ban");
21     } catch (err) {
22         throw `Konnte Formular zum Sperren von Benutzern nicht öffnen: ${err}`;
23     }
24
25     formData.set("ban", username);
26     formData.set("banreason", reason);
27     //formData.set("bangivereason", reason);
28     const resp = await postForm(form, formData, "bansubmit");
29     if (!resp.ok) {
30         throw "Konnte Sperrung des Benutzers nicht anfragen.";
31     }
32
33     try {
34         await confirmAction(resp);
35     } catch (err) {
36         throw `Konnte Sperrung des Benutzers nicht bestätigen: ${err}`;
37     }
38 }
39
40 async function closeReport(post) {
41     const reportLink = post.querySelector(".post-notice.reported a");
42
43     let form, formData;
44     try {
45         [form, formData] = await openForm(toAbsoluteURL(reportLink.href),
46             "form#mcp_report");
47     } catch (err) {
48         throw `Konnte Formular zum Schließen der Meldung nicht öffnen: ${err}`;
49     }
50
51     const resp = await postForm(form, formData, "action[close]");
52     if (!resp.ok) {
53         throw "Konnte Schließen der Meldung nicht anfragen.";
54     }
55
56     try {
57         await confirmAction(resp);
58     } catch (err) {
59         throw `Konnte Schließen der Meldung nicht bestätigen: ${err}`;
60     }
61 }
62
63 async function confirmAction(response) {
64     const [form, formData] = await openForm(response, "form#confirm");
65
66     /* Have to use explicit getAttribute() below since there is an input with
67      * name="action" which would be accessed with `form.action` :-/
68      */
69     const resp = await postForm(form, formData, "confirm");
70     if (!resp.ok) {
71         throw `${resp.url}: ${resp.status}`;
72     }
73     return resp.text();
74 }
75
76 function ellipsify(str, maxlen) {
77     const ell = str.length > maxlen ? " […]" : "";
78     return str.substring(0, maxlen - ell.length) + ell;
79 }
80
81 function isPostReported(post) {
82     return post.querySelector(".post-notice.reported a") !== null;
83 }
84
85 async function openForm(urlOrResponse, selector) {
86     const resp = urlOrResponse instanceof Response ? urlOrResponse :
87         await fetch(urlOrResponse);
88     if (!resp.ok) {
89         throw `${resp.url}: ${resp.status}`;
90     }
91
92     const parser = new DOMParser();
93     const txt = await resp.text();
94     const doc = parser.parseFromString(txt, "text/html");
95     const form = doc.querySelector(selector);
96     return [form, new FormData(form)];
97 }
98
99 function postForm(form, formData, submitName) {
100     /* "Press" the right submit button. */
101     const submitBtn = form.elements[submitName];
102     formData.set(submitBtn.name, submitBtn.value);
103
104     /* Have to use explicit getAttribute() below since there is an input with
105      * name="action" which would be accessed with `form.action` :-/
106      */
107     return fetch(toAbsoluteURL(form.getAttribute("action")),
108         { body: new URLSearchParams(formData), method: "POST" });
109 }
110
111 async function remove_post_handler(event) {
112     const post = event.currentTarget.closest('.post');
113     const usernameElem = post.querySelector(".author .username,.author .username-coloured");
114     const username = usernameElem.textContent;
115     const thread_title = document.querySelector('.topic-title a').text;
116     const content = ellipsify(post.querySelector(".content").innerText, 250);
117
118     const splitReason = window.prompt(`Folgenden Beitrag von „${username}“ im Thema „${ellipsify(thread_title, 100)}“ wirklich als Spam archivieren?\n\n„${content}“\n\nGrund:`, "Spam");
119     if (splitReason === null) {
120         /* Don't do any of the other actions if splitting was cancelled. */
121         return;
122     }
123
124     /* Prompting for a separate ban reason in case there is something more
125      * specific to note here.
126      */
127     const userStillExists = usernameElem.nodeName === "A";
128     const banReason = userStillExists &&
129         window.prompt(`Benutzer „${username}“ sperren?\n\nGrund:`, "Spam");
130     const shouldCloseReport = isPostReported(post) && window.confirm("Meldung zum Beitrag schließen?");
131
132     /* Initially, I wanted to use Promise.allSettled() below to trigger and wait
133      * for all actions in parallel. But that made at least one of them fail in
134      * most cases. Not sure, if there was a mistake in here somewhere (totally
135      * not impossible) or if phpBB just does not cope well with parallel mod
136      * actions (there are some session IDs and confirm_keys involved when a mod
137      * action is executed and confirmed).
138      *
139      * So essentially, we do not have any asynchronous execution here,
140      * unfortunately.
141      */
142     const errors = new Array();
143     try {
144         await send_mcp_request_archival(post, splitReason);
145     } catch (err) {
146         errors.push(err);
147     }
148
149     if (banReason) {
150         try {
151             await banUser(username, banReason);
152         } catch (err) {
153             errors.push(err);
154         }
155     } else if (!userStillExists) {
156         window.alert(`Benutzer „${username}“ wurde schon gelöscht und kann nicht mehr gesperrt werden.`);
157     }
158
159     if (shouldCloseReport) {
160         try {
161             await closeReport(post);
162         } catch (err) {
163             errors.push(err);
164         }
165     }
166
167     for (const error of errors) {
168         console.log(error);
169         window.alert(`ACHTUNG!\n\n${error}`);
170     }
171
172     if (errors.length === 0) {
173         updatePageAfterSplit(post);
174     }
175 }
176
177 async function send_mcp_request_archival(post, reason) {
178     const splitLink = document.querySelector("#quickmod .dropdown-contents a[href*='action=split']");
179
180     let form, formData;
181     try {
182         [form, formData] = await openForm(toAbsoluteURL(splitLink.href), "form#mcp");
183     } catch (err) {
184         throw `Konnte Formular zum Aufteilen des Themas nicht öffnen: ${err}`;
185     }
186
187     const post_id = post.id.slice(1);
188     const thread_title = (() => {
189         const prefix = `[${reason}]`;
190         let title = formData.get("subject");
191         if (reason && !title.toLowerCase().includes(prefix.toLowerCase())) {
192             title = `${prefix} ${title}`;
193         }
194         return title.slice(0, form.elements["subject"].maxLength);
195     })();
196     formData.set("post_id_list[]", post_id);
197     formData.set("subject", thread_title);
198     formData.set("to_forum_id", ARCHIVFORUMID);
199
200     const resp = await postForm(form, formData, "mcp_topic_submit");
201     if (!resp.ok) {
202         throw "Konnte Aufteilen des Themas nicht anfragen.";
203     }
204
205     try {
206         await confirmAction(resp);
207     } catch (err) {
208         throw `Konnte Aufteilen des Themas nicht bestätigen: ${err}`;
209     }
210 }
211
212 function toAbsoluteURL(relativeOrAbsoluteURL) {
213     return new URL(relativeOrAbsoluteURL, window.location);
214 }
215
216 function updatePageAfterSplit(post) {
217     if (document.querySelectorAll(".post").length > 1) {
218         post.parentNode.removeChild(post);
219     } else {
220         /* TODO: Make the location configurable, redirect to homepage, "Aktive
221          * Themen", "Neue Beiträge", or other?
222          */
223         window.location = "/";
224     }
225 }
226
227 function add_buttons() {
228     const del_post_btn_outer = document.createElement('li');
229     const del_post_btn = document.createElement('a');
230     del_post_btn.className = 'button button-icon-only';
231     del_post_btn.innerHTML = '<i class="icon fa-fire-extinguisher fa-fw" aria-hidden="true"></i><span class="sr-only">Abfall</span>';
232     del_post_btn.addEventListener("click", remove_post_handler);
233     del_post_btn.title = "Als Spam archivieren";
234     del_post_btn_outer.append(del_post_btn);
235
236     for (const postButtons of document.querySelectorAll(".post-buttons")) {
237         const del_post_btn_outer_clone = del_post_btn_outer.cloneNode(true);
238         del_post_btn_outer_clone.addEventListener("click", remove_post_handler);
239         postButtons.appendChild(del_post_btn_outer_clone);
240     }
241 }
242
243 add_buttons();