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>
Dinas Kependudukan dan Pencatatan Sipil
Pemerintah Kota
Samarinda
Kontak
© 2026 Pemerintah Kota SamarindaAll Rights Reserved
Dikembangkan olehEnter(Wind)