User:BZPN/MassRollback2.js: Difference between revisions

From Test Wiki
Jump to navigation Jump to search
Content deleted Content added
BZPN (talk | contribs)
No edit summary
BZPN (talk | contribs)
No edit summary
 
(One intermediate revision by the same user not shown)
Line 537: Line 537:
mw.notify('Successfully rolled back 1 edit.', { type: 'success' });
mw.notify('Successfully rolled back 1 edit.', { type: 'success' });
if (confirmResult.leaveWarning) {
if (confirmResult.leaveWarning) {
await self.leaveUserWarning(confirmResult.warningType);
await self.leaveUserWarning(confirmResult.warningType, edit.user || null);
}
}
if (confirmResult.reportVIP) {
if (confirmResult.reportVIP) {
Line 560: Line 560:
title = ($plink.attr('title') || '').trim();
title = ($plink.attr('title') || '').trim();
}
}
// Try to get username from the RC line
// Try to get username (author of the edit) from the RC line
let user = $line.find('.mw-userlink').first().text().trim();
let user = '';
// Prefer the direct user page link, not talk/contribs
const $userAnchor = $line.find('a.mw-userlink[href*="/wiki/User:"]:not([href*="User_talk"]):not([href*="Special:Contributions"])').first();
if ($userAnchor.length) {
user = ($userAnchor.find('bdi').text() || $userAnchor.text() || '').trim();
}
// Fallback: from talk link href
if (!user) {
if (!user) {
const $ulink = $line.find('a[href*="/wiki/User:"] , a.mw-userlink').first();
const $talk = $line.find('a[href*="/wiki/User_talk:"]').first();
user = ($ulink.text() || '').trim();
if ($talk.length) {
const href = $talk.attr('href');
const m = href && href.match(/User_talk:([^?#]+)/);
if (m) user = decodeURIComponent(m[1]);
}
}
// Fallback: from Special:Contributions link href
if (!user) {
const $contribs = $line.find('a[href*="Special:Contributions/"]').first();
if ($contribs.length) {
const href = $contribs.attr('href');
const m = href && href.match(/Special:Contributions\/([^?#]+)/);
if (m) user = decodeURIComponent(m[1]);
}
}
}
user = user || null;
if (revid && parentid && title) {
if (revid && parentid && title) {
return { title: title, revid: revid, parentid: parentid, timestamp: new Date().toISOString(), user: user || null };
return { title: title, revid: revid, parentid: parentid, timestamp: new Date().toISOString(), user: user || null };
Line 1,012: Line 1,032:
mw.notify(`Successfully rolled back ${selected.length} edits.`, { type: 'success' });
mw.notify(`Successfully rolled back ${selected.length} edits.`, { type: 'success' });
if (confirmResult.leaveWarning) {
if (confirmResult.leaveWarning) {
const targetUser = (selected[0] && selected[0].user) ? selected[0].user : null;
await this.leaveUserWarning(confirmResult.warningType);
await this.leaveUserWarning(confirmResult.warningType, targetUser);
}
}
if (confirmResult.reportVIP) {
if (confirmResult.reportVIP) {
Line 1,044: Line 1,065:
mw.notify(`Successfully rolled back ${selected.length} filtered edits.`, { type: 'success' });
mw.notify(`Successfully rolled back ${selected.length} filtered edits.`, { type: 'success' });
if (confirmResult.leaveWarning) {
if (confirmResult.leaveWarning) {
const targetUser = (selected[0] && selected[0].user) ? selected[0].user : null;
await this.leaveUserWarning(confirmResult.warningType);
await this.leaveUserWarning(confirmResult.warningType, targetUser);
}
}
if (confirmResult.reportVIP) {
if (confirmResult.reportVIP) {
Line 1,078: Line 1,100:
mw.notify(`Successfully rolled back all ${contributions.length} edits.`, { type: 'success' });
mw.notify(`Successfully rolled back all ${contributions.length} edits.`, { type: 'success' });
if (confirmResult.leaveWarning) {
if (confirmResult.leaveWarning) {
const targetUser = (contributions[0] && contributions[0].user) ? contributions[0].user : null;
await this.leaveUserWarning(confirmResult.warningType);
await this.leaveUserWarning(confirmResult.warningType, targetUser);
}
}
if (confirmResult.reportVIP) {
if (confirmResult.reportVIP) {
Line 1,153: Line 1,176:


// Leave a talk page warning using a chosen template (subst) under current month section
// Leave a talk page warning using a chosen template (subst) under current month section
leaveUserWarning: async function(templateName) {
leaveUserWarning: async function(templateName, targetUser) {
const api = new mw.Api();
const api = new mw.Api();
const userName = mw.config.get('wgRelevantUserName');
const userName = targetUser || mw.config.get('wgRelevantUserName');
if (!userName) {
mw.notify('Could not determine user name for talk page warning.', { type: 'error' });
return;
}
const title = `User talk:${userName}`;
const title = `User talk:${userName}`;



Latest revision as of 09:56, 13 September 2025

(function($, mw) {
    'use strict';

    const MassRollback = {
        init: function() {
            const page = mw.config.get('wgCanonicalSpecialPageName');
            if (page === 'Contributions') {
                this.injectStyles();
                this.createUI();
                this.bindEvents();
                this.fetchUserStats();
            } else if (page === 'Recentchanges') {
                this.injectStyles();
                this.createRCUI();
                this.bindRCEvents();
                this.enhanceRecentChanges();
            } else {
                return;
            }
        },

        injectStyles: function() {
            const styles = `
                /* Container as a clean drawer */
                #mass-rollback-details {
                    border: 1px solid #c8ccd1;
                    border-radius: 2px;
                    width: 100%;
                    max-width: none; /* stretch across the content area */
                    background: #fff; /* white panel */
                    font-size: 14px;
                    font-family: sans-serif; /* match the example */
                    margin-bottom: 20px;
                }
                #mass-rollback-details summary {
                    padding: .6em 1em;
                    cursor: pointer;
                    font-weight: 600;
                    list-style: none;
                    display: flex;
                    align-items: center;
                    gap: .5em;
                    color: #202122;
                }
                #mass-rollback-details summary::-webkit-details-marker { display: none; }
                #mass-rollback-details summary::before {
                    content: "";
                    display: inline-block;
                    border-left: 5px solid transparent;
                    border-right: 5px solid transparent;
                    border-top: 6px solid #202122;
                    transition: transform .2s ease;
                    margin-right: .2em;
                }
                #mass-rollback-details[open] summary::before { transform: rotate(180deg); }
                #mass-rollback-container, #filtered-edits-container {
                    padding: 10px 16px 16px 16px;
                }

                /* Sections */
                .mr-section h4 { margin: 0 0 .4em 0; font-weight: 600; }
                .mr-section + .mr-section { margin-top: 14px; }
                .mr-row { margin: .6em 0; display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; }
                .mr-row label { margin-right: 6px; color: #202122; }

                /* Inputs */
                .ooui-input, .ooui-select {
                    width: 100%;
                    max-width: 360px;
                    box-sizing: border-box;
                    border: 1px solid #a2a9b1;
                    border-radius: 2px;
                    padding: .45em .6em;
                    font-size: 14px;
                    background: #fff;
                }
                input[type="checkbox"] { margin-right: .4em; }

                /* Buttons – keep color scheme */
                .ooui-button {
                    display: inline-block;
                    padding: 6px 12px;
                    border: 1px solid #a2a9b1;
                    border-radius: 2px;
                    background: #f8f9fa;
                    cursor: pointer;
                    font-size: 14px;
                    line-height: 1.5;
                }
                .ooui-button:hover { background: #fff; border-color: #72777d; }
                .ooui-button.primary { background: #36c; color: #fff; border-color: #36c; }
                .ooui-button.primary:hover { background: #447ff5; border-color: #447ff5; }
                .ooui-button.danger { background: #d33; color: #fff; border-color: #d33; }
                .ooui-button.danger:hover { background: #c00; border-color: #c00; }

                /* Table */
                #filtered-edits-table { width:100%; border-collapse: collapse; background: #fff; }
                #filtered-edits-table th, #filtered-edits-table td {
                    border: 1px solid #a2a9b1;
                    padding: 8px;
                    font-size: 13px;
                }
                #filtered-edits-table th { background: #eaecf0; text-align: left; }

                /* Responsive table */
                @media (max-width: 600px) {
                    #filtered-edits-table, #filtered-edits-table thead, #filtered-edits-table tbody, #filtered-edits-table th, #filtered-edits-table td, #filtered-edits-table tr {
                        display: block;
                    }
                    #filtered-edits-table tr { margin-bottom: 10px; }
                    #filtered-edits-table td { border: none; position: relative; padding-left: 50%; }
                    #filtered-edits-table td:before {
                        position: absolute;
                        top: 0; left: 0;
                        width: 45%; padding-left: 5px;
                        font-weight: bold;
                        white-space: nowrap;
                    }
                    #filtered-edits-table td:nth-of-type(1):before { content: "Revid"; }
                    #filtered-edits-table td:nth-of-type(2):before { content: "Title"; }
                    #filtered-edits-table td:nth-of-type(3):before { content: "Timestamp"; }
                    #filtered-edits-table td:nth-of-type(4):before { content: "Namespace"; }
                    #filtered-edits-table td:nth-of-type(5):before { content: "Size"; }
                    #filtered-edits-table td:nth-of-type(6):before { content: "Select"; }
                }
            `;
            $('<style>').text(styles).appendTo('head');
        },

        createUI: function() {
            const $ui = $(`
                <details id="mass-rollback-details" open>
                    <summary>Mass rollback</summary>
                    <div id="mass-rollback-container">
                        <div id="stats-section" class="mr-section">
                            <h4>User statistics</h4>
                            <p>Total edits: <strong id="total-edits">-</strong></p>
                            <p>First edit: <span id="first-edit">-</span></p>
                            <p>Latest edit: <span id="last-edit">-</span>
                                <button id="refresh-stats" class="ooui-button">Refresh</button>
                            </p>
                        </div>
                        <div id="filters-section" class="mr-section">
                            <h4>Filter edits</h4>
                            <div class="mr-row">
                                <label>Date from:</label>
                                <input type="date" id="start-date" class="ooui-input">
                                <label>to:</label>
                                <input type="date" id="end-date" class="ooui-input">
                            </div>
                            <div class="mr-row">
                                <label>Namespace:</label>
                                <select id="namespace-filter" multiple class="ooui-select">
                                    <option value="all">All</option>
                                    <option value="0">Main</option>
                                    <option value="1">Talk</option>
                                    <option value="2">User</option>
                                    <option value="3">User talk</option>
                                    <option value="4">Project</option>
                                    <option value="5">Project talk</option>
                                    <option value="6">File</option>
                                    <option value="7">File talk</option>
                                    <option value="8">MediaWiki</option>
                                    <option value="9">MediaWiki talk</option>
                                    <option value="10">Template</option>
                                    <option value="11">Template talk</option>
                                    <option value="12">Help</option>
                                    <option value="13">Help talk</option>
                                    <option value="14">Category</option>
                                    <option value="15">Category talk</option>
                                    <option value="100">Portal</option>
                                    <option value="101">Portal talk</option>
                                </select>
                            </div>
                            <div class="mr-row">
                                <label>Edit size:</label>
                                <select id="size-filter" class="ooui-select">
                                    <option value="">Any</option>
                                    <option value="small">Small (<50 bytes)</option>
                                    <option value="medium">Medium (50-500 bytes)</option>
                                    <option value="large">Large (>500 bytes)</option>
                                </select>
                            </div>
                            <div class="mr-row">
                                <label>Sort order:</label>
                                <select id="sort-order" class="ooui-select">
                                    <option value="desc">Latest first</option>
                                    <option value="asc">Oldest first</option>
                                </select>
                            </div>
                            <div class="mr-row">
                                <button id="clear-filters" class="ooui-button">Clear filters</button>
                                <button id="show-filtered" class="ooui-button primary">Show filtered edits</button>
                            </div>
                        </div>
                        <div id="reason-section" class="mr-section">
                            <h4>Rollback reason</h4>
                            <select id="rollback-reason" class="ooui-select">
                                <option value=""></option>
                                <option value="vandalism">Vandalism</option>
                                <option value="spam">Adding spam links</option>
                                <option value="advert">Advertising or promotion</option>
                                <option value="unsourced">Unsourced or improperly cited material</option>
                                <option value="copyright">Copyright violation</option>
                                <option value="blanking">Removal of content, blanking</option>
                                <option value="wronginfo">Adding wrong information</option>
                                <option value="other">Other</option>
                            </select>
                            <input type="text" id="custom-reason" placeholder="Enter custom reason" class="ooui-input" style="display:none; width:100%; margin-top:6px;">
                        </div>
                        <div id="actions-section" class="mr-section" style="text-align:center;">
                            <button id="rollback-selected" class="ooui-button primary">Rollback selected</button>
                            <button id="rollback-all" class="ooui-button danger">Rollback all</button>
                        </div>
                        <div id="filtered-edits-container" class="mr-section" style="display:none;">
                            <h4>Filtered edits</h4>
                            <div class="mr-row">
                                <input type="checkbox" id="select-all"> <label for="select-all">Select all</label>
                            </div>
                            <table id="filtered-edits-table">
                                <thead>
                                    <tr>
                                        <th>Revid</th>
                                        <th>Title</th>
                                        <th>Timestamp</th>
                                        <th>Namespace</th>
                                        <th>Size</th>
                                        <th>Select</th>
                                    </tr>
                                </thead>
                                <tbody></tbody>
                            </table>
                            <div style="text-align:center; margin-top:10px;">
                                <button id="rollback-selected-filtered" class="ooui-button primary">Rollback selected filtered edits</button>
                            </div>
                        </div>
                    </div>
                </details>
            `);

            // Put the drawer at the top of the content area
            $('#mw-content-text').prepend($ui);
        },

        // RC version: collapsed by default, without statistics section
        createRCUI: function() {
            const $ui = $(`
                <details id="mass-rollback-details">
                    <summary>Mass rollback</summary>
                    <div id="mass-rollback-container">
                        <div id="filters-section" class="mr-section">
                            <h4>Filter edits</h4>
                            <div class="mr-row">
                                <label>Date from:</label>
                                <input type="date" id="start-date" class="ooui-input">
                                <label>to:</label>
                                <input type="date" id="end-date" class="ooui-input">
                            </div>
                            <div class="mr-row">
                                <label>Namespace:</label>
                                <select id="namespace-filter" multiple class="ooui-select">
                                    <option value="all">All</option>
                                    <option value="0">Main</option>
                                    <option value="1">Talk</option>
                                    <option value="2">User</option>
                                    <option value="3">User talk</option>
                                    <option value="4">Project</option>
                                    <option value="5">Project talk</option>
                                    <option value="6">File</option>
                                    <option value="7">File talk</option>
                                    <option value="8">MediaWiki</option>
                                    <option value="9">MediaWiki talk</option>
                                    <option value="10">Template</option>
                                    <option value="11">Template talk</option>
                                    <option value="12">Help</option>
                                    <option value="13">Help talk</option>
                                    <option value="14">Category</option>
                                    <option value="15">Category talk</option>
                                    <option value="100">Portal</option>
                                    <option value="101">Portal talk</option>
                                </select>
                            </div>
                            <div class="mr-row">
                                <label>Edit size:</label>
                                <select id="size-filter" class="ooui-select">
                                    <option value="">Any</option>
                                    <option value="small">Small (<50 bytes)</option>
                                    <option value="medium">Medium (50-500 bytes)</option>
                                    <option value="large">Large (>500 bytes)</option>
                                </select>
                            </div>
                            <div class="mr-row">
                                <label>Sort order:</label>
                                <select id="sort-order" class="ooui-select">
                                    <option value="desc">Latest first</option>
                                    <option value="asc">Oldest first</option>
                                </select>
                            </div>
                            <div class="mr-row">
                                <button id="clear-filters" class="ooui-button">Clear filters</button>
                                <button id="show-filtered" class="ooui-button primary">Show filtered edits</button>
                            </div>
                        </div>
                        <div id="reason-section" class="mr-section">
                            <h4>Rollback reason</h4>
                            <select id="rollback-reason" class="ooui-select">
                                <option value=""></option>
                                <option value="vandalism">Vandalism</option>
                                <option value="spam">Adding spam links</option>
                                <option value="advert">Advertising or promotion</option>
                                <option value="unsourced">Unsourced or improperly cited material</option>
                                <option value="copyright">Copyright violation</option>
                                <option value="blanking">Removal of content, blanking</option>
                                <option value="wronginfo">Adding wrong information</option>
                                <option value="other">Other</option>
                            </select>
                            <input type="text" id="custom-reason" placeholder="Enter custom reason" class="ooui-input" style="display:none; width:100%; margin-top:6px;">
                        </div>
                    </div>
                </details>
            `);
            // Insert after RC filters table if exists, else at top
            const $anchor = $('#mw-rcfilters-ui-table');
            if ($anchor.length) {
                $anchor.after($ui);
            } else {
                $('#mw-content-text').prepend($ui);
            }
        },

        bindEvents: function() {
            $('#rollback-reason').on('change', function() {
                $('#custom-reason').toggle($(this).val() === 'other');
            });
            $('#clear-filters').on('click', function() {
                $('#start-date').val('');
                $('#end-date').val('');
                $('#namespace-filter').val(['all']);
                $('#size-filter').val('');
                $('#sort-order').val('desc');
            });
            $('#refresh-stats').on('click', this.fetchUserStats.bind(this));
            $('#show-filtered').on('click', this.displayFilteredEdits.bind(this));
            $('#rollback-selected-filtered').on('click', this.rollbackSelectedFromFiltered.bind(this));
            $('#rollback-selected').on('click', this.rollbackSelected.bind(this));
            $('#rollback-all').on('click', this.rollbackAll.bind(this));
            $(document).on('change', '#select-all', function() {
                const checked = $(this).is(':checked');
                $('#filtered-edits-table tbody input[type="checkbox"]').prop('checked', checked);
            });
            $('li[data-mw-revid]').each(function() {
                const $li = $(this);
                const revid = $li.data('mw-revid');
                const parentid = $li.data('mw-prev-revid');
                const title = $li.find('.mw-contributions-title').text().trim();
                const $checkbox = $('<input>', {
                    type: 'checkbox',
                    class: 'rollback-checkbox',
                    'data-revid': revid,
                    'data-parentid': parentid,
                    'data-title': title,
                    style: 'margin-right:5px;'
                });
                $li.prepend($checkbox);
            });
        },

        fetchUserStats: function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();

            // 1) Get accurate total edits from 'users' prop
            const userPromise = api.get({
                action: 'query',
                list: 'users',
                ususers: userName,
                usprop: 'editcount'
            });

            // 2) Get first contribution (oldest)
            const firstPromise = api.get({
                action: 'query',
                list: 'usercontribs',
                ucuser: userName,
                uclimit: 1,
                ucprop: 'timestamp',
                ucdir: 'newer'
            });

            // 3) Get last contribution (newest)
            const lastPromise = api.get({
                action: 'query',
                list: 'usercontribs',
                ucuser: userName,
                uclimit: 1,
                ucprop: 'timestamp',
                ucdir: 'older'
            });

            Promise.all([userPromise, firstPromise, lastPromise]).then(([userData, firstData, lastData]) => {
                const user = (userData.query && userData.query.users && userData.query.users[0]) || {};
                const editcount = typeof user.editcount === 'number' ? user.editcount : '-';
                const first = (firstData.query && firstData.query.usercontribs && firstData.query.usercontribs[0]) || null;
                const last = (lastData.query && lastData.query.usercontribs && lastData.query.usercontribs[0]) || null;

                $('#total-edits').text(editcount);
                $('#first-edit').text(first ? new Date(first.timestamp).toLocaleDateString() : '-');
                $('#last-edit').text(last ? new Date(last.timestamp).toLocaleDateString() : '-');
            }).catch(() => {
                // Fallback to previous approach if anything fails
                api.get({
                    action: 'query',
                    list: 'usercontribs',
                    ucuser: userName,
                    uclimit: '5000',
                    ucprop: 'timestamp'
                }).done(function(data) {
                    const contributions = data.query.usercontribs || [];
                    $('#total-edits').text(contributions.length || '-');
                    if (contributions.length > 0) {
                        const timestamps = contributions.map(c => new Date(c.timestamp)).sort((a,b) => a-b);
                        $('#first-edit').text(timestamps[0].toLocaleDateString());
                        $('#last-edit').text(timestamps[timestamps.length-1].toLocaleDateString());
                    } else {
                        $('#first-edit').text('-');
                        $('#last-edit').text('-');
                    }
                });
            });
        },

        getNamespaceName: function(ns) {
            const mapping = {0:'Main',1:'Talk',2:'User',3:'User talk',4:'Project',5:'Project talk',6:'File',7:'File talk',8:'MediaWiki',9:'MediaWiki talk',10:'Template',11:'Template talk',12:'Help',13:'Help talk',14:'Category',15:'Category talk',100:'Portal',101:'Portal talk'};
            return mapping[ns] || ns;
        },

        applyFilters: function(contribs) {
            const start = $('#start-date').val() ? new Date($('#start-date').val()) : null;
            const end = $('#end-date').val() ? new Date($('#end-date').val()) : null;
            let nsFilter = $('#namespace-filter').val() || [];
            const sizeFilter = $('#size-filter').val();
            const sortOrder = $('#sort-order').val();
            if (nsFilter.includes('all')) nsFilter = [];
            let filtered = contribs.filter(c => {
                const d = new Date(c.timestamp);
                if (start && d < start) return false;
                if (end && d > end) return false;
                if (nsFilter.length && !nsFilter.includes(c.ns.toString())) return false;
                if (sizeFilter) {
                    const s = c.size || 0;
                    if (sizeFilter === 'small' && s >= 50) return false;
                    if (sizeFilter === 'medium' && (s < 50 || s > 500)) return false;
                    if (sizeFilter === 'large' && s <= 500) return false;
                }
                return true;
            });
            filtered.sort((a,b) => sortOrder==='asc'? new Date(a.timestamp)-new Date(b.timestamp) : new Date(b.timestamp)-new Date(a.timestamp));
            return filtered;
        },

        displayFilteredEdits: function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();
            api.get({
                action:'query',
                list:'usercontribs',
                ucuser:userName,
                uclimit:'5000',
                ucprop:'ids|title|timestamp|size|ns|parentid'
            }).done((data) => {
                const filtered = this.applyFilters(data.query.usercontribs);
                const $tbody = $('#filtered-edits-table tbody').empty();
                if (!filtered.length) {
                    $tbody.append('<tr><td colspan="6" style="text-align:center;">No edits match the selected filters.</td></tr>');
                } else {
                    filtered.forEach(c => {
                        $tbody.append(`
                            <tr>
                                <td>${c.revid}</td>
                                <td>${c.title}</td>
                                <td>${new Date(c.timestamp).toLocaleString()}</td>
                                <td>${this.getNamespaceName(c.ns)}</td>
                                <td>${c.size||0}</td>
                                <td style="text-align:center;"><input type="checkbox" class="filtered-rollback-checkbox" data-revid="${c.revid}" data-parentid="${c.parentid}" data-title="${c.title}"></td>
                            </tr>`);
                    });
                }
                $('#filtered-edits-container').slideDown();
            });
        },

        // RC-only bindings
        bindRCEvents: function() {
            $('#rollback-reason').on('change', function() {
                $('#custom-reason').toggle($(this).val() === 'other');
            });
        },

        // Enhance RC entries by adding a red Rollback button after the thanks link
        enhanceRecentChanges: function() {
            const self = this;
            const $rcRoot = $('#mw-content-text');
            $rcRoot.find('.mw-changeslist .mw-changeslist-line, .mw-changeslist li').each(function() {
                const $line = $(this);
                if ($line.data('mr-enhanced')) return;
                $line.data('mr-enhanced', true);

                const $anchor = $line.find('.mw-thanks-thank-link').last();
                const $btn = $('<button class="ooui-button danger rc-rollback-btn" style="margin-left:8px;">Rollback</button>');
                if ($anchor.length) $anchor.after($btn); else $line.append($btn);

                const parsed = self._extractRCEdit($line);
                if (!parsed) {
                    $btn.prop('disabled', true).attr('title', 'Unable to get revid/parentid');
                    return;
                }
                $btn.data('mr-edit', parsed);

                $btn.on('click', async function() {
                    const confirmResult = await self.confirmActionWithReason('Rollback this edit from Recent changes?', 1);
                    if (!confirmResult || !confirmResult.confirm) return;

                    if (confirmResult.reasonValue) {
                        $('#rollback-reason').val(confirmResult.reasonValue).trigger('change');
                    }
                    if (confirmResult.reasonValue === 'other') {
                        $('#custom-reason').val(confirmResult.customReason || '');
                    }

                    try {
                        const edit = $(this).data('mr-edit');
                        // If username missing from config, try from RC line extraction
                        if (!mw.config.get('wgRelevantUserName') && edit.user) {
                            mw.config.set('wgRelevantUserName', edit.user);
                        }
                        await self.performRollback([edit]);
                        mw.notify('Successfully rolled back 1 edit.', { type: 'success' });
                        if (confirmResult.leaveWarning) {
                            await self.leaveUserWarning(confirmResult.warningType, edit.user || null);
                        }
                        if (confirmResult.reportVIP) {
                            await self.performVIPReport(confirmResult.vip);
                        }
                    } catch (e) {
                        mw.notify('Error during rollback.', { type: 'error' });
                        console.error(e);
                    }
                });
            });
        },

        // Try to extract edit metadata (title, revid, parentid) from an RC line
        _extractRCEdit: function($line) {
            const revid = $line.data('mw-revid') || $line.attr('data-mw-revid');
            const parentid = $line.data('mw-rollback-revision') || $line.attr('data-mw-prev-revid') || $line.attr('data-mw-rollback-revision');
            // Try to get page title
            let title = $line.find('.mw-title, .mw-changeslist-title, a.mw-changeslist-title').first().text().trim();
            if (!title) {
                const $plink = $line.find('a[title]').first();
                title = ($plink.attr('title') || '').trim();
            }
            // Try to get username (author of the edit) from the RC line
            let user = '';
            // Prefer the direct user page link, not talk/contribs
            const $userAnchor = $line.find('a.mw-userlink[href*="/wiki/User:"]:not([href*="User_talk"]):not([href*="Special:Contributions"])').first();
            if ($userAnchor.length) {
                user = ($userAnchor.find('bdi').text() || $userAnchor.text() || '').trim();
            }
            // Fallback: from talk link href
            if (!user) {
                const $talk = $line.find('a[href*="/wiki/User_talk:"]').first();
                if ($talk.length) {
                    const href = $talk.attr('href');
                    const m = href && href.match(/User_talk:([^?#]+)/);
                    if (m) user = decodeURIComponent(m[1]);
                }
            }
            // Fallback: from Special:Contributions link href
            if (!user) {
                const $contribs = $line.find('a[href*="Special:Contributions/"]').first();
                if ($contribs.length) {
                    const href = $contribs.attr('href');
                    const m = href && href.match(/Special:Contributions\/([^?#]+)/);
                    if (m) user = decodeURIComponent(m[1]);
                }
            }
            user = user || null;
            if (revid && parentid && title) {
                return { title: title, revid: revid, parentid: parentid, timestamp: new Date().toISOString(), user: user || null };
            }
            // Fallback from diff link
            const $diff = $line.find('a[href*="diff="][href*="oldid="]').first();
            if ($diff.length) {
                const params = this._parseParams($diff.attr('href'));
                const rv = params.diff || params.curid || params.oldid;
                const pid = params.oldid || params.prev || '';
                if (!title) title = ($diff.attr('title') || '').replace(/^Special:Diff\/\d+/, '').trim();
                if (rv && title) return { title: title, revid: rv, parentid: pid, timestamp: new Date().toISOString(), user: user || null };
            }
            return null;
        },

        _parseParams: function(href) {
            try {
                const u = href.indexOf('http') === 0 ? new URL(href) : new URL(href, location.origin);
                const o = {};
                for (const [k, v] of u.searchParams.entries()) o[k] = v;
                return o;
            } catch (e) { return {}; }
        },

        confirmAction: function(message, count) {
            return new Promise(resolve => {
                const $modal = $(`
                    <div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:9999;">
                        <div style="background:#fff;padding:16px 16px 12px 16px;border-radius:4px;max-width:620px;width:92%;box-shadow:0 2px 12px rgba(0,0,0,0.2);font-family:sans-serif;">
                            <h4 style="margin:0 0 .4em 0;">Confirm rollback</h4>
                            <p style="margin:.2em 0;">${message}</p>
                            <p style="margin:.2em 0; color:#54595d;">Number of edits: <strong>${count}</strong></p>
                            <div style="margin-top:.6em; padding:.6em; border:1px solid #a2a9b1; border-radius:2px; background:#f8f9fa;">
                                <label style="display:flex; align-items:center; gap:.5em;">
                                    <input type="checkbox" id="leave-warning-checkbox"> Leave a message on user's talk page
                                </label>
                                <div id="warning-options" style="display:none; margin-top:.6em;">
                                    <div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
                                        <label>Warning level:</label>
                                        <select id="warning-level" class="ooui-select">
                                            <option value="level1">Level 1</option>
                                            <option value="level2">Level 2</option>
                                            <option value="level3">Level 3</option>
                                            <option value="level4">Level 4</option>
                                            <option value="level4im">Only warning</option>
                                        </select>
                                        <label>Warning type:</label>
                                        <select id="warning-type" class="ooui-select"></select>
                                    </div>
                                </div>
                            </div>
                            <div style="margin-top:.8em; padding:.6em; border:1px solid #a2a9b1; border-radius:2px; background:#f8f9fa;">
                                <label style="display:flex; align-items:center; gap:.5em;">
                                    <input type="checkbox" id="leave-vip-checkbox"> Report at Wikipedia:Vandalism in progress (User-reported)
                                </label>
                                <div id="vip-options" style="display:none; margin-top:.6em;">
                                    <div class="mr-row" style="margin:0 0 .6em 0;">
                                        <label style="min-width:180px;">Primary linked page:</label>
                                        <input type="text" id="vip-page" class="ooui-input" placeholder="Optional: Page title e.g. Article name">
                                    </div>
                                    <div class="mr-row" style="margin:0 0 .6em 0;">
                                        <label style="min-width:180px;">Revision ID when vandalised:</label>
                                        <input type="text" id="vip-badid" class="ooui-input" placeholder="Optional" disabled>
                                    </div>
                                    <div class="mr-row" style="margin:0 0 .6em 0;">
                                        <label style="min-width:180px;">Last good revision ID:</label>
                                        <input type="text" id="vip-goodid" class="ooui-input" placeholder="Optional" disabled>
                                    </div>
                                    <div class="mr-row" style="margin:0 0 .6em 0; align-items:flex-start;">
                                        <label style="min-width:180px;">Reasons:</label>
                                        <div>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="final"> Vandalism after final (level 4 or 4im) warning given</label>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="postblock"> Vandalism after recent (within 1 day) release of block</label>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="vandalonly"> Evidently a vandalism-only account</label>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="spambot"> Account is evidently a spambot or a compromised account</label>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="promoonly"> Account is a promotion-only account</label>
                                        </div>
                                    </div>
                                    <div class="mr-row" style="margin:0 0 .4em 0;">
                                        <label style="min-width:180px;">Comment:</label>
                                        <textarea id="vip-comment" class="ooui-input" style="height:70px; max-width:100%; width:100%;"></textarea>
                                    </div>
                                </div>
                            </div>
                            <div style="text-align:right; margin-top:.8em; display:flex; gap:8px; justify-content:flex-end;">
                                <button id="cancel-action" class="ooui-button">Cancel</button>
                                <button id="confirm-action" class="ooui-button primary">Confirm</button>
                            </div>
                        </div>
                    </div>`);

                const warningMap = {
                    level1: {
                        "uw-vandalism1": { label: "Vandalism" },
                        "uw-test1": { label: "Editing tests" },
                        "uw-delete1": { label: "Removal of content, blanking" },
                        "uw-advert1": { label: "Using Wikipedia for advertising or promotion" },
                        "uw-spam1": { label: "Adding spam links" },
                        "uw-copyright1": { label: "Copyright violation" },
                        "uw-unsourced1": { label: "Addition of unsourced or improperly cited material" }
                    },
                    level2: {
                        "uw-vandalism2": { label: "Vandalism" },
                        "uw-test2": { label: "Editing tests" },
                        "uw-delete2": { label: "Removal of content, blanking" },
                        "uw-advert2": { label: "Using Wikipedia for advertising or promotion" },
                        "uw-spam2": { label: "Adding spam links" },
                        "uw-copyright2": { label: "Copyright violation" },
                        "uw-unsourced2": { label: "Addition of unsourced or improperly cited material" }
                    },
                    level3: {
                        "uw-vandalism3": { label: "Vandalism" },
                        "uw-test3": { label: "Editing tests" },
                        "uw-delete3": { label: "Removal of content, blanking" },
                        "uw-advert3": { label: "Using Wikipedia for advertising or promotion" },
                        "uw-spam3": { label: "Adding spam links" },
                        "uw-error3": { label: "Deliberately adding wrong information" },
                        "uw-unsourced3": { label: "Addition of unsourced or improperly cited material" }
                    },
                    level4: {
                        "uw-vandalism4": { label: "Vandalism" },
                        "uw-test4": { label: "Editing tests" },
                        "uw-delete4": { label: "Removal of content, blanking" },
                        "uw-advert4": { label: "Using Wikipedia for advertising or promotion" },
                        "uw-spam4": { label: "Adding spam links" },
                        "uw-error4": { label: "Deliberately adding wrong information" },
                        "uw-unsourced4": { label: "Addition of unsourced or improperly cited material" }
                    },
                    level4im: {
                        "uw-vandalism4im": { label: "Vandalism" },
                        "uw-delete4im": { label: "Removal of content, blanking" },
                        "uw-spam4im": { label: "Adding spam links" },
                        "uw-biog4im": { label: "BLP violations" },
                        "uw-move4im": { label: "Page moves against conventions" },
                        "uw-npa4im": { label: "Personal attack" }
                    }
                };

                function populateTypes(level) {
                    const $type = $modal.find('#warning-type');
                    $type.empty();
                    const group = warningMap[level] || {};
                    Object.keys(group).forEach(key => {
                        $type.append(`<option value="${key}">${group[key].label}</option>`);
                    });
                }

                $('body').append($modal);
                populateTypes('level1');
                $modal.find('#warning-level').on('change', function(){ populateTypes(this.value); });
                $modal.find('#leave-warning-checkbox').on('change', function(){
                    $modal.find('#warning-options').toggle(this.checked);
                });

                // VIP UI logic
                $modal.find('#leave-vip-checkbox').on('change', function(){
                    $modal.find('#vip-options').toggle(this.checked);
                });
                const $vipPage = $modal.find('#vip-page');
                const $vipBad = $modal.find('#vip-badid');
                const $vipGood = $modal.find('#vip-goodid');
                $vipPage.on('input', function(){
                    const has = this.value.trim() !== '';
                    $vipBad.prop('disabled', !has);
                    $vipGood.prop('disabled', !has || $vipBad.val().trim() !== '');
                });
                $vipBad.on('input', function(){
                    $vipGood.prop('disabled', this.value.trim() === '');
                });

                $modal.find('#confirm-action').on('click', function(){ 
                    const leave = $modal.find('#leave-warning-checkbox').is(':checked');
                    const level = $modal.find('#warning-level').val();
                    const type = $modal.find('#warning-type').val();
                    const reportVIP = $modal.find('#leave-vip-checkbox').is(':checked');
                    const vip = {
                        page: $vipPage.val().trim(),
                        badid: $vipBad.val().trim(),
                        goodid: $vipGood.val().trim(),
                        types: $modal.find('.vip-type:checked').map(function(){return this.value;}).get(),
                        comment: $modal.find('#vip-comment').val().trim()
                    };
                    $modal.remove();
                    resolve({ confirm: true, leaveWarning: leave, warningLevel: level, warningType: type, reportVIP, vip }); 
                });
                $modal.find('#cancel-action').on('click', function(){ $modal.remove(); resolve({ confirm: false }); });
            });
        },

        // Variant used from RC: confirmation dialog with inline reason controls
        confirmActionWithReason: function(message, count) {
            return new Promise(resolve => {
                const $modal = $(`
                    <div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:9999;">
                        <div style="background:#fff;padding:16px 16px 12px 16px;border-radius:4px;max-width:640px;width:92%;box-shadow:0 2px 12px rgba(0,0,0,0.2);font-family:sans-serif;">
                            <h4 style="margin:0 0 .4em 0;">Confirm rollback</h4>
                            <p style="margin:.2em 0;">${message}</p>
                            <p style="margin:.2em 0; color:#54595d;">Number of edits: <strong>${count}</strong></p>
                            <div style="margin-top:.6em; padding:.6em; border:1px solid #a2a9b1; border-radius:2px; background:#f8f9fa;">
                                <label style="display:block; font-weight:600; margin-bottom:.4em;">Rollback reason</label>
                                <div class="mr-row" style="margin:0 0 .4em 0;">
                                    <select id="modal-rollback-reason" class="ooui-select" style="max-width:100%;">
                                        <option value=""></option>
                                        <option value="vandalism">Vandalism</option>
                                        <option value="spam">Adding spam links</option>
                                        <option value="advert">Advertising or promotion</option>
                                        <option value="unsourced">Unsourced or improperly cited material</option>
                                        <option value="copyright">Copyright violation</option>
                                        <option value="blanking">Removal of content, blanking</option>
                                        <option value="wronginfo">Adding wrong information</option>
                                        <option value="other">Other</option>
                                    </select>
                                </div>
                                <input type="text" id="modal-custom-reason" class="ooui-input" placeholder="Enter custom reason" style="display:none; width:100%;">
                            </div>
                            <div style="margin-top:.6em; padding:.6em; border:1px solid #a2a9b1; border-radius:2px; background:#f8f9fa;">
                                <label style="display:flex; align-items:center; gap:.5em;">
                                    <input type="checkbox" id="leave-warning-checkbox"> Leave a message on user's talk page
                                </label>
                                <div id="warning-options" style="display:none; margin-top:.6em;">
                                    <div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
                                        <label>Warning level:</label>
                                        <select id="warning-level" class="ooui-select">
                                            <option value="level1">Level 1</option>
                                            <option value="level2">Level 2</option>
                                            <option value="level3">Level 3</option>
                                            <option value="level4">Level 4</option>
                                            <option value="level4im">Only warning</option>
                                        </select>
                                        <label>Warning type:</label>
                                        <select id="warning-type" class="ooui-select"></select>
                                    </div>
                                </div>
                            </div>
                            <div style="margin-top:.8em; padding:.6em; border:1px solid #a2a9b1; border-radius:2px; background:#f8f9fa;">
                                <label style="display:flex; align-items:center; gap:.5em;">
                                    <input type="checkbox" id="leave-vip-checkbox"> Report at Wikipedia:Vandalism in progress (User-reported)
                                </label>
                                <div id="vip-options" style="display:none; margin-top:.6em;">
                                    <div class="mr-row" style="margin:0 0 .6em 0;">
                                        <label style="min-width:180px;">Primary linked page:</label>
                                        <input type="text" id="vip-page" class="ooui-input" placeholder="Optional: Page title e.g. Article name">
                                    </div>
                                    <div class="mr-row" style="margin:0 0 .6em 0;">
                                        <label style="min-width:180px;">Revision ID when vandalised:</label>
                                        <input type="text" id="vip-badid" class="ooui-input" placeholder="Optional" disabled>
                                    </div>
                                    <div class="mr-row" style="margin:0 0 .6em 0;">
                                        <label style="min-width:180px;">Last good revision ID:</label>
                                        <input type="text" id="vip-goodid" class="ooui-input" placeholder="Optional" disabled>
                                    </div>
                                    <div class="mr-row" style="margin:0 0 .6em 0; align-items:flex-start;">
                                        <label style="min-width:180px;">Reasons:</label>
                                        <div>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="final"> Vandalism after final (level 4 or 4im) warning given</label>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="postblock"> Vandalism after recent (within 1 day) release of block</label>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="vandalonly"> Evidently a vandalism-only account</label>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="spambot"> Account is evidently a spambot or a compromised account</label>
                                            <label style="display:block;"><input type="checkbox" class="vip-type" value="promoonly"> Account is a promotion-only account</label>
                                        </div>
                                    </div>
                                    <div class="mr-row" style="margin:0 0 .4em 0;">
                                        <label style="min-width:180px;">Comment:</label>
                                        <textarea id="vip-comment" class="ooui-input" style="height:70px; max-width:100%; width:100%;"></textarea>
                                    </div>
                                </div>
                            </div>
                            <div style="text-align:right; margin-top:.8em; display:flex; gap:8px; justify-content:flex-end;">
                                <button id="cancel-action" class="ooui-button">Cancel</button>
                                <button id="confirm-action" class="ooui-button primary">Confirm</button>
                            </div>
                        </div>
                    </div>`);

                const warningMap = {
                    level1: {
                        "uw-vandalism1": { label: "Vandalism" },
                        "uw-test1": { label: "Editing tests" },
                        "uw-delete1": { label: "Removal of content, blanking" },
                        "uw-advert1": { label: "Using Wikipedia for advertising or promotion" },
                        "uw-spam1": { label: "Adding spam links" },
                        "uw-copyright1": { label: "Copyright violation" },
                        "uw-unsourced1": { label: "Addition of unsourced or improperly cited material" }
                    },
                    level2: {
                        "uw-vandalism2": { label: "Vandalism" },
                        "uw-test2": { label: "Editing tests" },
                        "uw-delete2": { label: "Removal of content, blanking" },
                        "uw-advert2": { label: "Using Wikipedia for advertising or promotion" },
                        "uw-spam2": { label: "Adding spam links" },
                        "uw-copyright2": { label: "Copyright violation" },
                        "uw-unsourced2": { label: "Addition of unsourced or improperly cited material" }
                    },
                    level3: {
                        "uw-vandalism3": { label: "Vandalism" },
                        "uw-test3": { label: "Editing tests" },
                        "uw-delete3": { label: "Removal of content, blanking" },
                        "uw-advert3": { label: "Using Wikipedia for advertising or promotion" },
                        "uw-spam3": { label: "Adding spam links" },
                        "uw-error3": { label: "Deliberately adding wrong information" },
                        "uw-unsourced3": { label: "Addition of unsourced or improperly cited material" }
                    },
                    level4: {
                        "uw-vandalism4": { label: "Vandalism" },
                        "uw-test4": { label: "Editing tests" },
                        "uw-delete4": { label: "Removal of content, blanking" },
                        "uw-advert4": { label: "Using Wikipedia for advertising or promotion" },
                        "uw-spam4": { label: "Adding spam links" },
                        "uw-error4": { label: "Deliberately adding wrong information" },
                        "uw-unsourced4": { label: "Addition of unsourced or improperly cited material" }
                    },
                    level4im: {
                        "uw-vandalism4im": { label: "Vandalism" },
                        "uw-delete4im": { label: "Removal of content, blanking" },
                        "uw-spam4im": { label: "Adding spam links" },
                        "uw-biog4im": { label: "BLP violations" },
                        "uw-move4im": { label: "Page moves against conventions" },
                        "uw-npa4im": { label: "Personal attack" }
                    }
                };
                function populateTypes(level) {
                    const $type = $modal.find('#warning-type');
                    $type.empty();
                    const group = warningMap[level] || {};
                    Object.keys(group).forEach(key => {
                        $type.append(`<option value="${key}">${group[key].label}</option>`);
                    });
                }

                $('body').append($modal);
                populateTypes('level1');
                $modal.find('#modal-rollback-reason').on('change', function(){
                    $modal.find('#modal-custom-reason').toggle(this.value === 'other');
                });
                $modal.find('#warning-level').on('change', function(){ populateTypes(this.value); });
                $modal.find('#leave-warning-checkbox').on('change', function(){
                    $modal.find('#warning-options').toggle(this.checked);
                });
                $modal.find('#leave-vip-checkbox').on('change', function(){
                    $modal.find('#vip-options').toggle(this.checked);
                });
                const $vipPage = $modal.find('#vip-page');
                const $vipBad = $modal.find('#vip-badid');
                const $vipGood = $modal.find('#vip-goodid');
                $vipPage.on('input', function(){
                    const has = this.value.trim() !== '';
                    $vipBad.prop('disabled', !has);
                    $vipGood.prop('disabled', !has || $vipBad.val().trim() !== '');
                });
                $vipBad.on('input', function(){
                    $vipGood.prop('disabled', this.value.trim() === '');
                });

                $modal.find('#confirm-action').on('click', function(){
                    const reasonValue = $modal.find('#modal-rollback-reason').val();
                    const customReason = $modal.find('#modal-custom-reason').val().trim();
                    const leave = $modal.find('#leave-warning-checkbox').is(':checked');
                    const level = $modal.find('#warning-level').val();
                    const type = $modal.find('#warning-type').val();
                    const reportVIP = $modal.find('#leave-vip-checkbox').is(':checked');
                    const vip = {
                        page: $vipPage.val().trim(),
                        badid: $vipBad.val().trim(),
                        goodid: $vipGood.val().trim(),
                        types: $modal.find('.vip-type:checked').map(function(){return this.value;}).get(),
                        comment: $modal.find('#vip-comment').val().trim()
                    };
                    $modal.remove();
                    resolve({ confirm: true, reasonValue, customReason, leaveWarning: leave, warningLevel: level, warningType: type, reportVIP, vip });
                });
                $modal.find('#cancel-action').on('click', function(){ $modal.remove(); resolve({ confirm: false }); });
            });
        },

        // Nowa wersja: grupujemy edycje wg tytułu i dla każdej grupy wykrywamy ciąg kolejnych edycji.
        performRollback: function(contributions) {
            // Ensure userName is available (RC may not set wgRelevantUserName)
            let userName = mw.config.get('wgRelevantUserName');
            if (!userName && contributions && contributions.length && contributions[0].user) {
                userName = contributions[0].user;
            }
            const reason = $('#rollback-reason').val() === 'other' 
                ? $('#custom-reason').val() 
                : $('#rollback-reason option:selected').text();
            const summary = userName
                ? `[[Help:Revert a page|Reverted]] edit(s) by [[User:${userName}|${userName}]]: ${reason} ([[User:BZPN/MassRollback|MR]])`
                : `[[Help:Revert a page|Reverted]] edit(s): ${reason} ([[User:BZPN/MassRollback|MR]])`;
            const api = new mw.Api();

            // If username still not available, fallback to extracting from contributions array
            if (!userName) {
                const fromList = (contributions || []).map(c => c.user).filter(Boolean)[0];
                if (fromList) userName = fromList;
            }

            // Group edits by page title
            const groups = {};
            contributions.forEach(contrib => {
                if (!groups[contrib.title]) groups[contrib.title] = [];
                groups[contrib.title].push(contrib);
            });

            const promises = [];
            for (let title in groups) {
                let group = groups[title].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
                let latest = group[0];
                let currentParent = latest.parentid;
                for (let i = 1; i < group.length; i++) {
                    const contrib = group[i];
                    if (parseInt(contrib.revid, 10) === parseInt(currentParent, 10)) {
                        currentParent = contrib.parentid;
                    } else {
                        break;
                    }
                }
                promises.push(api.postWithToken('csrf', {
                    action: 'edit',
                    undo: latest.revid,
                    undoafter: currentParent,
                    title: title,
                    summary: summary
                }));
            }
            return Promise.all(promises);
        },

        rollbackSelected: async function() {
            const selected = $('.rollback-checkbox:checked').map(function() {
                return {
                    title: $(this).data('title'),
                    revid: $(this).data('revid'),
                    parentid: $(this).data('parentid'),
                    timestamp: $(this).closest('li').find('.mw-contributions-timestamp').text() || new Date().toISOString()
                };
            }).get();

            if (selected.length === 0) {
                mw.notify('No edits selected.', { type: 'warn' });
                return;
            }
            const confirmResult = await this.confirmAction('Are you sure you want to rollback the selected edits?', selected.length);
            if (!confirmResult || !confirmResult.confirm) return;
            try {
                await this.performRollback(selected);
                mw.notify(`Successfully rolled back ${selected.length} edits.`, { type: 'success' });
                if (confirmResult.leaveWarning) {
                    const targetUser = (selected[0] && selected[0].user) ? selected[0].user : null;
                    await this.leaveUserWarning(confirmResult.warningType, targetUser);
                }
                if (confirmResult.reportVIP) {
                    await this.performVIPReport(confirmResult.vip);
                }
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        },

        rollbackSelectedFromFiltered: async function() {
            const selected = $('.filtered-rollback-checkbox:checked').map(function() {
                return {
                    title: $(this).data('title'),
                    revid: $(this).data('revid'),
                    parentid: $(this).data('parentid'),
                    timestamp: new Date().toISOString() // Jeśli API już zwraca timestamp, można go tutaj użyć
                };
            }).get();

            if (selected.length === 0) {
                mw.notify('No filtered edits selected.', { type: 'warn' });
                return;
            }
            const confirmResult = await this.confirmAction('Are you sure you want to rollback the selected filtered edits?', selected.length);
            if (!confirmResult || !confirmResult.confirm) return;
            try {
                await this.performRollback(selected);
                mw.notify(`Successfully rolled back ${selected.length} filtered edits.`, { type: 'success' });
                if (confirmResult.leaveWarning) {
                    const targetUser = (selected[0] && selected[0].user) ? selected[0].user : null;
                    await this.leaveUserWarning(confirmResult.warningType, targetUser);
                }
                if (confirmResult.reportVIP) {
                    await this.performVIPReport(confirmResult.vip);
                }
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        },

        rollbackAll: async function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();
            try {
                // Dodajemy "timestamp" by móc grupować edycje
                const data = await api.get({
                    action: 'query',
                    list: 'usercontribs',
                    ucuser: userName,
                    uclimit: 'max',
                    ucprop: 'ids|title|parentid|timestamp'
                });
                const contributions = data.query.usercontribs;
                if (contributions.length === 0) {
                    mw.notify('No edits to rollback.', { type: 'warn' });
                    return;
                }
                const confirmResult = await this.confirmAction('Are you sure you want to rollback ALL edits?', contributions.length);
                if (!confirmResult || !confirmResult.confirm) return;
                await this.performRollback(contributions);
                mw.notify(`Successfully rolled back all ${contributions.length} edits.`, { type: 'success' });
                if (confirmResult.leaveWarning) {
                    const targetUser = (contributions[0] && contributions[0].user) ? contributions[0].user : null;
                    await this.leaveUserWarning(confirmResult.warningType, targetUser);
                }
                if (confirmResult.reportVIP) {
                    await this.performVIPReport(confirmResult.vip);
                }
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        },

        performVIPReport: async function(vip) {
            const api = new mw.Api();
            const uid = mw.config.get('wgRelevantUserName');
            const isIP = /^(?:\d{1,3}\.){3}\d{1,3}$/.test(uid) || uid.includes(':');

            const typeMap = {
                final: 'vandalism after final warning',
                postblock: 'vandalism after recent release of block',
                spambot: 'account is evidently a spambot or a compromised account',
                vandalonly: 'actions evidently indicate a vandalism-only account',
                promoonly: 'account is being used only for promotional purposes'
            };
            const typesText = (vip.types || []).map(v => typeMap[v] || 'unknown reason').join('; ');

            let reason = '';
            if (vip.page) {
                const safe = vip.page.replace(/^(Image|Category|File):/i, ':$1:');
                reason += `On [[${safe}]]`;
                if (vip.badid) {
                    reason += ` ({{diff|${vip.page}|${vip.badid}|${vip.goodid || ''}|diff}})`;
                }
                reason += ':';
            }
            if (typesText) reason += (reason ? ' ' : '') + typesText;
            if (vip.comment) reason += (reason ? '. ' : '') + vip.comment;
            reason += '. ' + '~~'+'~~';
            reason = reason.replace(/\r?\n/g, "\n*:");

            const vandalTpl = isIP ? 'IPvandal' : 'vandal';
            const userParam = /=/.test(uid) ? `1=${uid}` : uid;
            const entry = `\n*{{${vandalTpl}|${userParam}}} &ndash; ${reason}`;

            const title = 'Wikipedia:Vandalism in progress';
            let sectionIndex = null;
            try {
                const parseData = await api.get({ action: 'parse', page: title, prop: 'sections' });
                if (parseData && parseData.parse && Array.isArray(parseData.parse.sections)) {
                    const sec = parseData.parse.sections.find(s => (s.line || '').trim().toLowerCase() === 'user-reported');
                    if (sec) sectionIndex = sec.index;
                }
            } catch (e) {}

            if (sectionIndex) {
                return api.postWithToken('csrf', {
                    action: 'edit',
                    title: title,
                    section: sectionIndex,
                    appendtext: entry,
                    summary: `Reporting [[Special:Contributions/${uid}|${uid}]]. ([[User:BZPN/MassRollback|MR]])`
                });
            } else {
                return api.postWithToken('csrf', {
                    action: 'edit',
                    title: title,
                    section: 'new',
                    sectiontitle: 'User-reported',
                    text: entry.trimStart(),
                    summary: `Reporting [[Special:Contributions/${uid}|${uid}]]. ([[User:BZPN/MassRollback|MR]])`
                });
            }
        },

        // Leave a talk page warning using a chosen template (subst) under current month section
        leaveUserWarning: async function(templateName, targetUser) {
            const api = new mw.Api();
            const userName = targetUser || mw.config.get('wgRelevantUserName');
            if (!userName) {
                mw.notify('Could not determine user name for talk page warning.', { type: 'error' });
                return;
            }
            const title = `User talk:${userName}`;

            // Build current month/year (English)
            const monthNames = ["January","February","March","April","May","June","July","August","September","October","November","December"];
            const now = new Date();
            const monthStr = `${monthNames[now.getUTCMonth()]} ${now.getUTCFullYear()}`;

            // Compose warning w/ subst and signature
            const insertion = `{{`+`subst:${templateName}}} `+`~~`+`~~`+`\n`;

            // Try to find existing section index via parse
            let sectionIndex = null;
            try {
                const parseData = await api.get({ action: 'parse', page: title, prop: 'sections' });
                if (parseData && parseData.parse && Array.isArray(parseData.parse.sections)) {
                    const sec = parseData.parse.sections.find(s => (s.line || '').trim() === monthStr);
                    if (sec) sectionIndex = sec.index; // string index
                }
            } catch (e) {
                // If page doesn't exist or parse fails, we'll create a new section below
            }

            if (sectionIndex) {
                // Append to existing month section
                return api.postWithToken('csrf', {
                    action: 'edit',
                    title: title,
                    section: sectionIndex,
                    appendtext: `\n\n${insertion}`,
                    summary: `Automated warning: ${templateName} ([[User:BZPN/MassRollback|MR]])`
                });
            } else {
                // Create a new month section and post inside it
                return api.postWithToken('csrf', {
                    action: 'edit',
                    title: title,
                    section: 'new',
                    sectiontitle: monthStr,
                    text: insertion,
                    summary: `Automated warning: ${templateName} ([[User:BZPN/MassRollback|MR]])`
                });
            }
        }
    };

    $(document).ready(function() {
        MassRollback.init();
    });
})(jQuery, mediaWiki);