User:Peterxy12/AdvancedRollback/core.js

From Test Wiki
Revision as of 05:53, 25 December 2025 by zhwiki>1F616EMO (// Edited via InPageEdit)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*
 * Advanced Rollback script for rollbackers
 * 
 * This script asks the user to supply an optional rollback summary.
 * After the rollback is complete, the user is taken to the diff page.
 * The diff page will open in a new window if the user is on Special:RecentChanges.
 * 
 * Do not load this script directly. Load a localization script instead.
 * e.g. [[:zh:User:1F616EMO/AdvancedRollback/zh.js]].
 * 
 * Use the following line to load this script:
 * mw.loader.load( "https://zh.wikipedia.org/w/index.php?title=User:1F616EMO/AdvancedRollback/core.js&action=raw&ctype=text/javascript" );
 * 
 * This script is made by 1F616EMO on zhwiki, licensed under CC BY-SA 4.0.
 * 
 * 225-02-16 forked from https://zh.wikipedia.org/wiki/MediaWiki:Gadget-rollback-summary.js
 * 
 */
// <nowiki>

"use strict";

$.when(mw.loader.using([
    'mediawiki.api',
    'mediawiki.jqueryMsg',
    'oojs-ui-core',
    'oojs-ui-windows',
]), $.ready)
    .then(() => new mw.Api({
        userAgent: 'AdvancedRollback/1.0.0'
    }).loadMessagesIfMissing([
        'rollbacklinkcount',
        'rollbacklinkcount-morethan',
        'rollback',
        'colon-separator',
        'cancel',
        'summary-preview',
    ]))
    .then(() => {
        const api = new mw.Api({
            userAgent: 'AdvancedRollback/1.0.0'
        });
        const myname = mw.config.get('wgUserName');
        const windowManager = new OO.ui.WindowManager();
        $(document.body).append(windowManager.$element);

        const rvlimit = Math.min((window.AdvancedRollBackRevisionLimit || 20) + 1, 500);
        const rollbackTag = mw.message('advanced-rollback-tag').plain();

        let diffMode = window.AdvancedRollBackDiffMode || 'default';

        let ipe_quickdiff = null;
        if (diffMode === 'inpageedit') {
            ipe_quickdiff =
                (window.InPageEdit && window.InPageEdit.quickDiff) ||
                (window.ipe && window.ipe.quickDiff.comparePages && window.ipe.quickDiff.comparePages.bind(window.ipe.quickDiff)) || null;
            if (!ipe_quickdiff) {
                console.warn('InPageEdit is not available, fallback to default diff display mode.');
                diffMode = 'default';
            }
        }

        /* Rollback helpers */

        async function rollbackUserEdits(params) {
            const { title, user, summary } = params;
            if (!title) {
                throw new Error('Invalid parameters for rollbackUserEdits');
            }

            try {
                const responce = await api.postWithToken('rollback', {
                    action: 'rollback',
                    title: title,
                    user: user,
                    summary: summary,
                });

                return {
                    success: true,
                    diff: responce.rollback.revid,
                    oldid: responce.rollback.old_revid,
                }
            } catch (error) {
                console.error('Rollback failed:', error);
                return {
                    success: false,
                    error: error,
                };
            }
        }

        async function undoEdits(params) {
            const { title, undo, undoafter, summary } = params;
            if (!title || !undo) {
                throw new Error('Invalid parameters for undoEdits');
            }

            try {
                const response = await api.postWithEditToken({
                    action: 'edit',
                    title: title,
                    baserevid: undo,
                    undo: undo,
                    undoafter: undoafter,
                    summary: summary,
                });

                if (response.edit && response.edit.nochange === "") {
                    return {
                        success: false,
                        error: 'nochange',
                    };
                }

                return {
                    success: true,
                    diff: response.edit.newrevid,
                    oldid: response.edit.oldrevid,
                };
            } catch (error) {
                console.error('Undo failed:', error);
                return {
                    success: false,
                    error: error,
                };
            }
        }

        /* Dialog Definition */

        const rollbackDialog = window.AdvancedRollbackDialog = function (config) {
            rollbackDialog.super.call(this, config);
        }
        OO.inheritClass(rollbackDialog, OO.ui.ProcessDialog);

        rollbackDialog.static.name = 'rollbackDialog';
        rollbackDialog.static.title = mw.message('rollback').plain();
        rollbackDialog.static.actions = [
            {
                action: 'rollback',
                label: mw.message('rollback').plain(),
                flags: ['primary', 'progressive'],
            },
            {
                flags: 'safe',
                label: mw.message('cancel').plain(),
                flags: ['safe', 'close']
            }
        ];
        rollbackDialog.prototype.initialize = function () {
            rollbackDialog.super.prototype.initialize.apply(this, arguments);

            this.panel = new OO.ui.PanelLayout({
                padded: true,
                expanded: false
            });

            const fieldset = this.content = new OO.ui.FieldsetLayout({
                // label: () => mw.message('rollback-fieldset-label', this.data ? this.data.from : 'Example').parseDom(),
            });

            const menuItems = [];
            menuItems.push({
                data: 'custom',
                label: mw.message('rollback-summary-custom').plain(),
            });

            window.AdvancedRollbackSummaryPresets = window.AdvancedRollbackSummaryPresets || {};
            const OptionGroups = {};
            for (const key in window.AdvancedRollbackSummaryPresets) {
                if (typeof window.AdvancedRollbackSummaryPresets[key] === 'string') {
                    window.AdvancedRollbackSummaryPresets[key] = {
                        description: window.AdvancedRollbackSummaryPresets[key],
                        text: window.AdvancedRollbackSummaryPresets[key],
                    }
                }
                if (window.AdvancedRollbackSummaryPresets[key].optgroup) {
                    if (typeof window.AdvancedRollbackSummaryPresets[key].items !== 'object') {
                        console.warn('Invalid AdvancedRollbackSummaryPresets Option Group: ',
                            key, window.AdvancedRollbackSummaryPresets[key].optgroup);
                        continue;
                    }
                    if (typeof window.AdvancedRollbackSummaryPresets[key].optgroup !== 'string') {
                        window.AdvancedRollbackSummaryPresets[key].optgroup = key;
                    }

                    OptionGroups[key] = window.AdvancedRollbackSummaryPresets[key];
                    delete window.AdvancedRollbackSummaryPresets[key];
                } else {
                    menuItems.push({
                        data: key,
                        label: window.AdvancedRollbackSummaryPresets[key].description,
                    });
                }
            }

            // Due to how OOUI works, create option groups later here
            for (const key in OptionGroups) {
                menuItems.push({
                    optgroup: OptionGroups[key].optgroup,
                });

                const items = OptionGroups[key].items;
                for (const itemKey in items) {
                    if (typeof items[itemKey] === 'string') {
                        items[itemKey] = {
                            description: items[itemKey],
                            text: items[itemKey],
                        };
                    }
                    window.AdvancedRollbackSummaryPresets[key + "-" + itemKey] = items[itemKey];
                    menuItems.push({
                        data: key + "-" + itemKey,
                        label: items[itemKey].description,
                    });
                }
            }

            this.editWarWarningWidget = new OO.ui.MessageWidget({
                type: 'warning',
                label: mw.message('rollback-warn-3rr').parseDom(),
            });

            $(this.editWarWarningWidget.$element).css('margin-bottom', '12px');

            // (1) Select intention (default: unspecified)
            //    Note: Not AGF cuz rollback is usually for combatting vandalism
            //          embarrasing if AGF'ed
            // (2) Choose from summary presets (first element: custom -> blankern input)
            // (3) Input custom summary

            this.summaryDropdown = new OO.ui.DropdownInputWidget({
                required: true,
                value: 'custom',
                options: menuItems,
            });

            // this.summaryDropdown.dropdownWidget.menu.getClosestScrollableElementContainer = () => $(document.body);

            this.summaryInput = new OO.ui.MultilineTextInputWidget({
                placeholder: mw.message('rollback-summary-prompt').plain(),
                rows: 5,
                allowLinebreaks: false,
            });

            this.useRollbackCheckbox = new OO.ui.CheckboxInputWidget({
                selected: false,
            });

            this.hideUserNameCheckbox = new OO.ui.CheckboxInputWidget({
                selected: false,
            });

            this.showTalkPageCheckbox = new OO.ui.CheckboxInputWidget({
                selected: false,
            });

            this.intentionSelection = new OO.ui.ButtonSelectWidget({
                items: [
                    new OO.ui.ButtonOptionWidget({
                        data: 'unspecified',
                        label: mw.message('rollback-intention-unspecified').plain(),
                    }).setSelected(true),
                    new OO.ui.ButtonOptionWidget({
                        icon: 'success',
                        data: 'good',
                        label: mw.message('rollback-intention-good').plain(),
                    }),
                    new OO.ui.ButtonOptionWidget({
                        icon: 'clear',
                        data: 'vandalism',
                        label: mw.message('rollback-intention-vandalism').plain(),
                    }),
                ],
            });

            this.summaryPreviewSpan = $('<span id="advancedrollback-summary-preview">');

            const summaryPreviewShell = $('<span id="advancedrollback-summary-preview-shell">')
                .append(mw.message('summary-preview').parseDom())
                .append(this.summaryPreviewSpan);

            fieldset.addItems([
                new OO.ui.FieldLayout(this.intentionSelection, {
                    align: 'top',
                }),

                new OO.ui.FieldLayout(this.useRollbackCheckbox, {
                    label: mw.message('rollback-use-rollback').plain(),
                    align: 'inline',
                }),

                new OO.ui.FieldLayout(this.hideUserNameCheckbox, {
                    label: mw.message('rollback-summary-hide-user').plain(),
                    align: 'inline',
                }),

                new OO.ui.FieldLayout(this.showTalkPageCheckbox, {
                    label: mw.message('rollback-summary-show-talk-page').plain(),
                    align: 'inline',
                }),

                new OO.ui.FieldLayout(this.summaryDropdown, {
                    label: mw.message('rollback-summary-presets').plain(),
                    align: 'top',
                }),

                new OO.ui.FieldLayout(this.summaryInput, {
                    label: mw.message('rollback-summary-custom').plain(),
                    align: 'top',
                }),
            ]);

            this.panel.$element.append(this.editWarWarningWidget.$element);
            this.panel.$element.append(fieldset.$element);
            this.panel.$element.append(summaryPreviewShell);
            this.$body.append(this.panel.$element);

            this.summaryDropdown.on('change', this.onSummaryDropdownChange.bind(this));
            this.summaryInput.on('change', this.onSummaryInputChange.bind(this));
        };

        rollbackDialog.prototype.getSetupProcess = function (data) {
            data = data || {};
            this.setData(data);
            return rollbackDialog.super.prototype.getSetupProcess.call(this, data)
                .next(() => {
                    this.intentionSelection.selectItemByData('unspecified');
                    this.summaryDropdown.setValue('custom');
                    this.summaryInput.setValue('');

                    if (!data.undo || !data.undoafter || !data.token) {
                        this.useRollbackCheckbox.setDisabled(true);
                        this.useRollbackCheckbox.setSelected(data.token ? true : false);
                    } else {
                        this.useRollbackCheckbox.setDisabled(false);
                        this.useRollbackCheckbox.setSelected(false);
                    }

                    this.hideUserNameCheckbox.setDisabled(!data.from);
                    this.showTalkPageCheckbox.setDisabled(!data.from);
                    this.hideUserNameCheckbox.setSelected(false);
                    this.showTalkPageCheckbox.setSelected(false);

                    if (data.warn3rr) {
                        this.editWarWarningWidget.toggle(true);
                    } else {
                        this.editWarWarningWidget.toggle(false);
                    }
                });
        };

        rollbackDialog.prototype.onSummaryDropdownChange = function (value) {
            if (value === 'custom') {
                this.summaryInput.setValue('');
            } else {
                this.summaryInput.setValue(window.AdvancedRollbackSummaryPresets[value].text || '');
            }
            this.updateSummaryPreview();
        };

        rollbackDialog.prototype.updateSummaryPreview = function () {
            delete this.updateSummaryPreviewTimeout;
            const data = this.data;
            const summary = this.summaryInput.getValue();
            if (this.lastHandledSummary === summary)
                return;
            this.lastHandledSummary = summary;
            this.summaryPreviewSpan.text('[...]');
            api.get({
                action: 'parse',
                title: data.pagetitle,
                text: '',
                summary: summary,
            }).then((rtnData) => {
                console.log(rtnData);
                const parsedHTML = rtnData.parse.parsedsummary["*"];
                this.summaryPreviewSpan.html(parsedHTML);
                this.updateSize();
            });
        };

        rollbackDialog.prototype.onSummaryInputChange = function () {
            if (this.updateSummaryPreviewTimeout)
                clearTimeout(this.updateSummaryPreviewTimeout);
            this.summaryPreviewSpan.text('[...]');
            this.updateSummaryPreviewTimeout = setTimeout(this.updateSummaryPreview.bind(this), 500);
        };

        rollbackDialog.prototype.getActionProcess = function (action) {
            if (action === 'rollback') {
                const data = this.data;
                const from = data.from;

                const $this = data.elem;
                if ($this) {
                    $this.addClass('advancedrollback-clicked');
                    $this.text(mw.message('rollback-processing'));
                }

                const summaryInput = this.summaryInput.getValue().trim();
                const intentionItem = this.intentionSelection.findSelectedItem();
                const intention = intentionItem ? intentionItem.data : 'unspecified';
                const fromString = this.hideUserNameCheckbox.isSelected()
                    ? mw.message('rollback-summary-inappropriate-user-string').plain() : (from
                        ? mw.message('rollback-summary-user-string', from).plain()
                        : mw.message('rollback-summary-revdel-user-string').plain());
                const useRollback = this.useRollbackCheckbox.isSelected();
                const showTalkPage = this.showTalkPageCheckbox.isSelected();

                let summaryTypePrefix;
                let summaryParameters;

                if (data.type === 'rollback') {
                    summaryTypePrefix = 'rollback-summary-';
                    summaryParameters = [fromString];
                } else if (data.type === 'undo') {
                    summaryTypePrefix = 'rollback-summary-undo-';
                    summaryParameters = [data.undo, fromString];
                } else if (data.type === 'undoseries') {
                    summaryTypePrefix = 'rollback-summary-undoseries-';
                    summaryParameters = [data.undo, data.undoafter];
                } else {
                    throw new Error('Invalid rollback dialog type: ' + data.type);
                }

                let summary = mw.message(summaryTypePrefix + intention, ...summaryParameters).plain();
                if (summaryInput !== '') {
                    summary += mw.message('colon-separator').plain() + summaryInput;
                }
                summary += " " + rollbackTag;

                return rollbackDialog.super.prototype.getActionProcess.call(this, action)
                    .next(async function () {
                        let responce;
                        if (useRollback) {
                            responce = await rollbackUserEdits({
                                title: data.pagetitle,
                                user: from,
                                summary: summary,
                            });
                        } else {
                            responce = await undoEdits({
                                title: data.pagetitle,
                                undo: data.undo,
                                undoafter: data.undoafter,
                                summary: summary,
                            });
                        }

                        if (responce.success) {
                            if ($this)
                                $this.text(mw.message('rollback-done'));

                            /* Show user talk page and Twinkle integration */

                            if (showTalkPage) {
                                const url = mw.util.getUrl('User talk:' + from, {
                                    'vanarticle': data.pagetitle,
                                });
                                window.open(url);
                            }

                            /* Display diff page */

                            let thisDiffMode = diffMode;

                            if (diffMode === 'inpageedit') {
                                ipe_quickdiff({
                                    fromrev: responce.oldid,
                                    torev: responce.diff,
                                });
                            } else if (diffMode === 'default') {
                                if (mw.config.get('wgCanonicalSpecialPageName') === 'Recentchanges'
                                    || mw.config.get('wgCanonicalSpecialPageName') === 'Watchlist') {
                                    thisDiffMode = 'newtab';
                                } else {
                                    thisDiffMode = 'this';
                                }
                            }

                            const diff_page = mw.util.getUrl('Special:Diff/' + responce.oldid + '/' + responce.diff);
                            if (thisDiffMode === 'newtab') {
                                window.open(diff_page);
                            } else if (thisDiffMode === 'this') {
                                window.location.href = diff_page;
                            }
                        } else {
                            console.warn("Rollback failed: ", responce.error);
                            if ($this)
                                $this.text(mw.message('rollback-failed', responce.error));
                        }
                    })
                    .next(() => this.close());

            } else {
                return rollbackDialog.super.prototype.getActionProcess.call(this, action);
            }
        };

        let doRollbackDialog;

        const openRollbackDialog = window.AdvancedRollbackOpenDialog = function (params) {
            if (!doRollbackDialog) {
                window.AdvancedRollbackDialogInstance = doRollbackDialog = new rollbackDialog();
                windowManager.addWindows([doRollbackDialog]);
            }
            windowManager.openWindow(doRollbackDialog, params);
        };

        const onRollbackClick = function (e) {
            e.preventDefault();
            const $this = $(this);
            if ($this.hasClass('advancedrollback-clicked'))
                return;

            const title = $this.attr('data-ar-title');
            const from = $this.attr('data-ar-from');
            const token = $this.attr('data-ar-token');
            const action = $this.attr('data-ar-action') || 'rollback';

            const undo = $this.attr('data-ar-undo');
            const undoafter = $this.attr('data-ar-undoafter');

            const warn3rr = $this.attr('data-warn-3rr') == '1';

            if (!(title)) {
                $this.text(mw.message('rollback-failed', mw.message('rolback-failed-href-error')));
                return;
            }

            openRollbackDialog({
                type: action,
                title: mw.message('rollback-window-title', title, from).parseDom(),
                pagetitle: title,
                from: from,
                token: token,
                elem: $this,
                undo: undo,
                undoafter: undoafter,
                warn3rr: warn3rr,
            });
        };

        mw.util.addCSS('.mw-rollback-link .advancedrollback-processed { color: var(--color-subtle, #54595d) !important; }');

        const createRollbackLink = function ($container, title, from, token) {
            if (mw.util.isIPAddress(from)) {
                from = mw.util.sanitizeIP(from);
            }
            let $rollbacklink = $container.find('a');
            if ($rollbacklink.length === 0) {
                $rollbacklink = $('<a>')
                    .addClass('mw-rollback-link advancedrollback-created')
                    .attr('href', '#')
                    .text(mw.message('rollback').plain())
                    .appendTo($container);
            } else {
                if ($rollbacklink.hasClass('advancedrollback-processed'))
                    return;
                const href = $rollbacklink.attr('href');
                token = mw.util.getParamValue('token', href);
            }

            $rollbacklink
                .addClass('advancedrollback-processed')
                .off('click')
                .on('click', onRollbackClick);

            $rollbacklink.attr('data-ar-title', title);
            $rollbacklink.attr('data-ar-from', from != '' ? from : null);
            $rollbacklink.attr('data-ar-token', token);
            $rollbacklink.attr('data-ar-action', 'rollback');

            const yesterday = new Date(Date.now() - 86400000);
            api.get({
                action: 'query',
                prop: 'revisions',
                titles: title,
                rvprop: 'user|ids|tags|timestamp',
                rvlimit: rvlimit,
            }).then((data) => {
                const page = Object.values(data.query.pages)[0];
                if (!page)
                    return;

                let edit_count = 0;
                let undo = 0;
                let undoafter = 0;
                let revert_count = 0;
                for (const rev of page.revisions) {
                    const user = mw.util.isIPAddress(rev.user) ? mw.util.sanitizeIP(rev.user) : rev.user;
                    if (undo === 0) {
                        undo = rev.revid;
                    }
                    if ((user !== from || rev.userhidden) && undoafter === 0) {
                        undoafter = rev.revid;
                    }
                    if (undoafter === 0) {
                        edit_count++;
                    }
                    if (
                        user == myname &&
                        new Date(rev.timestamp) > yesterday &&
                        rev.tags &&
                        (
                            rev.tags.includes('mw-undo') ||
                            rev.tags.includes('mw-manual-revert') ||
                            rev.tags.includes('mw-rollback')
                        )
                    ) {
                        revert_count++;
                    }
                }

                if (
                    undo === undoafter || (
                        edit_count >= page.revisions.length &&
                        (
                            !token ||
                            page.revisions[page.revisions.length - 1].parentid === 0
                        )
                    )
                ) {
                    const $parent = $container.parent();
                    if ($parent.children().length <= 1) {
                        $parent.remove();
                    } else {
                        $container.remove();
                    }
                    return;
                }

                if (revert_count >= 3) {
                    $rollbacklink.attr('data-warn-3rr', 1);
                }

                $rollbacklink.attr('data-ar-undo', undo);
                $rollbacklink.attr('data-ar-undoafter', undoafter);

                if (!window.AdvancedRollbackNoCountRevision) {
                    if (edit_count >= rvlimit)
                        $rollbacklink.text(mw.message('rollbacklinkcount-morethan', edit_count - 1));
                    else
                        $rollbacklink.text(mw.message('rollbacklinkcount', edit_count));
                }
            })
        }

        const process = function ($content) {
            // For rolling back changes made by the same user in a row
            // We cannot assume .mw-rollback-link a is always present
            // Where are we?
            const SpecialPageName = mw.config.get('wgCanonicalSpecialPageName');
            if (['Recentchanges', 'Watchlist'].includes(SpecialPageName)) {
                let selector =
                    '.mw-changeslist-line.mw-changeslist-edit.mw-changeslist-last .mw-changeslist-line-inner' +
                    ', .mw-changeslist-line.mw-changeslist-edit .mw-changeslist-last td.mw-enhanced-rc-nested';

                const $lastChange = $content.find(selector);
                $lastChange.each(function () {
                    const $this = $(this);
                    const title = $this.attr('data-target-page');
                    const from = $this.find('.mw-userlink bdi').text();

                    let $pagertools = $this.find('.mw-changeslist-links.mw-pager-tools');
                    if ($pagertools.length === 0) {
                        let $insertpoint = $this.find('.comment');
                        if ($insertpoint.length === 0)
                            $insertpoint = $this.find('.mw-changeslist-links.mw-usertoollinks');
                        $pagertools = $('<span class="mw-changeslist-links mw-pager-tools">')
                            .insertAfter($insertpoint);
                        $pagertools.before(' ');
                    }

                    let $rollbacklinkcontainer = $pagertools.find('.mw-rollback-link');
                    if ($rollbacklinkcontainer.length === 0) {
                        $rollbacklinkcontainer = $('<span class="mw-rollback-link">')
                            .appendTo($('<span>').prependTo($pagertools));
                    }

                    createRollbackLink($rollbacklinkcontainer, title, from);
                });

                $content.find('.mw-changeslist-groupdiff:not(.advancedrollback-processed)').each(function () {
                    const $this = $(this);
                    const title = $this.attr('title');
                    const href = $this.attr('href');
                    const undo = mw.util.getParamValue('diff', href);
                    const undoafter = mw.util.getParamValue('oldid', href);

                    $("<a>")
                        .addClass('rollback-summary-undoseries-groupdiff-button')
                        .attr('href', 'javascript:void(0);')
                        .text(mw.message('rollback-summary-undoseries-groupdiff-button').plain())
                        .click(function (e) {
                            e.preventDefault();
                            openRollbackDialog({
                                type: 'undoseries',
                                title: mw.message('rollback-window-title-nofrom', title).parseDom(),
                                pagetitle: title,
                                elem: $(this),
                                from: null,
                                undo: undo,
                                undoafter: undoafter,
                            });
                        })
                        .appendTo($("<span>").insertAfter($this.parent()))

                    $this.addClass('advancedrollback-processed');
                });
            } else if (SpecialPageName === 'Contributions') {
                $(".mw-contributions-list .mw-contributions-current").each(function () {
                    const $this = $(this);
                    const title = $(this).find("bdi .mw-contributions-title").text();
                    const from = mw.config.get('wgRelevantUserName');

                    let $pagertools = $this.find('.mw-changeslist-links.mw-pager-tools');
                    if ($pagertools.length === 0) {
                        let $insertpoint = $this.find('.mw-uctop');
                        $pagertools = $('<span class="mw-changeslist-links mw-pager-tools">')
                            .insertAfter($insertpoint);
                        $pagertools.before(' ');
                    }

                    let $rollbacklinkcontainer = $pagertools.find('.mw-rollback-link');
                    if ($rollbacklinkcontainer.length === 0) {
                        $rollbacklinkcontainer = $('<span class="mw-rollback-link">')
                            .appendTo($('<span>').prependTo($pagertools));
                    }

                    createRollbackLink($rollbacklinkcontainer, title, from);
                });
            } else if (mw.config.get('wgAction') === 'history') {
                // We are in history page, where the first .mw-contributions-list li *may* be the last change
                // We have to check it against wgCurRevisionId
                const $lastChange = $content.find('.mw-contributions-list li:first');
                if ($lastChange.length <= 0)
                    return;
                const revid = $lastChange.attr('data-mw-revid');
                if (revid != mw.config.get('wgCurRevisionId'))
                    return;

                // We don't have ot worry about the existance of .mw-pager-tools, as "revert" exists
                const $pagertools = $lastChange.find('.mw-changeslist-links.mw-pager-tools');

                let $rollbacklinkcontainer = $pagertools.find('.mw-rollback-link');
                if ($rollbacklinkcontainer.length === 0) {
                    $rollbacklinkcontainer = $('<span class="mw-rollback-link">')
                        .appendTo($('<span>').prependTo($pagertools));
                }

                const title = mw.config.get('wgPageName');
                const from = $lastChange.find('.history-user .mw-userlink bdi').text();
                createRollbackLink($rollbacklinkcontainer, title, from);

                // Handle range revert links
                if (
                    $content.find('.mw-history-compareselectedversions .historysubmit').length > 0 &&
                    $content.find(".advancedrollback-undoseries-button").length === 0
                ) {
                    const compareForm = $content.find("#mw-history-compare")[0];
                    $("<button>")
                        .text(mw.message('rollback-summary-undoseries-button').plain())
                        .addClass("cdx-button advancedrollback-undoseries-button")
                        .click(function (e) {
                            const $this = $(this);
                            e.preventDefault();

                            const undo = compareForm.diff ? compareForm.diff.value : null;
                            const undoafter = compareForm.oldid ? compareForm.oldid.value : null;

                            if (undo && undoafter) {
                                openRollbackDialog({
                                    type: 'undoseries',
                                    title: mw.message('rollback-window-title-nofrom', title).parseDom(),
                                    pagetitle: title,
                                    from: from,
                                    undo: undo,
                                    undoafter: undoafter,
                                });
                            }
                        })
                        .insertBefore($content.find(".mw-history-revisionactions"));
                }
            } else if (mw.config.get('wgDiffOldId') !== null) {
                // We're on diff page, where .diff-side-added *may* be the last edit
                const $diffAdded = $content.find('.diff-side-added');
                if ($diffAdded.length <= 0)
                    return;
                const $userlink = $diffAdded.find('#mw-diff-ntitle2 a.mw-userlink:first');
                const revid = $userlink.attr('data-mw-revid');
                if (revid != mw.config.get('wgCurRevisionId'))
                    return;
                // Find .mw-rollback-link or append it after .mw-usertoollinks
                let $rollbacklinkcontainer = $content.find('.mw-rollback-link');
                if ($rollbacklinkcontainer.length === 0) {
                    const $usertoollinks = $content.find('.mw-usertoollinks');
                    $rollbacklinkcontainer = $('<span class="mw-rollback-link">')
                        .appendTo($usertoollinks);
                }

                const title = mw.config.get('wgPageName');
                const from = $userlink.find('bdi').text();

                createRollbackLink($rollbacklinkcontainer, title, from);
            }

            // Handle .mw-history-undo, i.e. revert only one edit
            $content.find(`
                .mw-history-undo a:not(.advancedrollback-processed),
                .mw-diff-undo a:not(.advancedrollback-processed)
            `).each(function () {
                const $this = $(this);
                if ($this.length <= 0)
                    return;

                $this.addClass('advancedrollback-processed');

                (async function () {
                    const href = $this.attr('href');
                    const undo = mw.util.getParamValue('undo', href);
                    const undoafter = mw.util.getParamValue('undoafter', href);
                    if (!undo || !undoafter)
                        return;

                    const rev_data = await api.get({
                        action: 'query',
                        prop: 'revisions',
                        rvprop: 'user',
                        revids: undo,
                    });

                    if (!rev_data.query || !rev_data.query.pages)
                        return;
                    const rev_user = Object.values(rev_data.query.pages)[0].revisions[0].user;
                    if (!rev_user)
                        return;

                    $this.addClass('advancedrollback-processed');

                    const $quickRevertButtonFrame = $('<span>');
                    const $quickRevertButtonFrameInner = $('<span>').appendTo($quickRevertButtonFrame);
                    const $quickRevertButton = $('<a>').appendTo($quickRevertButtonFrameInner);

                    $quickRevertButton.text(mw.message('rollback-button-quick-revert').plain());
                    $quickRevertButton.attr('href', '#');
                    $quickRevertButton.attr('data-ar-undo', undo);
                    $quickRevertButton.attr('data-ar-undoafter', undoafter);
                    $quickRevertButton.attr('data-ar-title', mw.config.get('wgPageName'));
                    $quickRevertButton.attr('data-ar-from', rev_user);
                    $quickRevertButton.attr('data-ar-action', 'undo');

                    $quickRevertButton.click(onRollbackClick);
                    $quickRevertButton.addClass('advancedrollback-processed');

                    console.log($this.parent().parent());
                    $quickRevertButtonFrame.insertAfter($this.parent().parent().first());
                })();
            });
        };

        mw.hook('wikipage.content').add(process);
        process($("#mw-content-text")); // HACK: Sometimes the above fail on Special:Diff
    })
    .fail(console.warn)

// </nowiki> Nya~!