MediaWiki:Gadget-FoxTools-Rollback.js
Basis Pengetahuan Terbuka Wikipedia Indonesia
/**
* [FOXTOOLS — PATROLLERS HELPER SCRIPT]
*
* •==============================================•
* > Pencipta: Janorovic Volkov
* > Pengembang: Janorovic Volkov
* > Tipe: JavaScript (Module)
*
* Lihat [[WP:FT]] untuk informasi selengkapnya
* tentang skrip ini
* •==============================================•
*/
// <nowiki>
mw.loader.load('mediawiki.ui.button');
(function () {
const FT = window.FoxTools || {};
const api = new mw.Api();
function notiOK(msg) { mw.notify ? mw.notify(msg, { type: 'success' }) : alert(msg); }
function notiWARN(msg) { mw.notify ? mw.notify(msg, { type: 'warn' }) : alert(msg); }
function askComment() {
const c = prompt("🦊 FoxTools — Rollback\n\nKlik batal jika ingin membatalkan pengembalian.\nMasukkan komentar (opsional):", "");
if (c === null) return null;
if (c.trim() === "") return "";
return `: ${c.trim()}`;
}
async function foxUndoRollback(page, undoRev, undoAfterRev, summary) {
const params = {
action: 'edit',
title: page,
undo: undoRev,
summary,
minor: true,
tags: 'FoxTools',
redirect: false
};
if (undoAfterRev) params.undoafter = undoAfterRev;
return await api.postWithToken('csrf', params);
}
function findUndoAfter(revs, targetRevId, targetUser) {
const idx = revs.findIndex(r => r.revid === targetRevId);
if (idx === -1) return null;
for (let i = idx + 1; i < revs.length; i++) {
if (revs[i].user !== targetUser) return revs[i].revid;
}
return null;
}
async function rollbackUser(page, targetRevId, user, isANB = false) {
const revs = await fetchRevisions(page, 'max');
const idx = revs.findIndex(r => r.revid === targetRevId);
if (idx === -1) return;
let undoRev = targetRevId;
let undoAfter = null;
for (let i = idx + 1; i < revs.length; i++) {
if (revs[i].user !== user) {
undoAfter = revs[i].revid;
break;
}
}
let editCount = 0;
for (let i = idx; i < revs.length; i++) {
if (revs[i].user === user) editCount++;
else break;
}
const comment = askComment();
if (comment === null) return;
const targetUserRev = undoAfter ? await getUserOfRevision(undoAfter) : null;
const summary = isANB
? summaryANBToTarget(page, user, editCount, targetUserRev, comment)
: summaryNormalToTarget(page, user, editCount, targetUserRev, comment);
await foxUndoRollback(page, undoRev, undoAfter, summary);
notiOK(`🟢 Pengembalian revisi berhasil: ${user} (${editCount})`);
location.reload();
}
async function getUserOfRevision(revid) {
const res = await api.get({
action: 'query',
prop: 'revisions',
revids: revid,
rvprop: 'user',
formatversion: 2
});
const page = res.query.pages[0];
const rev = page.revisions && page.revisions[0];
return rev ? rev.user : null;
}
async function getLatestRevId(page) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: page,
rvlimit: 1,
rvprop: 'ids',
formatversion: 2
});
const pageObj = res.query.pages && res.query.pages[0];
return pageObj && pageObj.revisions && pageObj.revisions[0] && pageObj.revisions[0].revid;
}
async function fetchRevisions(page, limit = 'max') {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: page,
rvlimit: limit,
rvprop: 'ids|user|timestamp',
formatversion: 2
});
const revs = (res.query.pages && res.query.pages[0] && res.query.pages[0].revisions) || [];
return revs;
}
function findLastGoodRevIdFromRevisions(revs, targetRevId, targetUser) {
let idx = revs.findIndex(r => r.revid === targetRevId);
if (idx === -1) return null;
for (let i = idx + 1; i < revs.length; i++) {
if (revs[i].user !== targetUser) {
return revs[i].revid;
}
}
return null;
}
function countConsecutiveEdits(revs, targetRevId, targetUser) {
let idx = revs.findIndex(r => r.revid === targetRevId);
if (idx === -1) return 1;
let count = 0;
for (let i = idx; i < revs.length; i++) {
if (revs[i].user === targetUser) count++;
else break;
}
return Math.max(1, count);
}
function summaryNormalToTarget(page, sourceUser, count, targetUser, comment) {
if (count && count > 1) {
return `Mengembalikan ${count} suntingan oleh [[Istimewa:Kontribusi/${sourceUser}|${sourceUser}]] ([[User talk:${sourceUser}|bicara]]) ke revisi terakhir oleh [[Istimewa:Kontribusi/${targetUser}|${targetUser}]]${comment} (${FT.ads})`;
}
return `Mengembalikan suntingan oleh [[Istimewa:Kontribusi/${sourceUser}|${sourceUser}]] ([[User talk:${sourceUser}|bicara]]) ke revisi terakhir oleh [[Istimewa:Kontribusi/${targetUser}|${targetUser}]]${comment} (${FT.ads})`;
}
function summaryANBToTarget(page, sourceUser, count, targetUser, comment) {
if (count && count > 1) {
return `Membatalkan ${count} [[WP:ANB|suntingan berniat baik]] oleh [[Istimewa:Kontribusi/${sourceUser}|${sourceUser}]] ([[User talk:${sourceUser}|bicara]]) ke revisi terakhir oleh [[Istimewa:Kontribusi/${targetUser}|${targetUser}]]${comment} (${FT.ads})`;
}
return `Membatalkan [[WP:ANB|suntingan berniat baik]] oleh [[Istimewa:Kontribusi/${sourceUser}|${sourceUser}]] ([[User talk:${sourceUser}|bicara]]) ke revisi terakhir oleh [[Istimewa:Kontribusi/${targetUser}|${targetUser}]]${comment} (${FT.ads})`;
}
function summaryRestoreToTarget(page, revId, targetUser, comment) {
return `Memulihkan ke revisi [[Istimewa:Diff/${revId}|${revId}]] oleh [[Istimewa:Kontribusi/${targetUser}|${targetUser}]]${comment} (${FT.ads})`;
}
function mkButton(text, classes = '', title = '') {
const btn = document.createElement('button');
btn.className = 'mw-ui-button ' + classes;
btn.textContent = text;
if (title) btn.title = title;
return btn;
}
function attachButtonsToListItem($li, page, revid, user) {
if ($li.find('.foxtools-rollback-panel').length) return;
const panel = document.createElement('span');
panel.className = 'foxtools-rollback-panel';
panel.style.marginLeft = '8px';
panel.style.display = 'inline-flex';
panel.style.gap = '6px';
const btnANB = mkButton('Kembalikan (ANB)', 'mw-ui-button mw-ui-progressive');
const btnNormal = mkButton('Kembalikan', 'mw-ui-button mw-ui-destructive');
btnANB.onclick = async () => {
const user = await getUserOfRevision(revid);
await rollbackUser(page, revid, user, true);
};
btnNormal.onclick = async () => {
const user = await getUserOfRevision(revid);
await rollbackUser(page, revid, user, false);
};
panel.appendChild(btnANB);
panel.appendChild(btnNormal);
const $tag = $li.find('.mw-tag-markers').first();
if ($tag.length) {
$tag.before(panel);
} else {
$li.append(panel);
}
}
function attachHistoryButtons($row, page, revid, user) {
if ($row.find('.foxtools-rollback-panel-hist').length) return;
const panel = document.createElement('span');
panel.className = 'foxtools-rollback-panel-hist';
panel.style.marginLeft = '8px';
panel.style.display = 'inline-flex';
panel.style.gap = '6px';
const btnANB = mkButton('Kembalikan (ANB)', 'mw-ui-button mw-ui-progressive');
const btnNormal = mkButton('Kembalikan', 'mw-ui-button mw-ui-destructive');
btnANB.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
const user = await getUserOfRevision(revid);
await rollbackUser(page, revid, user, true);
};
btnNormal.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
const user = await getUserOfRevision(revid);
await rollbackUser(page, revid, user, false);
};
panel.appendChild(btnANB);
panel.appendChild(btnNormal);
const $tag = $row.find('.mw-tag-markers').first();
if ($tag.length) {
$tag.before(panel);
} else {
$row.append(panel);
}
}
async function addFoxRollbackButtons() {
const pageType = mw.config.get('wgCanonicalSpecialPageName');
if (!['Contributions', 'IPContributions'].includes(pageType)) return;
const cfg = mw.config.get();
$('li[data-mw-revid]').each(async function () {
const $li = $(this);
const revid = $li.data('mw-revid');
if (!revid) return;
let page = mw.config.get('wgPageName');
const $titleEl = $li.find('.mw-contributions-title, .mw-title, .mw-contrib-title').first();
if ($titleEl.length) {
const t = $titleEl.text().trim();
if (t) page = t.replace(/ /g, '_');
}
let targetUser = mw.config.get('wgRelevantUserName') || '';
const $userlink = $li.find('.mw-userlink, .mw-contrib-username, .mw-contributions-username').first();
if ($userlink.length) targetUser = $userlink.text().trim() || targetUser;
if (!targetUser) return;
if ($li.data('foxtools-processed')) return;
let latestRevId = null;
try {
latestRevId = await getLatestRevId(page);
} catch (err) {
console.warn('Gagal mendapatkan revisi terbaru untuk', page, err);
return;
}
if (latestRevId !== revid) return;
let revs = [];
try {
revs = await fetchRevisions(page, 50);
} catch (err) {
console.warn('Gagal fetch revisions', err);
}
const lastGoodRevId = findLastGoodRevIdFromRevisions(revs, revid, targetUser);
const editCount = countConsecutiveEdits(revs, revid, targetUser);
attachButtonsToListItem($li, page, revid, targetUser, editCount, lastGoodRevId);
$li.data('foxtools-processed', true);
});
}
async function addFoxRollbackInHistory() {
if (mw.config.get('wgAction') !== 'history') return;
const page = mw.config.get('wgPageName');
const $rows = $('li[data-mw-revid]');
if (!$rows.length) return;
let latest;
try {
latest = await getLatestRevId(page);
} catch (err) {
console.warn('Gagal mendapatkan revisi terbaru', err);
return;
}
const $top = $rows.first();
const topRevid = $top.data('mw-revid');
const topUser = $top.find('.mw-userlink').text().trim();
if (topRevid === latest) {
let revs = [];
try {
revs = await fetchRevisions(page, 'max');
} catch (err) {
console.warn('Gagal fetch revisions for history', err);
}
const lastGoodTop = findLastGoodRevIdFromRevisions(revs, topRevid, topUser);
attachHistoryButtons($top, page, topRevid, topUser, lastGoodTop);
} else {
if ($top.data('foxtools-restore-added')) return;
$top.data('foxtools-restore-added', true);
const revid = topRevid;
const restorePanel = document.createElement('span');
restorePanel.className = 'foxtools-restore';
restorePanel.style.marginLeft = '8px';
const btnRestore = mkButton('Pulihkan revisi', 'mw-ui-button mw-ui-progressive');
btnRestore.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
try {
const comment = askComment();
if (comment === null) return;
const targetUser = await getUserOfRevision(revid);
const summary = summaryRestoreToTarget(page, revid, targetUser, comment);
await foxUndoRollback(page, latest, revid, summary);
notiOK(`🟢 Halaman berhasil dipulihkan ke revisi ${revid}`);
location.reload();
} catch (err) {
console.error(err);
notiWARN(`⚠️ Gagal memulihkan ke revisi ${revid}`);
}
};
restorePanel.appendChild(btnRestore);
const $tag = $top.find('.mw-tag-markers').first();
if ($tag.length) {
$tag.before(restorePanel);
} else {
$top.append(restorePanel);
}
}
$rows.each(function () {
const $row = $(this);
if ($row.is($top)) return;
if ($row.find('.foxtools-restore-only').length) return;
const revid = $row.data('mw-revid');
const restorePanel = document.createElement('span');
restorePanel.className = 'foxtools-restore-only';
restorePanel.style.marginLeft = '8px';
const btnRestore = mkButton('Pulihkan revisi', 'mw-ui-button mw-ui-progressive');
btnRestore.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
try {
const comment = askComment();
if (comment === null) return;
const targetUser = await getUserOfRevision(revid);
const summary = summaryRestoreToTarget(page, revid, targetUser, comment);
await foxUndoRollback(page, latest, revid, summary);
notiOK(`🟢 Halaman berhasil dipulihkan ke revisi ${revid}`);
location.reload();
} catch (err) {
console.error(err);
notiWARN(`⚠️ Gagal memulihkan ke revisi ${revid}`);
}
};
restorePanel.appendChild(btnRestore);
const $tag = $row.find('.mw-tag-markers').first();
if ($tag.length) {
$tag.before(restorePanel);
} else {
$row.append(restorePanel);
}
});
}
async function attachDiffButtons() {
const page = mw.config.get('wgPageName');
const diffId = Number(mw.util.getParamValue('diff'));
const oldId = Number(mw.util.getParamValue('oldid'));
if (!diffId) return;
const latest = await getLatestRevId(page);
const waitForDiff = () => new Promise(resolve => {
const el = document.querySelector('#mw-content-text .diff');
if (el) return resolve(el);
const obs = new MutationObserver((mutations, observer) => {
const el2 = document.querySelector('#mw-content-text .diff');
if (el2) {
observer.disconnect();
resolve(el2);
}
});
obs.observe(document.body, { childList: true, subtree: true });
});
const diffElement = await waitForDiff();
let panel = document.querySelector('.foxtools-diff-inline');
if (!panel) {
panel = document.createElement('div');
panel.className = 'foxtools-diff-inline';
Object.assign(panel.style, {
display: 'flex',
gap: '6px',
marginBottom: '6px'
});
diffElement.parentNode.insertBefore(panel, diffElement);
} else {
panel.innerHTML = '';
}
const makeBtn = (label, mwClass, onClick) => {
const btn = document.createElement('div');
btn.textContent = label;
btn.className = mwClass;
Object.assign(btn.style, {
cursor: 'pointer',
padding: '4px 8px',
textAlign: 'center'
});
btn.onclick = onClick;
return btn;
};
if (diffId === latest) {
const btnANB = makeBtn('Kembalikan (ANB)', 'mw-ui-button mw-ui-progressive');
const btnNormal = makeBtn('Kembalikan', 'mw-ui-button mw-ui-destructive');
btnANB.onclick = async () => {
const user = await getUserOfRevision(diffId);
await rollbackUser(page, diffId, user, true);
};
btnNormal.onclick = async () => {
const user = await getUserOfRevision(diffId);
await rollbackUser(page, diffId, user, false);
};
panel.appendChild(btnANB);
panel.appendChild(btnNormal);
} else {
const btnRestore = makeBtn('Pulihkan revisi', 'mw-ui-button mw-ui-progressive', async () => {
const comment = askComment();
if (comment === null) return;
const targetUser = await getUserOfRevision(diffId);
const summary = summaryRestoreToTarget(page, diffId, targetUser, comment);
await foxUndoRollback(page, latest, diffId, summary);
notiOK(`🟢 Halaman berhasil dipulihkan ke revisi ${diffId}`);
location.reload();
});
panel.appendChild(btnRestore);
}
}
const module = {
name: 'Rollback',
init() {
$(addFoxRollbackButtons);
$(addFoxRollbackInHistory);
$(attachDiffButtons);
const observer = new MutationObserver(() => {
try {
addFoxRollbackButtons();
addFoxRollbackInHistory();
attachDiffButtons();
} catch (e) {}
});
observer.observe(document.body, { childList: true, subtree: true });
console.log('[FoxTools] Rollback module initialized.');
},
openPanel() {}
};
FT.register && FT.register('Rollback', module);
})();
// </nowiki>