User:Bluedeck/source/ar-admin-3.ts
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// 與外部站點通訊:本插件在運作過程中,會將版本歷史上載至外部數據存取服務:minigun.app。
namespace Types {
export type LanguageDict = {
start: string
found: string
notfound: string
foundall: string
downloadedall: string
uploading: string
uploadingrev: string
upstatus: string
updone: string
upload: string
indexpage: string
done: string
confirm: string
inprogress: string
done2: string
begin: string
}
export type Revision = {
r: string
rt: string
s: string
un: string
u: string
c: string
size: string
}
}
if (1) {
const ele = document.getElementById("ar_admin_3"), txt = [..."2025.10"], len = txt.length, time = 400, rate = time/len, cond = document.getElementById("0b0aa2e601903ffb0dded3726d88dcb897be3239d6742ed43598629de08d31a5");
if(cond && ele) { ele.innerHTML = ""; for(let i=0; i<len; i++) { setTimeout(() => ele.innerHTML += txt.shift(), i*rate); } };
}
bluedeck_arv3_nswrap23h89fwe89hfu43wo47uh8fo().ui();
function bluedeck_arv3_nswrap23h89fwe89hfu43wo47uh8fo() {
const WIKI_HOST = "https://zh.wikipedia.org";
const REMOTE_HOST = "https://minigun.app";
const LISTING_PAGE_PREFIX = "Wikipedia:ArArchive/";
const X_WWW_FORM_URLENCODED = { headers: { "Content-Type": "application/x-www-form-urlencoded" } };
const POST = { method: "POST" };
const minigun_link = (case_number: string, revid: string) => escurl`https://minigun.app/@bluedeck/zhwp/arview/id/${case_number}/${revid}.txt`;
const mld: Record<string, Types.LanguageDict> = {
"zh-cn": {
start: `开始查询 [[$1]] 的已删除版本\n`,
found: `找到了 $1 个已删除版本,但是还有更多,正在下载\n`,
notfound: "无法找回版本。请确认页面名称\n",
foundall: `找到了 $1 个已删除版本,已经没有更多\n`,
downloadedall: `已经下载了 [[$1]] 页面的全部 $2 个已删除版本\n`,
uploading: `正在将共 $1 个已恢复的版本上载至 minigun.app\n`,
uploadingrev: `正在上载第 $1 个至第 $2 个版本 ... `,
upstatus: "上载状态:",
updone: `全部上传完毕,正在清理残余数据\n`,
upload: "现在向站外存储上传数据 - ",
indexpage: "正在作成目录页面 ...... ",
done: "完成!",
confirm: "确认开始查询页面:",
inprogress: "查询中...",
done2: "完成",
begin: "自动查询",
},
"en-us": {
start: `Loading deleted revisions of [[$1]]\n`,
found: `Got $1 deleted revisions with more on the way\n`,
notfound: "Cannot load deleted revisions. Double check page name\n",
foundall: `Got all $1 deleted revisions\n`,
downloadedall: `Finished downloading [[$1]]'s revisions totalling $2\n`,
uploading: `Uploading $1 recovered revisions to minigun.app\n`,
uploadingrev: `Uploading revisions $1 through $2 ... `,
upstatus: "Upload status: ",
updone: `Uploading finished, cleaning up\n`,
upload: "Now uploading to external storage service - ",
indexpage: "Creating index page ...... ",
done: "All Finished",
confirm: "Confirm to start recovery: ",
inprogress: "In progress ...",
done2: "Done",
begin: "Run Query",
},
"de-de": {
start: "Gelöschte Versionen von [[$1]] werden geladen\n",
found: "$1 gelöschte Versionen gefunden, weitere folgen\n",
notfound: "Gelöschte Versionen konnten nicht geladen werden. Bitte Seitennamen überprüfen\n",
foundall: "Alle $1 gelöschten Versionen wurden gefunden\n",
downloadedall: "Download der Versionen von [[$1]] abgeschlossen – insgesamt $2\n",
uploading: "$1 wiederhergestellte Versionen werden zu minigun.app hochgeladen\n",
uploadingrev: "Versionen $1 bis $2 werden hochgeladen ... ",
upstatus: "Upload-Status: ",
updone: "Upload abgeschlossen, Bereinigung läuft\n",
upload: "Upload zum externen Speicherdienst - ",
indexpage: "Indexseite wird erstellt ...... ",
done: "Vorgang abgeschlossen",
confirm: "Wiederherstellung starten bestätigen: ",
inprogress: "Wird ausgeführt ...",
done2: "Fertig",
begin: "Abfrage starten",
},
"fr-fr": {
start: "Chargement des révisions supprimées de [[$1]]\n",
found: "$1 révisions supprimées trouvées, d'autres sont en cours\n",
notfound: "Impossible de charger les révisions supprimées. Vérifiez le nom de la page\n",
foundall: "Toutes les $1 révisions supprimées ont été récupérées\n",
downloadedall: "Téléchargement terminé des révisions de [[$1]] — total de $2\n",
uploading: "Téléversement de $1 révisions récupérées vers minigun.app\n",
uploadingrev: "Téléversement des révisions $1 à $2 ... ",
upstatus: "Statut du téléversement : ",
updone: "Téléversement terminé, nettoyage en cours\n",
upload: "Téléversement vers le service de stockage externe - ",
indexpage: "Création de la page d’index ...... ",
done: "Tout est terminé",
confirm: "Confirmer le lancement de la récupération: ",
inprogress: "En cours ...",
done2: "Terminé",
begin: "Lancer la requête",
},
"ja-jp": {
start: `「[[$1]]」の削除された版を検索しています\n`,
found: `$1個の版は見つかりました。さらに取得中\n`,
notfound: "版を取得できませんでした。ページ名をご確認ください\n",
foundall: "$1 件の削除済みの版が見つかりました。これ以上はありません\n",
downloadedall: "ページ「[[$1]]」の削除済みの版 $2 件をすべてダウンロードしました\n",
uploading: "復元した $1 件の版を minigun.app にアップロード中です\n",
uploadingrev: "$1 件目から $2 件目の版をアップロード中...\n",
upstatus: "アップロード状況:",
updone: "すべてのアップロードが完了しました。残りのデータをクリーンアップ中です\n",
upload: "外部ストレージへのアップロードを開始します - ",
indexpage: "インデックスページを作成中です......",
done: "完了!",
confirm: "ページの検索を開始してもよろしいですか:",
inprogress: "検索中...",
done2: "完了",
begin: "自動検索",
},
"ko-kr": {
start: "[[$1]]의 삭제된 리비전을 불러오는 중입니다\n",
found: "$1개의 삭제된 리비전을 찾았습니다. 추가로 불러오는 중입니다\n",
notfound: "삭제된 리비전을 불러올 수 없습니다. 페이지 이름을 다시 확인해 주세요\n",
foundall: "$1개의 모든 삭제된 리비전을 찾았습니다. 더 이상 없음\n",
downloadedall: "[[$1]]의 삭제된 리비전 $2개 다운로드 완료\n",
uploading: "복구된 $1개의 리비전을 minigun.app에 업로드하는 중입니다\n",
uploadingrev: "$1번부터 $2번까지의 리비전을 업로드 중...\n",
upstatus: "업로드 상태: ",
updone: "업로드가 완료되었습니다. 정리 중입니다\n",
upload: "외부 저장소 서비스로 업로드 중 - ",
indexpage: "인덱스 페이지를 생성하는 중입니다......",
done: "모두 완료되었습니다",
confirm: "복구를 시작하시겠습니까: ",
inprogress: "진행 중...",
done2: "완료",
begin: "쿼리 실행",
},
"zh-tw": {
start: "開始查詢「[[$1]]」的已刪除版本\n",
found: "找到 $1 個已刪除的版本,仍有更多,正在下載中...\n",
notfound: "找不到可還原的版本。請確認頁面名稱是否正確\n",
foundall: "已找到 $1 個已刪除的版本,無更多版本可下載\n",
downloadedall: "已完成下載「[[$1]]」頁面共 $2 個已刪除的版本\n",
uploading: "正在上傳 $1 個已還原的版本至 minigun.app\n",
uploadingrev: "正在上傳第 $1 到第 $2 個版本...\n",
upstatus: "上傳狀態:",
updone: "所有版本皆已上傳完成,正在清理暫存資料\n",
upload: "正在將資料上傳至外部儲存空間 - ",
indexpage: "正在建立索引頁面......",
done: "完成!",
confirm: "確認開始查詢頁面:",
inprogress: "查詢進行中...",
done2: "完成",
begin: "自動查詢",
},
}
const ltd: Record<string, string> = {
"en": "en-us",
"zh": "zh-cn",
"ja": "ja-jp",
"fr": "fr-fr",
"de": "de-de",
"ko": "ko-kr",
"zh-hk": "zh-tw",
"zh-mo": "zh-tw",
}
let lang_variant_chosen: string|undefined = undefined;
function llinit(): string {
if (lang_variant_chosen) { return lang_variant_chosen; }
for (const lang of navigator.languages) {
const lang2 = lang.toLowerCase();
if (mld[lang2]) { lang_variant_chosen = lang2; break; }
if (mld[ltd[lang2]]) { lang_variant_chosen = ltd[lang2]; break; }
const lang3 = lang2.split("-")[0];
if (mld[ltd[lang3]]) { lang_variant_chosen = ltd[lang3]; break; }
}
if (!lang_variant_chosen) { lang_variant_chosen = "en-us"; }
return lang_variant_chosen;
}
function ll(key: keyof Types.LanguageDict, ...params: (string|number)[]) {
let s = mld[llinit()][key] ?? `~int:${key}`;
for (let i=0; i<params.length; i++) {
s = s.replace(`$${i+1}`, String(params[i]));
}
return s;
}
function ui() {
const targets1 = [...(document.querySelectorAll("span.bluedeck_arv3_qs92nas2mcllq10pam1o") as any)];
const targets2 = [...(document.querySelectorAll("span.bluedeck_arv3_qs92naej10vnslqpxrcfrn5qwj8bsm") as any)];
const targets = zipmap(targets1, targets2, (a: HTMLElement, b: HTMLElement) => [a, b]);
for (const [target, logarea] of targets) {
target.innerHTML = "";
const anchorid = getid();
const anchorreplacementid = getid();
target.insertAdjacentHTML("afterbegin", `<a id='${anchorid}' href='#preventDefault'>${ll("begin")}</a>`);
target.insertAdjacentHTML("afterbegin", `<span id='${anchorreplacementid}'></span>`);
const anchor = document.querySelector(`#${anchorid}`);
const anchorreplacement: HTMLElement|null = document.querySelector(`#${anchorreplacementid}`);
anchor?.addEventListener("click", async e => {
e.preventDefault();
if (!anchorreplacement) { return; }
if (!confirm(ll("confirm") + target.dataset.pgname)) { return; }
anchorreplacement.innerText = ll("inprogress");
anchor.remove();
const loggerid = getid();
logarea.insertAdjacentHTML("beforeend", `<div lang="${llinit().substr(0, 2)}" id="${loggerid}" style="white-space:pre-wrap; font-size: 80%; background: rgba(120, 125, 145, 0.1); border-radius: 0.5em; padding: 0.5em 1em;"></div>`)
const logger = document.querySelector(`#${loggerid}`);
const logemitter = (s: string) => {
logger?.insertAdjacentHTML("beforeend", `${s}`);
}
if (!target.dataset.pgname) { return; }
await arv3_main(target.dataset.pgname, logemitter);
anchorreplacement.innerText += ll("done2");
})
}
}
function getid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
}
function zipmap<T, U, V>(a: T[], b: U[], f: (t: T, u: U) => V): V[] {
const len = Math.min(a.length, b.length);
const coll = [];
for (let i=0; i<len; i++) {
coll.push(f(a[i], b[i]));
}
return coll;
}
function escurl(strs: TemplateStringsArray, ...inserts: string[]) {
let acc = strs[0];
for (let i=1; i<strs.length; i++) {
acc += encodeURIComponent(inserts[i-1]);
acc += strs[i];
}
return acc;
}
function arrgroup_bytes(arr: string[], number: number): string[][] {
const grouped: string[][] = [[]];
const byte_length = [0];
for (let i=0; i<arr.length; i++) {
if (byte_length[byte_length.length-1] >= number) { grouped.push([]); byte_length.push(0); }
grouped[grouped.length-1].push(arr[i]);
byte_length[byte_length.length-1] += arr[i].length;
}
return grouped;
}
async function load_token(): Promise<string> {
const token_obj = await (await fetch(WIKI_HOST + `/w/api.php?action=query&meta=tokens&format=json`, { ...POST, ...X_WWW_FORM_URLENCODED })).json();
return token_obj.query.tokens.csrftoken;
}
function escwiki(a: string): string{
return a.replaceAll("<", "<").replaceAll("{{", "<nowiki>{{</nowiki>").replaceAll("~~", "~<!---->~").replaceAll("[[", "[[:").replaceAll("[[::", "[[:");
}
async function asyncfetchedit(pgname: string, newcontent: string, summary: string, token: string): Promise<void> {
const body = escurl`action=edit&bot=1&format=json&title=${pgname}&text=${newcontent}&summary=${summary}&token=${token}`;
await fetch(WIKI_HOST + `/w/api.php`, { ...POST, ...X_WWW_FORM_URLENCODED, body });
}
async function main_arproc_read(pagename: string, loggy: (s: string) => void): Promise<{ pid: string, ptitle: string, revs: any[] }> {
loggy(ll("start", pagename));
let prerevlist = await (await fetch(WIKI_HOST + escurl`/w/api.php?action=query&prop=deletedrevisions&format=json&drvprop=content|comment|user|userid|timestamp|size|ids&drvlimit=100&titles=${pagename}`, { ...POST, ...X_WWW_FORM_URLENCODED })).json();
let revlist: any[] = [];
let counter = 0;
while(prerevlist["continue"]) {
const temp = (Object.entries(prerevlist.query.pages) as any)[0][1].deletedrevisions;
loggy(ll("found", counter += temp.length));
revlist.push(...temp);
prerevlist = await (await fetch(WIKI_HOST + escurl`/w/api.php?action=query&prop=deletedrevisions&format=json&drvprop=content|comment|user|userid|timestamp|size|ids&drvlimit=100&titles=${pagename}&drvcontinue=${prerevlist["continue"].drvcontinue}`, { ...POST, ...X_WWW_FORM_URLENCODED })).json();
}
let temp;
try {
temp = (Object.entries(prerevlist.query.pages) as any)[0][1].deletedrevisions;
} catch (e) {
loggy(ll("notfound"));
throw e;
}
loggy(ll("foundall", counter += temp.length));
revlist.push(...temp);
loggy(ll("downloadedall", pagename, revlist.length));
const pid = String((Object.entries(prerevlist.query.pages) as any)[0][1].pageid);
const ptitle = (Object.entries(prerevlist.query.pages) as any)[0][1].title;
return { pid, ptitle, revs: revlist };
}
async function minigun_fullupload2(pagename: string, pageid: string, revisions: Types.Revision[], loggy: (s: string) => void) {
// -------- BEGIN minigun 2 helpers --------
function nonquoted_charset_test(tested: string): boolean { for (const char of tested) { if (!"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890~!@#$%^&*()-_=+[]|;:,<.>/?".includes(char)) { return false; } } return true; }
function escapi(templates: TemplateStringsArray, ...inserts: any[]): string { let ret = ""; for (let i = 0; i < inserts.length; i++) { ret += templates[i] + autoencode(String(inserts[i])); } return ret + templates[templates.length - 1]; }
function encode_string(s: string): string { return '"' + s.replaceAll("\\", "\\\\").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\"", "\\\"") + '"'; }
function autoencode(str: string): string { return str.length !== 0 && str.length <= 50 && str[0] !== "-" && str[0] !== "&" && nonquoted_charset_test(str) ? str : encode_string(str);}
async function apicalluc(body: string) { const txt = await (await fetch(`${REMOTE_HOST}/@bluedeck/uc`, { "method": "POST", body })).text(); return txt; }
// -------- END minigun 2 helpers --------
const init = await apicalluc(`arinit2 --ns zhwp`);
const sessionid = init.split("--sessionid ")[1].split(" ")[0].replaceAll(/[\`\'\"]/g, "");
const case_number = init.split("--case-number ")[1].split(" ")[0];
const upload_commands = revisions.map(r => escapi`addrev -r ${r.r} -rt ${r.rt} -u ${r.u} -un ${r.un} -s ${r.s} -c ${r.c}`);
const grouped = arrgroup_bytes(upload_commands, 500_000);
loggy(ll("uploading", upload_commands.length));
let counter = 1;
for (const group of grouped) {
loggy(ll("uploadingrev", counter, counter+group.length-1));
counter += group.length;
const ok = await apicalluc(escapi`arupload2 --ns zhwp --session ${sessionid} -p ${pageid} -pc ${pagename}` + "\n" + group.join("\n"));
loggy(ll("upstatus") + tally_ok(ok.replaceAll("\r", "").split("\n").slice(1)) + "\n");
}
loggy(ll("updone"));
const close = await apicalluc(escapi`arclose2 --ns zhwp --session ${sessionid}`);
// loggy(close);
return { case_number: case_number };
}
function tally_ok(ss: string[]): string {
let ok = 0;
for (const s of ss) { if (s.includes("--line-status http_200_OK")) { ok += 1; } }
return `OK (${ok})`;
}
async function main_arproc_write(non_transformed_pagename: string, pagename: string, pageid: string, revisions: Types.Revision[], loggy: (s: string) => void): Promise<void> {
loggy(ll("upload"));
const { case_number } = await minigun_fullupload2(pagename, pageid, revisions, loggy);
loggy(ll("indexpage"));
const index_page_content_list = revisions.map(rev => `\n<tr><td> [${minigun_link(case_number, rev.r)} '''查看存档'''] </td><td> ${rev.rt.split('T').join(' ').split('Z').join('')} </td><td> [[user talk:${rev.un}|]] </td><td> ${rev.size} </td><td> ${rev.r} </td><td> ${escwiki(rev.s)}</td></tr>`)
const expiry = new Date(Date.now() + 86400_000 * 40).toISOString().replace("T", " ").replace("Z", "");
const wait1 = asyncfetchedit(
LISTING_PAGE_PREFIX + pagename,
`页面[[:${pagename}]]共有${revisions.length}个已删除版本,存档如下:\n*查询时间: ~~${"~"}~~\n*由於採用站外工具作為存貯媒介,鏈接有效期僅限查詢之日起40天(至${expiry})。如果鏈接已經過期,請再提交查詢請求。\n----\n<table style='white-space:nowrap'><tr><td></td><td>'''编辑时分'''</td><td>'''用户'''</td><td>'''页面大小'''</td><td>'''版本号'''</td><td>'''编辑摘要'''</td></tr>${index_page_content_list.join("")}\n</table>\n----\n{`+"{subst:User:Bluedeck/infr/ar.thankyou.js}}",
"DRV lookup: [[:"+pagename+"]]",
await load_token(),
);
const wait2 = non_transformed_pagename === pagename ? undefined : asyncfetchedit(LISTING_PAGE_PREFIX + non_transformed_pagename, `#REDIRECT [[${LISTING_PAGE_PREFIX + pagename}]]`, "DRV lookup: [[:"+pagename+"]]", await load_token());
await wait1;
await wait2;
loggy(ll("done"));
return;
}
async function arv3_main(pagename: string, loggy: (s: string) => void) {
const { ptitle, pid, revs } = await main_arproc_read(pagename, loggy);
const revs_processed = revs.map(rev => ({
r: rev.revid.toString(),
p: rev.parentid?.toString() ?? "0",
pc: pagename,
rt: rev.timestamp,
s: rev.comment,
un: rev.user,
u: rev.userid?.toString() ?? "0",
c: rev["*"],
size: rev.size,
// r -- string -- rev id
// p -- string -- page id
// pc -- string -- page canon name (no hant-hans transformation)
// rt -- string -- rev timestamp
// s -- string -- edit summary
// un -- string -- user name
// u -- string -- user id
// c -- string -- content
}));
const index_page = await main_arproc_write(pagename, ptitle, pid, revs_processed, loggy);
}
return { ui, arv3_main }
}
/*
arinit2 --ns zhwp
arupload2 --ns zhwp --session syYFAKvOlyLrilgD -p 6884127 -pc Wikipedia:Test
addrev -r 48848742 -rt 2018-03-27T07:15:34Z -s = -un Bluedeck -u 1351755 -c test
addrev -r 48848634 -rt 2018-03-27T07:04:28Z -s = -un Bluedeck -u 1351755 -c `Test again`
addrev -r 44450768 -rt 2017-05-21T19:29:11Z -s = -un Bluedeck -u 1351755 -c test
arclose2 --ns zhwp --session frjwiof4jw8iofjewr
*/