User:WhitePhosphorus/js/AutoUndo.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* A fork of https://publictestwiki.com/wiki/User:%E9%80%86%E8%A5%B2%E7%9A%84%E5%A4%A9%E9%82%AA%E9%AC%BC/AutoUndo.js
 * @author [[User:逆襲的天邪鬼]], [[User:WhitePhosphorus]]
 * 
 * See also [[User:WhitePhosphorus/js/AutoUndoGlobal.js]] (work globally, not only on the wiki you activate the script)
 *
 * If your wiki does not use { {delete}} as speedy template:
 * Copy the following snippet to your global.js, then replace '<wikidbname>' and '<speedytemplatename>'
 * Check https://noc.wikimedia.org/conf/highlight.php?file=dblists/all.dblist for the <wikidbname> list. eg: English Wikipedia is enwiki, Meta Wiki is metawiki
   	p4js_auto_undo_csd_template = {
		// default: 'delete'
		'commonswiki': 'speedy',  // example, no braces are needed in template name
		'<wikidbname>': '<speedytemplatename>',  // edit this line!
	};
 */

$(function () {
mw.loader.using(['mediawiki.util'], function() {
    var translations = {
        portletlink: {
            en: 'Auto undo',
            zh: '自動跟蹤與回退編輯'
        },
        introduction: {
            en: 'For IP ranges, only /16 or /24 is supported. Please use UPPER CASE for IPv6 addresses. Only the new edits will be reverted after you click [start], please manually revert previous edits if needed.',
            zh: '用户支持 IP 段,支持 /16 和 /24。IPv6 请使用大写字母。確定之後將會跟蹤此用戶的編輯,並將其全部回退。(之前的編輯請手動回退)'
        },
        username: {
            en: 'Username: ',
            zh: '用户名:'
        },
        showname: {
            en: 'Show username in rollback summary',
            zh: '在回退摘要中显示用户名'
        },
        shownamedesp: {
            en: ' (Useful if the username deserves a revision deletion locally)',
            zh: '(通常只应该隐藏在本地需要被修订版本删除的用户名)'
        },
        newpage: {
            en: 'Actions on new page creation: ',
            zh: '新建页面的处理:'
        },
        speedydelete: {
            en: 'Empty the page and request speedy deletion (Vandalism)',
            zh: '清空页面并请求快速删除(破坏)'
        },
        empty: {
            en: 'Empty the page',
            zh: '清空页面'
        },
        nothing: {
            en: 'Do nothing',
            zh: '不处理'
        },
        start: {
            en: '[Start]',
            zh: '[开始]'
        },
        undid: {
            en: 'Successfully undid on page ',
            zh: '已撤銷頁面 '
        },
        cannotundo: {
            en: 'Cannot undo on page ',
            zh: '無法撤銷頁面 '
        },
        revision: {
            en: ', revision id ',
            zh: ' 的修訂版本 '
        },
        newedit: {
            en: 'New edit: ',
            zh: '新編輯:'
        },
        stopped: {
            en: 'Stopped.',
            zh: '已停止。'
        },
        newwindow: {
            en: 'Please open a new window to monitor a new target.',
            zh: '請開新視窗。'
        },
        monitoring: {
            en: 'Monitoring ',
            zh: '正在監視 '
        },
        interval: {
            en: ', interval is ',
            zh: ',時間間隔 '
        },
        second: {
            en: ' seconds',
            zh: ' 秒。'
        },
    };

    var i18n = function(key) {
        var userlang = mw.config.get('wgUserLanguage');
        var lang = userlang;
        // we have zh, zh-hans, zh-cn, zh-tw, etc.
        if (userlang.startsWith('zh')) {
            lang = 'zh';
        }
        // fallback to en
        return translations[key][lang] || translations[key]['en'];
    }

	if (typeof(p4js_auto_undo_csd_template) !== 'object') {
		p4js_auto_undo_csd_template = {
			// default: 'delete'
			'commonswiki': 'speedy',
		};
	}

    var DELAY = 3;
    var SUMMARY = 'revert edits by $1';
    var CONTRIB = '[[Special:Contributions/$1|$1]]';
    var HIDDEN = '<username hidden>';

    var uid = null;
    var hidename = false;
    var handle_creation = 'nothing';

    var summary;
    var now = null;
    var timerId = null;
    var wgScript = mw.config.get('wgScript');

    var print = function (html) {
        $('#content').append($('<p>').html(html));
    };

    var getNewEdits = function (username, time) {
        var data = {
            format: 'json',
            action: 'query',
            list: 'usercontribs',
            uclimit: 'max',
            ucstart: new Date(time).toISOString(),
            ucdir: 'newer'
        };
        if (username.endsWith('/16')) {
            $.extend(data, {ucuserprefix: username.split('.').slice(0, 2).join('.')});
        } else if (username.endsWith('/24')) {
            $.extend(data, {ucuserprefix: username.split('.').slice(0, 3).join('.')});
        } else {
            $.extend(data, {ucuser: username});
        }
        return $.ajax({
            url: mw.util.wikiScript('api'),
            data: data,
            dataType: 'json',
            type: 'POST',
        });
    };

    var pageLink = function (page, title) {
        return '<a target="_blank" href="' + wgScript + '?title=' + page + '">' + (title || page) + '</a>';
    };

    var undo = function (edits) {
        var callback = function (edit) {
            return function (data) {
                if (data.edit && data.edit.result === 'Success') {
                    print('<span style="color:green">' + i18n('undid') + pageLink(edit.title) + i18n('revision') + pageLink('Special:Diff/' + edit.revid, edit.revid) + '</span>');
                } else {
                    if (data.error && data.error.code === 'nosuchrevid' && (handle_creation === 'empty' || handle_creation === 'csd')) {
                        var newtext = (handle_creation === 'csd' ? '{{' + (p4js_auto_undo_csd_template[change.wiki] || 'delete') + '|vandalism}}' : '');
                        $.ajax({
                            url: mw.util.wikiScript('api'),
                            data: {
                                format: 'json',
                                action: 'edit',
                                title: edits[i].title,
                                text: newtext,
                                minor: true,
                                bot: true,
                                summary: summary,
                                token: mw.user.tokens.get('csrfToken')
                            },
                            dataType: 'json',
                            type: 'POST',
                        }).done(function(data2) {
                            if (data2 && data2.edit && data2.edit.result && data2.edit.result === 'Success') {
                                // TODO: log that it's a creation
                                print('<span style="color:green">' + i18n('undid') + pageLink(edit.title) + i18n('revision') + pageLink('Special:Diff/' + edit.revid, edit.revid) + '</span>');
                            } else {
                                if (data2.error) {
                                    print('<span style="color:red">' + i18n('cannotundo') + pageLink(edit.title) + i18n('revision') + pageLink('Special:Diff/' + edit.revid, edit.revid) + '</span>');
                                    console.log(data2.error.info);
                                    return;
                                }
                                print('<span style="color:red">' + i18n('cannotundo') + pageLink(edit.title) + i18n('revision') + pageLink('Special:Diff/' + edit.revid, edit.revid) + '</span>');
                                console.log(data2);
                            }
                        });
                    } else {
                        print('<span style="color:red">' + i18n('cannotundo') + pageLink(edit.title) + i18n('revision') + pageLink('Special:Diff/' + edit.revid, edit.revid) + '</span>');
                    }
                }
            };
        };

        for (var i=0; i<edits.length; i++) {
            var cb = callback(edits[i]);
            $.ajax({
                url: mw.util.wikiScript('api'),
                data: {
                    format: 'json',
                    action: 'edit',
                    title: edits[i].title,
                    undo: edits[i].revid,      // 換成 undoafter 更狠
                    minor: true,
                    bot: true,
                    summary: summary,
                    token: mw.user.tokens.get('csrfToken')
                },
                dataType: 'json',
                type: 'POST',
            }).then(cb);
        }
    };

    var monitor = function () {
        getNewEdits(uid, now).then(function (data) {
            now = new Date().getTime();

            var edits = [];
            if (data.query && data.query.usercontribs) {
                for (var i = 0; i<data.query.usercontribs.length; i++) {
                    var rev = data.query.usercontribs[i];
                    print(i18n('newedit') + rev.user + i18n('revision') + rev.revid + ' @ ' + pageLink(rev.title));
                    edits.push({
                        title: rev.title,
                        revid: rev.revid
                    });
                }
            }

            undo(edits);
        });
    };

    var start = function () {
        $('#content').html(i18n('monitoring') + uid + i18n('interval') + DELAY + i18n('second'));

        summary = SUMMARY.replace(/\$1/g, hidename ? HIDDEN : CONTRIB.replace(/\$1/g, uid));

        timerId = setInterval(monitor, DELAY * 1000);
        now = new Date().getTime();
    };

    var stop = function () {
        clearInterval(timerId);
        timerId = null;
        print(i18n('stopped'));
    };

    $(mw.util.addPortletLink('p-cactions', '#', i18n('portletlink'))).click(function (e) {
        if (timerId !== null) {
            alert(i18n('newwindow'));
            return;
        }

        $('#content').html(`<div id="p4js-autoundo-settings"><p>${i18n('introduction')}</p>
            <label>${i18n('username')}<input type="text" id="p4js-autoundo-username"></label><br>
            <label><input type="checkbox" checked id="p4js-autoundo-showusername">${i18n('showname')}</label>${i18n('shownamedesp')}<br>
            <label>${i18n('newpage')}<select id="p4js-autoundo-creation">
            <option value="csd">${i18n('speedydelete')}</option>
            <option value="empty">${i18n('empty')}</option>
            <option value="nothing">${i18n('nothing')}</option></select></label><br>
            <a id="p4js-autoundo-start">${i18n('start')}</a></div>`);

        $('#p4js-autoundo-start').click(function(e) {
            uid = $('#p4js-autoundo-username')[0].value.trim();
            if (uid) {
            	// normalize the username: replace underscore with space
                uid = uid.replace(/_/g, ' ');
                // make the first letter uppercase
                uid = uid.charAt(0).toUpperCase() + uid.slice(1);
                hidename = !$('#p4js-autoundo-showusername')[0].checked;
                handle_creation = $('#p4js-autoundo-creation')[0].value;
                $('#p4js-autoundo-settings').hide();
                start();
            }
        });
    });
});
});