Skip to content

Commit c783443

Browse files
committed
feat: 提删小工具
1 parent 17908c1 commit c783443

4 files changed

Lines changed: 367 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ dist
105105

106106
# WorkBuddy agent workspace data (project memory, local skills, caches)
107107
.workbuddy/
108+
.tmp-verify/
108109

109110
.tmp
110111

‎src/gadgets/Gadgets-definition-list.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
- usergroup
6363
- queryContributions
6464
- userpageDelete
65+
- requestDeletion
6566

6667
- name: editing
6768
gadgets:
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
"use strict";
2+
$(() => {
3+
try {
4+
const { wgArticleId, wgPageName, wgUserName, wgUserGroups, wgNamespaceNumber, wgNamespaceIds } = mw.config.get([
5+
"wgArticleId",
6+
"wgPageName",
7+
"wgUserName",
8+
"wgUserGroups",
9+
"wgNamespaceNumber",
10+
"wgNamespaceIds",
11+
]);
12+
13+
if (
14+
wgArticleId === 0
15+
|| wgNamespaceNumber < 0
16+
|| wgPageName.startsWith("萌娘百科_talk:讨论版")
17+
|| $(".will2Be2Deleted")[0]
18+
|| (wgUserGroups.includes("patroller") || wgUserGroups.includes("sysop")) && !(new URL(location.href).searchParams.get("AnnTools-debug") || "").split("|").includes("requestDeletion")
19+
) {
20+
return;
21+
}
22+
23+
const isZh = /^m?zh\.moegirl\.org\.cn$/.test(location.hostname);
24+
const api = new mw.Api(), zhAPI = isZh ? api : new mw.ForeignApi("https://mzh.moegirl.org.cn/api.php");
25+
26+
const BOARD_PAGE = "萌娘百科_talk:讨论版/操作申请";
27+
const boardUrl = `${isZh ? "" : "https://mzh.moegirl.org.cn"}${mw.util.getUrl(BOARD_PAGE)}`;
28+
const linkedPageName = isZh ? wgPageName : `cm:${wgPageName}`;
29+
30+
const $body = $("body");
31+
$("#mw-notification-area").appendTo($body);
32+
33+
const getRequestTitleSuffix = (userName, now) => ` - ${userName} - ${now.getFullYear()}.${libPrefixNumber(now.getMonth() + 1)}.${libPrefixNumber(now.getDate())}`;
34+
35+
const buildSectionTitle = () => `请求删除页面:[[${linkedPageName}]]${getRequestTitleSuffix(wgUserName, new Date())}`;
36+
37+
/**
38+
* 新讨论串的正文
39+
*
40+
* @param {string} reason 理由
41+
* @param {string} detail 详情;为空时整行不输出
42+
* @returns {string} 正文 wikitext
43+
*/
44+
const buildWikitext = (reason, detail) => {
45+
const lines = [
46+
`* '''${wgULS("页面", "頁面")}''':[[${linkedPageName}]]`,
47+
`* '''${wgULS("理由", "緣由")}''':${reason}`,
48+
];
49+
if (detail) {
50+
lines.push(`* '''${wgULS("详情", "詳情")}''':${detail}`);
51+
}
52+
lines.push("~~~~");
53+
return lines.join("\n");
54+
};
55+
56+
const fetchNewSectionAnchor = async (revid) => {
57+
if (!revid) {
58+
return "";
59+
}
60+
try {
61+
const res = await zhAPI.get({
62+
action: "parse",
63+
oldid: revid,
64+
prop: "sections",
65+
formatversion: 2,
66+
});
67+
return res?.parse?.sections?.at(-1)?.anchor ?? "";
68+
} catch (e) {
69+
console.warn("[RequestDeletion] 读取新讨论串锚点失败", e);
70+
return "";
71+
}
72+
};
73+
74+
class RDWindow extends OO.ui.ProcessDialog {
75+
static static = {
76+
...super.static,
77+
tagName: "div",
78+
name: "lr-rd",
79+
title: wgULS("提删", "提刪"),
80+
actions: [
81+
{
82+
action: "cancel",
83+
label: "取消",
84+
flags: ["safe", "close", "destructive"],
85+
},
86+
{
87+
action: "submit",
88+
label: wgULS("确认", "確認"),
89+
flags: ["primary", "progressive"],
90+
},
91+
],
92+
};
93+
constructor(config) {
94+
// Parent constructor
95+
super(config);
96+
97+
this.storage = config.data.storage;
98+
}
99+
initialize() {
100+
// Parent method
101+
super.initialize();
102+
103+
this.panelLayout = new OO.ui.PanelLayout({
104+
scrollable: false,
105+
expanded: false,
106+
padded: true,
107+
});
108+
this.reasonsDropdown = new OO.ui.DropdownInputWidget({
109+
options: [{
110+
data: "",
111+
label: wgULS("其他(请自行说明理由)", "其他(請自行說明緣由)"),
112+
}, {
113+
data: "",
114+
disabled: true,
115+
label: wgULS("加载中……", "載入中……"),
116+
}],
117+
});
118+
this.detailsText = new OO.ui.TextInputWidget();
119+
this.enterCheckbox = new OO.ui.CheckboxInputWidget({
120+
selected: this.storage.getItem("enterToSubmit"),
121+
});
122+
123+
this.reasonsDropdownMenu = this.reasonsDropdown.dropdownWidget.getMenu();
124+
125+
const reasonsField = new OO.ui.FieldLayout(this.reasonsDropdown, {
126+
label: wgULS("提删理由", "提刪緣由"),
127+
align: "top",
128+
});
129+
const detailsField = new OO.ui.FieldLayout(this.detailsText, {
130+
label: wgULS("详情", "詳情"),
131+
align: "top",
132+
});
133+
const enterField = new OO.ui.FieldLayout(this.enterCheckbox, {
134+
label: wgULS("理由详情按回车提交(浏览器级设置)", "緣由詳情按回車鍵提交(瀏覽器級設置)"),
135+
align: "inline",
136+
});
137+
138+
this.panelLayout.$element.append(
139+
reasonsField.$element,
140+
detailsField.$element,
141+
enterField.$element,
142+
);
143+
144+
this.reasonsDropdownMenu.connect(this, { toggle: "dropdownToggle" });
145+
this.detailsText.connect(this, { enter: "onEnter" });
146+
this.enterCheckbox.connect(this, { change: "setStorage" });
147+
148+
this.$body.append(this.panelLayout.$element);
149+
}
150+
updateReasons(reasons) {
151+
this.reasonsDropdown.setOptions([{
152+
data: "",
153+
label: wgULS("其他(请自行说明理由)", "其他(請自行說明緣由)"),
154+
}, ...reasons]);
155+
}
156+
setStorage(selected) {
157+
this.storage.setItem("enterToSubmit", selected);
158+
}
159+
onEnter() {
160+
if (this.enterCheckbox.isSelected()) {
161+
this.executeAction("submit");
162+
}
163+
}
164+
dropdownToggle(visible) {
165+
if (visible) {
166+
// Manually resize
167+
const newHeight = parseFloat(this.$frame.css("height")) + 300;
168+
this.withoutSizeTransitions(() => {
169+
this.$frame.css("height", newHeight);
170+
this.reasonsDropdownMenu.clip();
171+
$(this.$frame).animate({
172+
height: newHeight,
173+
}, 700);
174+
});
175+
} else {
176+
this.updateSize();
177+
}
178+
}
179+
getBodyHeight() {
180+
return this.panelLayout.$element.outerHeight(true);
181+
}
182+
getReadyProcess(data) {
183+
return super.getReadyProcess(data).next(() => {
184+
this.reasonsDropdown.focus();
185+
}, this);
186+
}
187+
getActionProcess(action) {
188+
if (action === "cancel") {
189+
return new OO.ui.Process(() => {
190+
this.close({ action });
191+
}, this);
192+
} else if (action === "submit") {
193+
return new OO.ui.Process($.when((async () => {
194+
const reason = this.reasonsDropdown.getValue();
195+
const detail = this.detailsText.getValue();
196+
if (!reason && !detail) {
197+
throw new OO.ui.Error(wgULS("请填写理由或详情", "請填寫緣由或詳情"));
198+
}
199+
try {
200+
const anchor = await this.postRequest(buildWikitext(reason || detail, reason ? detail : ""));
201+
this.close({ action });
202+
mw.notify(wgULS("提删请求已发出,3 秒后跳转到讨论串", "提刪請求已發出,3 秒後跳轉到討論串"), {
203+
title: wgULS("提删成功", "提刪成功"),
204+
type: "success",
205+
tag: "lr-rd",
206+
});
207+
setTimeout(() => {
208+
location.assign(anchor === "" ? boardUrl : `${boardUrl}#${encodeURIComponent(anchor)}`);
209+
}, 3000);
210+
} catch (e) {
211+
console.error("[RequestDeletion] Error:", e);
212+
throw new OO.ui.Error(e);
213+
}
214+
})()).promise(), this);
215+
}
216+
// Fallback to parent handler
217+
return super.getActionProcess(action);
218+
}
219+
220+
/**
221+
* 往讨论版发一个新话题,并返回该讨论串的锚点。
222+
*
223+
* @param {string} wikitext 正文 wikitext
224+
* @returns {Promise<string>} 讨论串锚点(不含 `#`);取不到时为空串
225+
*/
226+
postRequest = async (wikitext) => {
227+
const res = await zhAPI.postWithToken("csrf", {
228+
action: "discussiontoolsedit",
229+
paction: "addtopic",
230+
page: BOARD_PAGE,
231+
sectiontitle: buildSectionTitle(),
232+
wikitext,
233+
assertuser: wgUserName,
234+
nocontent: "",
235+
watchlist: "nochange",
236+
tags: "Automation tool",
237+
});
238+
if (res.error) {
239+
throw res.error.code;
240+
}
241+
return fetchNewSectionAnchor(res?.discussiontoolsedit?.newrevid);
242+
};
243+
}
244+
245+
const storage = new LocalObjectStorage("AnnTools-requestDeletion");
246+
247+
const windowManager = new OO.ui.WindowManager();
248+
$body.append(windowManager.$element);
249+
const rdDialog = new RDWindow({
250+
size: "medium",
251+
data: { storage },
252+
});
253+
windowManager.addWindows([rdDialog]);
254+
255+
let loadReason = false;
256+
257+
$(mw.util.addPortletLink("p-cactions", "#", wgULS("提删", "提刪"), "ca-lr-rd", wgULS("向讨论版提出删除请求", "向討論版提出刪除請求"))).on("click", async (e) => {
258+
e.preventDefault();
259+
windowManager.openWindow(rdDialog);
260+
$body.css("overflow", "auto");
261+
if (loadReason === false) {
262+
loadReason = true;
263+
const { parse: { text: { "*": html } } } = await api.post({
264+
action: "parse",
265+
assertuser: wgUserName,
266+
page: wgNamespaceNumber === wgNamespaceIds.file ? "MediaWiki:Filedelete-reason-dropdown" : "MediaWiki:Deletereason-dropdown",
267+
prop: "text",
268+
});
269+
const container = $("<div>");
270+
container.html(html);
271+
const result = [];
272+
const getReason = (ele) => {
273+
result.push({
274+
data: ele.innerText.trim(),
275+
label: ele.innerText.trim(),
276+
});
277+
};
278+
container.children(".mw-parser-output").children("ul").children("li").each((_, ele) => {
279+
const $ele = $(ele);
280+
if ($ele.children("ul").length > 0) {
281+
result.push({
282+
optgroup: $ele.clone().find("*").remove().end().text().trim(),
283+
});
284+
$ele.children("ul").children("li").each((_, e) => getReason(e));
285+
} else {
286+
getReason(ele);
287+
}
288+
});
289+
rdDialog.updateReasons(result);
290+
}
291+
});
292+
} catch (e) {
293+
const parseError = (errLike, _space) => {
294+
let space = _space;
295+
if (_space === void 0) {
296+
space = 4;
297+
}
298+
return JSON.stringify(errLike, (_, v) => {
299+
if (v instanceof Error) {
300+
const stack = [];
301+
if (v.stack) {
302+
Reflect.apply(stack.push, stack, v.stack.split("\n").map((n) => n.trim()).filter((n) => {
303+
let _a;
304+
return ((_a = n === null || n === void 0 ? void 0 : n.length) !== null && _a !== void 0 ? _a : -1) > 0;
305+
}));
306+
}
307+
const keys = Object.keys(v).filter((key) => !Reflect.has(Error.prototype, key));
308+
if (keys.length) {
309+
stack.push(JSON.stringify(Object.fromEntries(keys.map((key) => [key, v[key]])), null, space));
310+
}
311+
return stack.join("\n").trim() || "";
312+
}
313+
return v;
314+
}, space).replace(/^"(.*)"$/, "$1");
315+
};
316+
oouiDialog.alert(`错误信息:<br>${oouiDialog.sanitize(parseError(e))}`, {
317+
title: "提删工具发生错误",
318+
});
319+
console.error("[RequestDeletion] Setup error:", e);
320+
}
321+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
ResourceLoader: true
2+
3+
hidden: false
4+
5+
default: true
6+
7+
supportsUrlLoad: false
8+
9+
skins: []
10+
11+
actions: []
12+
13+
categories: []
14+
15+
namespaces: []
16+
17+
contentModels: []
18+
19+
type: general
20+
21+
package: false
22+
23+
rights:
24+
- edit
25+
- autopatrol
26+
27+
peers: []
28+
29+
dependencies:
30+
- ext.gadget.libOOUIDialog
31+
- ext.gadget.site-lib
32+
- ext.gadget.LocalObjectStorage
33+
- mediawiki.api
34+
- mediawiki.ForeignApi
35+
- mediawiki.util
36+
37+
_sites:
38+
- zh
39+
- commons
40+
41+
_section: user
42+
43+
_files:
44+
- Gadget-requestDeletion.js

0 commit comments

Comments
 (0)