User:WhitePhosphorus/js/all-in-one.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.
/* Rollback, block, (revision) delete, and ignore.
 *
 * For more info: https://meta.wikimedia.org/wiki/User:WhitePhosphorus/all-in-one
 * 
 * See also https://zh.wikipedia.org/wiki/User:WhitePhosphorus/js/all-in-one.js
 * which is a legacy script.
 */

$(function () {
    mw.loader.using(['mediawiki.util', 'mediawiki.api', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows'], function() {

    let config = {};
    let inited = false;
    let loaded = false;
    let windowManager = new OO.ui.WindowManager();
    let aio_dialog;

    const work = function() {
        let api = new mw.Api();

        if (config.block) {
            let data = {
                action: 'block',
                user: config.username,
                expiry: config.blockDur,
                reason: config.blockReason
            };
            if (config.blockAnon) data.anononly = 1;
            if (config.blockAuto) data.autoblock = 1;
            if (config.blockCreate) data.nocreate = 1;
            if (!config.blockTalk) data.allowusertalk = 1;
            if (config.blockMail) data.noemail = 1;
            if (config.blockHide) data.hidename = 1;
            api.postWithEditToken(data).done(() => mw.notify('The villain is blocked.')).fail(function(e) {
                mw.notify('Failed to block the villain: ' + e, { type: 'warn' });
                console.log(e);
            });
        }

        let untildate = new Date();
        if (config.endtime === 'inf') {
            untildate = null;
        } else {
            untildate.setSeconds(untildate.getSeconds() - parseInt(config.endtime));
        }
        data = {
            action: 'query',
            list: 'usercontribs',
            ucuser: config.username,
            uclimit: 'max'
            // TODO: continue
        };
        if (untildate) data.ucend = untildate.toISOString();

        api.get(data).done(function(data) {
            let contribs = data.query;
            if (!contribs) {
                mw.notify('Failed to fetch the contribs!', { type: 'warn' });
                return;
            }
            contribs = contribs.usercontribs;
            if (!contribs || !contribs.length) {
                mw.notify('No contribs found.');
                return;
            }

            let ids = {};
            let creation = [];
            for (let edit of contribs) {
                if (edit.new === '') {
                    creation.push(edit.title);
                } else {
                    if (ids[edit.title] === undefined) ids[edit.title] = [];
                    ids[edit.title].push(edit.revid);
                }
            }
            for (let [title, idlist] of Object.entries(ids)) {
                if (!config.rollback) {
                    // only check revdel
                    if (config.rd) {
                        api.postWithEditToken({
                            action: 'revisiondelete',
                            type: 'revision',
                            ids: idlist,
                            hide: config.rdHides,
                            reason: config.rdReason,
                            suppress: config.os ? "yes" : "nochange"
                        }).done(() => mw.notify(`${idlist.length} revisions on page ${title} hidden.`)).fail(function(e) {
                            mw.notify(`Failed to hide revisions on ${title}: ${e}`, { type: 'warn' });
                            console.log('revisiondelete', title, idlist, e);
                        });
                    }
                    continue;
                }
                // we'd like to rollback first then revdel - if not, attempts to revdel the content may fail
                data = config.rollbackBot ? {markbot: 1} : {};
                data.summary = config.rollbackShow ? '' : 'revert edits by <username hidden>';
                api.rollback(title, config.username, data).done(function() {
                    mw.notify(`Page ${title} Reverted.`);
                }).fail(function(e) {
                    mw.notify(`Failed to revert on the page ${title}: ${e}`, { type: 'warn' });
                    console.log('revert', title, e);
                }).always(function() {
                    if (config.rd) {
                        api.postWithEditToken({
                            action: 'revisiondelete',
                            type: 'revision',
                            ids: idlist,
                            hide: config.rdHides,
                            reason: config.rdReason,
                            suppress: config.os ? "yes" : "nochange"
                        }).done(() => mw.notify(`${idlist.length} revisions on page ${title} hidden.`)).fail(function(e) {
                            mw.notify(`Failed to hide revisions on ${title}: ${e}`, { type: 'warn' });
                            console.log('revisiondelete', title, idlist, e);
                        });
                    }
                });
            }
            if (config.massdel) {
                for (let title of creation) {
                    api.postWithEditToken({
                        action: 'delete',
                        title: title,
                        reason: config.massdelReason
                    }).done(() => mw.notify(`Deleted page ${title}.`)).fail(function(e) {
                        mw.notify(`Failed to delete ${title}: ${e}`, { type: 'warn' });
                        console.log('delete', title, e);
                    });
                }
            }
        });
    };

    // Build OOUI interface
    const init = function() {
        if (inited) return;
        inited = true;

        function AIODialog(config) {
            AIODialog.super.call(this, config);
        }
        OO.inheritClass(AIODialog, OO.ui.ProcessDialog);

        AIODialog.static.name = 'p4AIODialog';
        AIODialog.static.actions = [
            {
                flags: ['primary', 'destructive'],
                label: 'Start [enter]',
                action: 'start',
            },
            {
                flags: ['safe', 'close'],
            },
        ];

        AIODialog.prototype.initialize = function() {
            AIODialog.super.prototype.initialize.call(this);
            this.panel = new OO.ui.PanelLayout({
                classes: ['p4-aio-form'],
                padded: true,
                expanded: false,
            });

            // General info: username, package, etc.
            this.top_fieldset = new OO.ui.FieldsetLayout({ 
		        label: 'All in One',
	        });

            this.top_username_input = new OO.ui.TextInputWidget({
                placeholder: 'username or IP (not range)',
            });

            this.top_endtime_dropdown = new OO.ui.DropdownWidget({
                menu: {
                    items: [
                        new OO.ui.MenuOptionWidget({
                            data: 3600,
                            label: 'in 1 hour',
                        }),
                        new OO.ui.MenuOptionWidget({
                            data: 86400,
                            label: 'in 1 day',
                        }),
                        new OO.ui.MenuOptionWidget({
                            data: 'inf',
                            label: 'everything',
                        }),
                        new OO.ui.MenuOptionWidget({
                            data: 'other',
                            label: 'Other: (in seconds)',
                        }),
                    ],
                },
            });

            this.top_endtime_input = new OO.ui.TextInputWidget({
                placeholder: 'newer than ... seconds',
            });

            this.top_package_dropdown = new OO.ui.DropdownWidget();
            this.top_suffix_dropdown = new OO.ui.DropdownWidget({
                menu: {
                    items: [ new OO.ui.MenuOptionWidget({ data: '', label: '<no suffix>' }) ],
                },
            });

            this.top_fieldset.addItems([
                new OO.ui.FieldLayout(
                    this.top_username_input, {
                        label: 'Target',
                    }
                ),
                new OO.ui.FieldLayout(
                    new OO.ui.Widget({
                        content: [
                            this.top_endtime_dropdown,
                            this.top_endtime_input,
                        ]
                    }), {
                        label: 'Edits',
                    }
                ),
                new OO.ui.FieldLayout(
                    this.top_package_dropdown, {
                        label: 'Package',
                        help: new OO.ui.HtmlSnippet(
                            '<a href="https://meta.wikimedia.org/wiki/User:WhitePhosphorus/all-in-one#Add_your_own_package">how to define your packages?</a>'
                        ),
                    }
                ),
                new OO.ui.FieldLayout(
                    this.top_suffix_dropdown, {
                        label: 'Suffix',
                        help: new OO.ui.HtmlSnippet(
                            '<a href="https://meta.wikimedia.org/wiki/User:WhitePhosphorus/all-in-one#Add_your_reasons_in_drop_down_list">how to define your rationales and suffixes?</a>'
                        ),
                    }
                ),
            ]);

            // Rollback
            this.rollback_fieldset = new OO.ui.FieldsetLayout({ 
		        label: 'Rollback',
                invisibleLabel: true,
                classes: ['p4-aio-rollback-fieldset'],
	        });

            this.rollback_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.rollback_bot_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.rollback_showname_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });

            this.rollback_fieldset.addItems([
                new OO.ui.FieldLayout(
                    this.rollback_checkbox, {
                        label: 'Enable rollback',
                        align: 'inline',
                        classes: ['p4-aio-checkbox', 'p4-aio-checkbox-block'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.rollback_bot_checkbox, {
                        label: 'Mark rollback as bot edits',
                        align: 'inline',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.rollback_showname_checkbox, {
                        label: 'Show username in rollback summary',
                        align: 'inline',
                        help: 'For some vandals with inappropriate username, you may want to uncheck this and the revert summary will look like "revert edits by <username hidden>".',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
            ]);

            // Block
            this.block_fieldset = new OO.ui.FieldsetLayout({ 
		        label: 'Block',
                invisibleLabel: true,
                classes: ['p4-aio-block-fieldset'],
	        });

            this.block_duration_dropdown = new OO.ui.DropdownWidget({
                menu: {
                    items: [
                        new OO.ui.MenuOptionWidget({ data: '1 day',  label: '1 day' }),
                        new OO.ui.MenuOptionWidget({ data: '31 hours',  label: '31 hours' }),
                        new OO.ui.MenuOptionWidget({ data: '3 days',  label: '3 days' }),
                        new OO.ui.MenuOptionWidget({ data: '5 days',  label: '5 days' }),
                        new OO.ui.MenuOptionWidget({ data: '1 week',  label: '1 week' }),
                        new OO.ui.MenuOptionWidget({ data: '2 weeks',  label: '2 weeks' }),
                        new OO.ui.MenuOptionWidget({ data: '1 month',  label: '1 month' }),
                        new OO.ui.MenuOptionWidget({ data: '3 months',  label: '3 months' }),
                        new OO.ui.MenuOptionWidget({ data: '6 months',  label: '6 months' }),
                        new OO.ui.MenuOptionWidget({ data: '1 year',  label: '1 year' }),
                        new OO.ui.MenuOptionWidget({ data: '2 years',  label: '2 years' }),
                        new OO.ui.MenuOptionWidget({ data: 'never',  label: 'indefinite' }),
                        new OO.ui.MenuOptionWidget({ data: 'other',  label: 'Other:' }),
                    ],
                },
            });

            this.block_duration_input = new OO.ui.TextInputWidget({
                placeholder: 'duration/timestamp',
            });

            this.block_reason_dropdown = new OO.ui.DropdownWidget({
                menu: {
                    // Data for "Other:" must be empty, since it will be directly included in the actual block reason
                    // Same for the (revision) delete reasons (will be added to the input box if append button is clicked)
                    items: [ new OO.ui.MenuOptionWidget({ data: '', label: 'Other:' }) ],
                },
            });

            this.block_reason_input = new OO.ui.TextInputWidget({
                placeholder: 'other rationale to append',
            });

            this.block_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.block_hardblock_checkbox = new OO.ui.CheckboxInputWidget();
            this.block_autoblock_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.block_create_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.block_talk_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.block_mail_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.block_hidename_checkbox = new OO.ui.CheckboxInputWidget();

            this.block_fieldset.addItems([
                new OO.ui.FieldLayout(
                    this.block_checkbox, {
                        label: 'Enable block',
                        align: 'inline',
                        classes: ['p4-aio-checkbox', 'p4-aio-checkbox-block'],
                    }
                ),
                new OO.ui.FieldLayout(
                    new OO.ui.Widget({
                        content: [
                            this.block_duration_dropdown,
                            this.block_duration_input,
                        ]
                    }), {
                        label: 'Expiration',
                    }
                ),
                new OO.ui.FieldLayout(
                    new OO.ui.Widget({
                        content: [
                            this.block_reason_dropdown,
                            this.block_reason_input,
                        ]
                    }), {
                        label: 'Reason',
                    }
                ),
                new OO.ui.FieldLayout(
                    this.block_hardblock_checkbox, {
                        label: 'Hard block',
                        align: 'inline',
                        help: 'Apply block to logged-in users from this IP address',
                        classes: ['p4-aio-checkbox', 'p4-aio-anon-only'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.block_autoblock_checkbox, {
                        label: 'Auto block',
                        align: 'inline',
                        help: 'Automatically block the last IP address used by this user, and any subsequent IP addresses they try to edit from, for a period of 1 day',
                        classes: ['p4-aio-checkbox', 'p4-aio-reg-only'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.block_create_checkbox, {
                        label: 'Block account creation',
                        align: 'inline',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.block_talk_checkbox, {
                        label: 'Block editing own talk page',
                        align: 'inline',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.block_mail_checkbox, {
                        label: 'Block email',
                        align: 'inline',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.block_hidename_checkbox, {
                        label: 'Hide username from public logs',
                        align: 'inline',
                        help: 'needs "hideuser" right, or your action will fail',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
            ]);

            // Page deletion
            this.pagedelete_fieldset = new OO.ui.FieldsetLayout({ 
		        label: 'Page deletion',
                invisibleLabel: true,
                classes: ['p4-aio-pagedelete-fieldset'],
	        });

            this.pagedelete_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });

            this.pagedelete_reason_dropdown = new OO.ui.DropdownWidget({
                menu: {
                    items: [ new OO.ui.MenuOptionWidget({ data: '', label: 'Other:' }) ],
                },
            });

            this.pagedelete_reason_button = new OO.ui.ButtonWidget({ label: 'Append' });

            this.pagedelete_reason_input = new OO.ui.TextInputWidget({
                placeholder: 'full rationale to submit',
            });

            this.pagedelete_fieldset.addItems([
                new OO.ui.FieldLayout(
                    this.pagedelete_checkbox, {
                        label: 'Enable deletion',
                        align: 'inline',
                        classes: ['p4-aio-checkbox', 'p4-aio-checkbox-block'],
                    }
                ),
                new OO.ui.FieldLayout(
                    new OO.ui.Widget({
                        content: [
                            this.pagedelete_reason_dropdown,
                            this.pagedelete_reason_button,
                            this.pagedelete_reason_input,
                        ]
                    }), {
                        label: 'Reason',
                        classes: ['p4-aio-reason'],
                    }
                ),
            ]);

            // Revision deletion
            this.revisiondelete_fieldset = new OO.ui.FieldsetLayout({ 
		        label: 'Revision deletion',
                invisibleLabel: true,
                classes: ['p4-aio-revisiondelete-fieldset'],
	        });

            this.revisiondelete_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.revisiondelete_content_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.revisiondelete_summary_checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
            this.revisiondelete_username_checkbox = new OO.ui.CheckboxInputWidget();
            this.revisiondelete_oversight_checkbox = new OO.ui.CheckboxInputWidget();

            this.revisiondelete_reason_dropdown = new OO.ui.DropdownWidget({
                menu: {
                    items: [ new OO.ui.MenuOptionWidget({ data: '', label: 'Other:' }) ],
                },
            });

            this.revisiondelete_reason_button = new OO.ui.ButtonWidget({ label: 'Append' });

            this.revisiondelete_reason_input = new OO.ui.TextInputWidget({
                placeholder: 'full rationale to submit',
            });

            this.revisiondelete_fieldset.addItems([
                new OO.ui.FieldLayout(
                    this.revisiondelete_checkbox, {
                        label: 'Enable revision deletion',
                        align: 'inline',
                        classes: ['p4-aio-checkbox', 'p4-aio-checkbox-block'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.revisiondelete_content_checkbox, {
                        label: 'Hide revision content',
                        align: 'inline',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.revisiondelete_summary_checkbox, {
                        label: 'Hide summaries',
                        align: 'inline',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.revisiondelete_username_checkbox, {
                        label: 'Hide username',
                        align: 'inline',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
                new OO.ui.FieldLayout(
                    this.revisiondelete_oversight_checkbox, {
                        label: 'Oversight',
                        align: 'inline',
                        help: 'needs "suppressrevision" right, or your action will fail',
                        classes: ['p4-aio-checkbox'],
                    }
                ),
                new OO.ui.FieldLayout(
                    new OO.ui.Widget({
                        content: [
                            this.revisiondelete_reason_dropdown,
                            this.revisiondelete_reason_button,
                            this.revisiondelete_reason_input,
                        ]
                    }), {
                        label: 'Reason',
                        classes: ['p4-aio-reason'],
                    }
                ),
            ]);

            // Update here if new fieldsets are introduced
            for (let content of [
                this.top_fieldset, this.rollback_fieldset, this.block_fieldset,
                this.pagedelete_fieldset, this.revisiondelete_fieldset,
            ]) {
                this.panel.$element.append(content.$element);
            }
            this.$body.append(this.panel.$element);

            // CSS styles
            // Make checkboxes stay in the same line if possible ...
            $('.p4-aio-checkbox').css({
                display: 'inline-block',
                'margin-right': '12px',
                // workaround: the height of checkboxes with a tooltip are different from those without one
                // thus set height of every checkbox to 21px to align them
                height: '21px',
            });
            // ... except those marked as block
            $('.p4-aio-checkbox.p4-aio-checkbox-block').css({
                display: 'block',
                'font-weight': 'bold',
            });
            // The header consumes too much space by default (40%)
            $('.p4-aio-form .oo-ui-fieldLayout-align-left .oo-ui-fieldLayout-header').css({
                width: '20%',
            });
            $('.p4-aio-form .oo-ui-fieldLayout-align-left .oo-ui-fieldLayout-field').css({
                width: '80%',
            });
            // Border for rollback, block, etc.
            $('.p4-aio-form .oo-ui-fieldsetLayout:nth-child(n+2)').css({
                border: '1px solid #a2a9b1',
                'border-radius': '2px',
                padding: '12px',
            });
            // Make the button nowrap in reason fields and the text input be in the new line
            $('.p4-aio-reason .oo-ui-fieldLayout-field > .oo-ui-widget').css({
                display: 'flex',
                'flex-wrap': 'wrap',
            });
            $('.p4-aio-reason .oo-ui-fieldLayout-field > .oo-ui-widget > .oo-ui-dropdownWidget').css({
                'flex-basis': 0,
                'flex-grow': 1,
            });
            $('.p4-aio-reason .oo-ui-fieldLayout-field > .oo-ui-widget > .oo-ui-buttonWidget').css({
                'margin-right': 0,
            });
            $('.p4-aio-reason .oo-ui-fieldLayout-field > .oo-ui-widget > .oo-ui-textInputWidget').css({
                'flex-basis': '100%',
            });
        }

        AIODialog.prototype.getActionProcess = function(action) {
            if (action === 'start') {
                return new OO.ui.Process(function() {
                    // Load config
                    config.username = this.top_username_input.getValue();
                    if (!config.username) return;
 
                    config.suffix = this.top_suffix_dropdown.getMenu().findSelectedItem().getData();
                    config.isIP = mw.util.isIPAddress(config.username);
                    config.endtime = getValueFromDropdownAndInput('top_endtime');
        
                    config.rollback = this.rollback_checkbox.isSelected();
                    config.rollbackBot = this.rollback_bot_checkbox.isSelected();
                    config.rollbackShow = this.rollback_showname_checkbox.isSelected();

                    config.block = this.block_checkbox.isSelected();
                    config.blockDur = getValueFromDropdownAndInput('block_duration');
                    config.blockReason = getValueFromDropdownAndInput('block_reason', other_data = '', check_include = true);
                    config.blockReason += config.suffix;
                    config.blockAnon = config.isIP && !this.block_hardblock_checkbox.isSelected();
                    config.blockAuto = !config.isIP && this.block_autoblock_checkbox.isSelected();
                    config.blockCreate = this.block_create_checkbox.isSelected();
                    config.blockTalk = this.block_talk_checkbox.isSelected();
                    config.blockMail = this.block_mail_checkbox.isSelected();
                    config.blockHide = this.block_hidename_checkbox.isSelected();
        
                    config.massdel = this.pagedelete_checkbox.isSelected();
                    config.massdelReason = getValueFromDropdownAndInput('pagedelete_reason', other_data = '', check_include = true);
                    config.massdelReason += config.suffix;

                    config.rd = this.revisiondelete_checkbox.isSelected();
                    config.rdHides = '';
                    if (this.revisiondelete_content_checkbox.isSelected()) config.rdHides += 'content|';
                    if (this.revisiondelete_summary_checkbox.isSelected()) config.rdHides += 'comment|';
                    if (this.revisiondelete_username_checkbox.isSelected()) config.rdHides += 'user|';
                    config.rdReason = getValueFromDropdownAndInput('revisiondelete_reason', other_data = '', check_include = true);
                    config.rdReason += config.suffix;
                    config.os = this.revisiondelete_oversight_checkbox.isSelected();

                    // Begin execution
                    work();

                    this.close();
                }, this);
            }
            return AIODialog.super.prototype.getActionProcess.call(this, action);
        };

        // Set dialog width to be the same as its panel
        AIODialog.prototype.getBodyWidth = function() {
            return this.panel.$element.outerWidth(true);
        };

        $(document.body).append(windowManager.$element);
        aio_dialog = new AIODialog({
            size: 'large',
        });
        windowManager.addWindows([aio_dialog]);
    }

    // If `data` exists in dropdown, select it;
    // else fill the input box and select "other" (provided by `other_data`)
    const selectDropdownOrFillInput = function(field, data, other_data = 'other') {
        let dropdown = aio_dialog[field + '_dropdown'];
        let input = aio_dialog[field + '_input'];
        if (!dropdown || !input) return;
        let menu = dropdown.getMenu();
        if (!menu) return;

        menu.selectItemByData(data);
        if (menu.findSelectedItem() === null) {
            menu.selectItemByData(other_data);
            input.setValue(data);
        }
    }

    // If `data` selected is other, use the value from input box
    const getValueFromDropdownAndInput = function(field, other_data = 'other', check_include = false) {
        let dropdown = aio_dialog[field + '_dropdown'];
        let input = aio_dialog[field + '_input'];
        if (!dropdown || !input) return;
        let menu = dropdown.getMenu();
        if (!menu) return;

        let selected = menu.findSelectedItem().getData();
        let text = input.getValue();
        if (check_include) {
            if (text.includes(selected)) {
                // Do not include reason in dropdown again
                return text;
            } else {
                // Construct a reason like "dropdown: input"
                return selected + (text ? ': ' + text : '');
            }
        }
        return selected && selected === other_data ? text : selected;
    }

    $(mw.util.addPortletLink('p-cactions', '#', 'All-in-one')).click(function (e) {
        init();

        // Avoid re-loading config when user closed and opens the dialog again
        if (loaded) {
            windowManager.openWindow(aio_dialog, {});
            return;
        }
        loaded = true;
        aio_dialog.package_loaded = false;

        if (typeof(p4js_all_in_one) !== 'object') p4js_all_in_one = {};
        // load drop down reasons
        let suffixes = p4js_all_in_one.suffixes || ["", " (global sysop action)", " (stewards action)"];
        let reasons = p4js_all_in_one.reasons || {
            block: ["Long-term abuse", "Vandalism-only account", "Spam-only account", "Open proxy", "Vandalism"],
            pagedelete: ["Vandalism", "Spam"],
            revisiondelete: ["Blatant offensive materials", "Violation of copyright policy", "Private information"],
        };
        for (let suffix of suffixes) {
            if (!suffix) continue; 
            aio_dialog.top_suffix_dropdown.getMenu().addItems(new OO.ui.MenuOptionWidget({
                data: suffix,
                label: suffix,
            }));
        }
        aio_dialog.top_suffix_dropdown.getMenu().addItems(
            suffixes
                .filter(e => e)  // empty suffix is already included in dropdown
                .map(e => new OO.ui.MenuOptionWidget({
                    data: e,
                    label: e,
                }))
        );
        // Select the empty suffix by default
        aio_dialog.top_suffix_dropdown.getMenu().selectItemByData('');
        for (let action of ['block', 'pagedelete', 'revisiondelete']) {
            aio_dialog[action + '_reason_dropdown'].getMenu().addItems(
                (reasons[action] || []).map(e => new OO.ui.MenuOptionWidget({
                    data: e,
                    label: e,
                })),
                0  // Prepend to option list
            );
        }

        // load packages (or the default one)
        const default_package = {
            tracingedits: {
                duration: 3600,
                indefregistered: true,
            },
            rollback: {
                enabled: true,
                bot: false,
                showname: true,
            },
            block: {
                enabled: true,
                duration: "1 day",
                indefregistered: true,
                reason: "Vandalism",
                autoblock: true,
                hardblock: false,
                create: true,
                talk: false,
                mail: false,
                hidename: false,
            },
            pagedelete: {
                enabled: true,
                reason: "Vandalism",
            },
            revisiondelete: {
                enabled: false,
                content: true,
                summary: true,
                username: false,
                reason: "Blatant offensive materials",
                oversight: false,
            },
        };
        let packages = p4js_all_in_one.packages || {};
        if (!packages.Default) {
            packages.Default = default_package;
        }
        aio_dialog.top_package_dropdown.getMenu().addItems(
            Object.keys(packages).map(e => new OO.ui.MenuOptionWidget({
                data: e,
                label: e,
            })),
            0
        );

        // When dropdown changes (manually or automatically), decide whether to hide corresponding text input or not
        for (let field of ['top_endtime', 'block_duration']) {
            ['choose', 'select'].forEach(e => aio_dialog[field + '_dropdown'].getMenu().on(e, function(item) {
                aio_dialog[field + '_input'].toggle(item.getData() === 'other');
            }));
        }

        // When package changes (manually or automatically), change various config
        ['choose', 'select'].forEach(e => aio_dialog.top_package_dropdown.getMenu().on(e, function(item) {
            // Avoid config being changed when user closed and opens the dialog again
            if (e == 'select' && aio_dialog.package_loaded) return;
            aio_dialog.package_loaded = true;

            let isIP = mw.util.isIPAddress(aio_dialog.top_username_input.getValue());
            let pkg = packages[item.getData()] || {};

            // checkboxes
            for (let [action, action_config] of Object.entries(pkg)) {
                for (let [item, item_enabled] of Object.entries(action_config)) {
                    if (typeof item_enabled === "boolean" && item !== "enabled" && item !== "indefregistered") {
                        aio_dialog[`${action}_${item}_checkbox`].setSelected(item_enabled);
                    }
                }
            }

            // tracing edits
            pkg.tracingedits = pkg.tracingedits || {};
            selectDropdownOrFillInput('top_endtime', pkg.tracingedits.duration || 3600);
            if (pkg.tracingedits.indefregistered && !isIP) {
                aio_dialog.top_endtime_dropdown.getMenu().selectItemByData('inf');
            }

            // rollback: default is enabled
            aio_dialog.rollback_checkbox.setSelected(pkg.rollback.enabled || true);

            // block
            pkg.block = pkg.block || {};
            aio_dialog.block_checkbox.setSelected(pkg.block.enabled || false);
            selectDropdownOrFillInput('block_duration', pkg.block.duration || '1 day');
            if (pkg.tracingedits.indefregistered && !isIP) {
                aio_dialog.block_duration_dropdown.getMenu().selectItemByData('never');
            }
            selectDropdownOrFillInput('block_reason', pkg.block.reason || 'Long-term abuse', '');

            // page deletion
            pkg.pagedelete = pkg.pagedelete || {};
            aio_dialog.pagedelete_checkbox.setSelected(pkg.pagedelete.enabled || false);
            selectDropdownOrFillInput('pagedelete_reason', pkg.pagedelete.reason || 'Vandalism', '');

            // revision deletion
            pkg.revisiondelete = pkg.revisiondelete || {};
            aio_dialog.revisiondelete_checkbox.setSelected(pkg.revisiondelete.enabled || false);
            selectDropdownOrFillInput('revisiondelete_reason', pkg.revisiondelete.reason || 'Hiding blatant offending materials', '');
        }));

        // select the default package
        if (p4js_all_in_one.default_package && packages[p4js_all_in_one.default_package]) {
            aio_dialog.top_package_dropdown.getMenu().selectItemByData(p4js_all_in_one.default_package);
        } else {
            aio_dialog.top_package_dropdown.getMenu().selectItemByData('Default');
        }

        // Append reasons
        for (let action of ['pagedelete', 'revisiondelete']) {
            aio_dialog[action + '_reason_button'].on('click', function() {
                let text = aio_dialog[action + '_reason_input'].getValue();
                aio_dialog[action + '_reason_input'].setValue(text + '; ' + aio_dialog[action + '_reason_dropdown'].getMenu().findSelectedItem().getData());
                aio_dialog[action + '_reason_dropdown'].getMenu().selectItemByData('');
            })
        }

        aio_dialog.top_username_input.on('change', function(username) {
            let isIP = mw.util.isIPAddress(username);
            let pkg = packages[aio_dialog.top_package_dropdown.getMenu().findSelectedItem().getData()] || default_package;
            // Check indef
            if (pkg.tracingedits && pkg.tracingedits.indefregistered && !isIP) {
                aio_dialog.top_endtime_dropdown.getMenu().selectItemByData('inf');
                aio_dialog.block_duration_dropdown.getMenu().selectItemByData('never');
            } else {
                pkg.tracingedits = pkg.tracingedits || {};
                pkg.block = pkg.block || {};
                selectDropdownOrFillInput('top_endtime', pkg.tracingedits.duration || 3600);
                selectDropdownOrFillInput('block_duration', pkg.block.duration || '1 day');
            }
            // Anon block / registered user block settings
            if (isIP) {
                $('.p4-aio-reg-only').hide();
                $('.p4-aio-anon-only').show();
            } else {
                $('.p4-aio-reg-only').show();
                $('.p4-aio-anon-only').hide();
            }
        });

        // Make fieldset transparent if not enabled
        for (let action of ['rollback', 'block', 'pagedelete', 'revisiondelete']) {
            aio_dialog[action + '_checkbox'].on('change', function(selected, _) {
                $(`.p4-aio-${action}-fieldset`).css({
                    opacity: selected ? '100%': '50%',
                });
            });
            // Initialize
            $(`.p4-aio-${action}-fieldset`).css({
                opacity: aio_dialog[action + '_checkbox'].isSelected() ? '100%': '50%',
                transition: 'opacity 0.3s',
            });
        }

        // enter to submit
        $(document).on("keyup", event => {
            if(event.key !== "Enter" || !aio_dialog.isOpened()) return;
            aio_dialog.executeAction('start');
            event.preventDefault();
        });

        // fill the current username if applicable
        aio_dialog.top_username_input.setValue(mw.config.get('wgRelevantUserName') || '');
        aio_dialog.top_username_input.focus();

        windowManager.openWindow(aio_dialog, {});
    });
    
    });
});