User:BZPN/MassRollback2.js: Difference between revisions

From Test Wiki
Content deleted Content added
BZPN (talk | contribs)
No edit summary
BZPN (talk | contribs)
No edit summary
Line 3:
 
const MassRollback = {
 
init: function() {
ifconst page = (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') {;
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;
}
this.createUI();
// Try to enhance to Codex after UI is present (only if styles loaded)
if (mw.loader.getState && (mw.loader.getState('codex-styles') === 'ready' || mw.loader.getState('codex-search-styles') === 'ready')) {
this.applyCodexClasses();
}
this.bindEvents();
this.fetchUserStats();
},
 
createUIinjectStyles: function() {
const $containerstyles = $(`
/* Container as a clean drawer */
<div id=\"mass-rollback-container\" class=\"mw-portlet\" style=\"display:none;\">\n <h3 class=\"mw-headline\" style=\"margin-top:0;\">Mass rollback tool</h3>\n\n <div id=\"stats-section\" class=\"mw-message-box mw-message-box-notice\" style=\"margin-bottom:1em;\">\n <h4 class=\"mw-ui-headline\" style=\"margin:0 0 .5em 0;\">User statistics</h4>\n <p>Edits: <strong id=\"total-edits\">-</strong></p>\n <p>First edit: <span id=\"first-edit\">-</span></p>\n <p>Recent edit: <span id=\"last-edit\">-</span> <button id=\"refresh-stats\" class=\"mw-ui-button mw-ui-progressive\">Refresh</button></p>\n </div>\n\n <div id=\"filters-section\" class=\"mw-portlet-body\" style=\"margin-bottom:1em;\">\n <h4 class=\"mw-ui-headline\">Filter edits</h4>\n <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n <label class=\"mw-label\" style=\"margin-right:.5em;\">Date from:</label>\n <input type=\"date\" id=\"start-date\" class=\"mw-ui-input\">\n <label class=\"mw-label\" style=\"margin:0 .5em;\">to:</label>\n <input type=\"date\" id=\"end-date\" class=\"mw-ui-input\">\n </div>\n <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n <label class=\"mw-label\" style=\"margin-right:.5em;\">Namespace:</label>\n <select id=\"namespace-filter\" multiple class=\"mw-ui-input\" style=\"min-width:150px;\">\n <option value=\"all\">All</option>\n <option value=\"0\">Main</option>\n <option value=\"1\">Talk</option>\n <option value=\"2\">User</option>\n <option value=\"3\">User talk</option>\n <option value=\"4\">Project</option>\n <option value=\"5\">Project talk</option>\n <option value=\"6\">File</option>\n <option value=\"7\">File talk</option>\n <option value=\"8\">MediaWiki</option>\n <option value=\"9\">MediaWiki talk</option>\n <option value=\"10\">Template</option>\n <option value=\"11\">Template talk</option>\n <option value=\"12\">Help</option>\n <option value=\"13\">Help talk</option>\n <option value=\"14\">Category</option>\n <option value=\"15\">Category talk</option>\n <option value=\"100\">Portal</option>\n <option value=\"101\">Portal talk</option>\n </select>\n </div>\n <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n <label class=\"mw-label\" style=\"margin-right:.5em;\">Edit size:</label>\n <select id=\"size-filter\" class=\"mw-ui-input\">\n <option value=\"\">Any</option>\n <option value=\"small\">Small (&lt; 50 bytes)</option>\n <option value=\"medium\">Medium (50-500 bytes)</option>\n <option value=\"large\">Large (&gt; 500 bytes)</option>\n </select>\n </div>\n <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n <label class=\"mw-label\" style=\"margin-right:.5em;\">Sort order:</label>\n <select id=\"sort-order\" class=\"mw-ui-input\">\n <option value=\"desc\">Latest first</option>\n <option value=\"asc\">Oldest first</option>\n </select>\n </div>\n <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n <button id=\"clear-filters\" class=\"mw-ui-button\">Clear filters</button>\n <button id=\"show-filtered\" class=\"mw-ui-button mw-ui-progressive\" style=\"margin-left:.5em;\">Show filtered edits</button>\n </div>\n </div>\n\n <div id=\"reason-section\" class=\"mw-portlet-body\" style=\"margin-bottom:1em;\">\n <h4 class=\"mw-ui-headline\">Rollback reason</h4>\n <select id=\"rollback-reason\" class=\"mw-ui-input\">\n <option value=\"\"></option>\n <option value=\"vandalism\">Vandalism</option>\n <option value=\"spam\">Spam</option>\n <option value=\"test\">Test rollback</option>\n <option value=\"teste\">Test edit</option>\n <option value=\"rules-violation\">Violation</option>\n <option value=\"other\">Other</option>\n </select>\n <input type=\"text\" id=\"custom-reason\" placeholder=\"Enter custom reason\" class=\"mw-ui-input\" style=\"display:none; margin-top:.5em; width:100%;\">\n </div>\n\n <div id=\"actions-section\" class=\"mw-portlet-body\" style=\"text-align:center;\">\n <button id=\"rollback-selected\" class=\"mw-ui-button mw-ui-progressive\" style=\"margin-right:.5em;\">Rollback selected</button>\n <button id=\"rollback-all\" class=\"mw-ui-button mw-ui-destructive\">Rollback all</button>\n </div>\n </div>\n\n <div id=\"filtered-edits-container\" class=\"mw-portlet\" style=\"display:none;\">\n <h4 class=\"mw-ui-headline\">Filtered edits</h4>\n <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n <input type=\"checkbox\" id=\"select-all\"> <label for=\"select-all\">Select all</label>\n </div>\n <table id=\"filtered-edits-table\" class=\"wikitable\" style=\"width:100%;\">\n <thead>\n <tr>\n <th>Revid</th>\n <th>Title</th>\n <th>Timestamp</th>\n <th>Namespace</th>\n <th>Size</th>\n <th>Select</th>\n </tr>\n </thead>\n <tbody>\n <!-- Wstawiane dynamicznie -->\n </tbody>\n </table>\n <div style=\"text-align:center; margin-top:.5em;\">\n <button id=\"rollback-selected-filtered\" class=\"mw-ui-button mw-ui-progressive\">Rollback selected filtered edits</button>\n </div>\n </div>\n `);
#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;
}
 
const $toggleButton = $(` /* Sections */
.mr-section h4 { margin: 0 0 .4em 0; font-weight: 600; }
<button id=\"mass-rollback-toggle\" class=\"mw-ui-button mw-ui-progressive\" style=\"margin-bottom:.75em;\">\n Show/hide MassRollback\n </button>\n `);
.mr-section + .mr-section { margin-top: 14px; }
.mr-row { margin: .6em 0; display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; }
$toggleButton.on('click', function() {
$('#mass.mr-rollbackrow label { margin-container').slideToggle()right: 6px; color: #202122; }
});
$('#mw-content-text').prepend($toggleButton).prepend($container);
},
 
/* Inputs */
// Apply Codex CSS-only classes when styles are available; keep MW UI as fallback
.ooui-input, .ooui-select {
applyCodexClasses: function() {
try { width: 100%;
max-width: 360px;
// Buttons: map MediaWiki UI modifiers to Codex equivalents and keep both for compatibility
box-sizing: border-box;
$('#mass-rollback-container').find('button.mw-ui-button').each(function() {
constborder: $btn1px =solid $(this)#a2a9b1;
$btn.addClass('cdxborder-button')radius: 2px;
ifpadding: ($btn.hasClass('mw-ui-progressive'))45em {.6em;
font-size: $btn.addClass('cdx-button--action-progressive')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;
}
if ($btn.hasClass('mw#filtered-uiedits-destructive'))table tr { margin-bottom: 10px; }
#filtered-edits-table td { border: $btn.addClass('cdxnone; position: relative; padding-button--action-destructive')left: 50%; }
#filtered-edits-table td:before {
position: absolute;
top: 0; left: 0;
width: 45%; padding-left: 5px;
font-weight: bold;
white-space: nowrap;
}
if#filtered-edits-table ($btn.hasClass('mwtd:nth-uiof-quiet')type(1):before { content: "Revid"; }
$btn.addClass('cdx#filtered-buttonedits-table td:nth-weightof-quiet'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"; }
});
// Inputs: add Codex input#filtered-edits-table classtd:nth-of-type(5):before to{ enhance stylingcontent: when"Size"; available}
#filtered-edits-table td:nth-of-type(6):before { content: "Select"; }
$('#mass-rollback-container').find('input[type="text"], input[type="date"], select').each(function() {
const $el = $(this);}
$el.addClass('cdx-text-input__input')`;
$('<style>').text(styles).appendTo('head');
// Optional: wrap with Codex container if not already wrapped
},
if (!$el.parent().hasClass('cdx-text-input')) {
 
$el.wrap('<div class="cdx-text-input"></div>');
createUI: function() }{
const $ui = });$(`
} catch (e) { <details id="mass-rollback-details" open>
// Non-fatal: just skip Codex<summary>Mass enhancementsrollback</summary>
console.warn('Codex class application skipped:', e);<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);
}
},
Line 67 ⟶ 333:
$('#custom-reason').toggle($(this).val() === 'other');
});
 
$('#clear-filters').on('click', function() {
$('#start-date').val('');
Line 75 ⟶ 340:
$('#sort-order').val('desc');
});
 
$('#refresh-stats').on('click', this.fetchUserStats.bind(this));
$('#show-filtered').on('click', this.displayFilteredEdits.bind(this));
Line 81 ⟶ 345:
$('#rollback-selected').on('click', this.rollbackSelected.bind(this));
$('#rollback-all').on('click', this.rollbackAll.bind(this));
 
$(document).on('change', '#select-all', function() {
const isCheckedchecked = $(this).is(':checked');
$('#filtered-edits-table tbody input[type="checkbox"]').prop('checked', isCheckedchecked);
});
 
// Dodaj checkbox przy edycjach w widoku kontrybucji
$('li[data-mw-revid]').each(function() {
const $li = $(this);
Line 99 ⟶ 360:
'data-parentid': parentid,
'data-title': title,
style: 'margin-right: 5px;'
});
$li.prepend($checkbox);
Line 109 ⟶ 370:
const api = new mw.Api();
 
// Poprawne1) pobieranieGet liczbyaccurate edycjitotal iedits pierwszej/ostatniejfrom edycji'users' prop
const editCountRequserPromise = api.get({
action: 'query',
list: 'users',
Line 116 ⟶ 377:
usprop: 'editcount'
});
 
const firstEditReq = api.get({
// 2) Get first contribution (oldest)
const firstPromise = api.get({
action: 'query',
list: 'usercontribs',
ucuser: userName,
uclimit: 1,
ucdirucprop: 'newertimestamp',
ucpropucdir: 'timestampnewer'
});
 
const lastEditReq = api.get({
// 3) Get last contribution (newest)
const lastPromise = api.get({
action: 'query',
list: 'usercontribs',
ucuser: userName,
uclimit: 1,
ucdirucprop: 'oldertimestamp',
ucpropucdir: 'timestampolder'
});
 
$Promise.whenall(editCountReq[userPromise, firstEditReqfirstPromise, lastEditReqlastPromise]).donethen(function(editCountData[userData, firstData, lastData]) => {
const user = (userData.query && userData.query.users && userData.query.users[0]) || {};
try {
const editcount = typeof const user.editcount === 'number' ? editCountData[0].query.users[0]user.editcount : '-';
const first = (firstData.query && firstData.query.usercontribs && firstData.query.usercontribs[0]) || null;
$('#total-edits').text(editcount != null ? editcount : '-');
const last = (lastData.query && lastData.query.usercontribs && lastData.query.usercontribs[0]) || null;
} catch (e) {
 
$('#total-edits').text('-');
}$('#total-edits').text(editcount);
$('#first-edit').text(first ? new Date(first.timestamp).toLocaleDateString() : '-');
try {
$('#last-edit').text(last ? new Date(last.timestamp).toLocaleDateString() : '-');
const firstList = firstData[0].query.usercontribs || [];
}).catch(() => {
$('#first-edit').text(firstList.length ? new Date(firstList[0].timestamp).toLocaleDateString() : '-');
}// catchFallback (e)to {previous approach if anything fails
$('#first-edit')api.textget('-');{
} action: 'query',
try { list: 'usercontribs',
constucuser: lastList = lastData[0].query.usercontribs || [];userName,
uclimit: '5000',
$('#last-edit').text(lastList.length ? new Date(lastList[0].timestamp).toLocaleDateString() : '-');
} catch (e) { ucprop: 'timestamp'
$('#last-edit'}).textdone('-'function(data); {
} const contributions = data.query.usercontribs || [];
} $('#total-edits').failtext(function()contributions.length {|| '-');
$ if ('#total-edits')contributions.text('-'length > 0); {
$ const timestamps = contributions.map('#first-edit'c => new Date(c.timestamp)).textsort('(a,b) => a-'b);
$('#lastfirst-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'};
const nsMapping = {
return mapping[ns] || 0: 'Main',ns;
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 nsMapping[ns] || ns;
},
 
applyFilters: function(contributionscontribs) {
const startDatestart = $('#start-date').val() ? new Date($('#start-date').val()) : null;
const endDateend = $('#end-date').val() ? new Date($('#end-date').val()) : null;
let namespaceFilternsFilter = $('#namespace-filter').val() || [];
const sizeFilter = $('#size-filter').val();
const sortOrder = $('#sort-order').val();
if (nsFilter.includes('all')) nsFilter = [];
 
iflet (namespaceFilterfiltered = contribs.includesfilter('all'))c => {
namespaceFilterconst 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;
let filtered = contributions.filter(contrib => {
const contribDate = new Date(contrib.timestamp);
if (startDate && contribDate < startDate) return false;
if (endDate && contribDate > endDate) return false;
if (namespaceFilter.length > 0 && !namespaceFilter.includes(contrib.ns.toString())) return false;
if (sizeFilter) {
const sizes = contribc.size || 0;
switchif (sizeFilter === 'small' && s >= 50) {return false;
if (sizeFilter case=== 'smallmedium': return&& size(s < 50 || s > 500)) return false;
if (sizeFilter case=== 'mediumlarge': return size >= 50 && sizes <= 500) return false;
case 'large': return size > 500;
}
}
return true;
});
filtered.sort((a,b) => sortOrder==='asc'? new Date(a.timestamp)-new Date(b.timestamp) : new Date(b.timestamp)-new Date(a.timestamp));
 
filtered.sort((a, b) => {
return sortOrder === 'asc'
? new Date(a.timestamp) - new Date(b.timestamp)
: new Date(b.timestamp) - new Date(a.timestamp);
});
 
return filtered;
},
Line 223 ⟶ 463:
const api = new mw.Api();
api.get({
action: 'query',
list: 'usercontribs',
ucuser: userName,
uclimit: 'max5000',
ucprop: 'ids|title|timestamp|size|ns|parentid'
}).done((data) => {
const allContributionsfiltered = this.applyFilters(data.query.usercontribs);
const filtered$tbody = this$('#filtered-edits-table tbody').applyFiltersempty(allContributions);
constif $tbody = $('#!filtered-edits-table.length) tbody');{
$tbody.append('<tr><td colspan="6" style="text-align:center;">No edits match the selected filters.</td></tr>');
$tbody.empty();
 
if (filtered.length === 0) {
$tbody.append('<tr><td colspan="6" style="text-align:center; padding:5px;">No edits match the selected filters.</td></tr>');
} else {
filtered.forEach(contribc => {
const row = $tbody.append(`
<tr>
<td style="padding: 5px; border: 1px solid #ccc;">${contribc.revid}</td>
<td style="padding: 5px; border: 1px solid #ccc;">${contribc.title}</td>
<td style="padding: 5px; border: 1px solid #ccc;">${new Date(contribc.timestamp).toLocaleString()}</td>
<td style="padding: 5px; border: 1px solid #ccc;">${this.getNamespaceName(contribc.ns)}</td>
<td style="padding: 5px; border: 1px solid #ccc;">${contribc.size || 0}</td>
<td style="paddingtext-align: 5pxcenter;"><input border:type="checkbox" 1pxclass="filtered-rollback-checkbox" soliddata-revid="${c.revid}" #ccc; textdata-align:parentid="${c.parentid}" center;data-title="${c.title}"></td>
</tr>`);
<input type="checkbox" class="filtered-rollback-checkbox" data-revid="${contrib.revid}" data-parentid="${contrib.parentid}" data-title="${contrib.title}">
</td>
</tr>
`;
$tbody.append(row);
});
}
$('#filtered-edits-container').css("display", "block").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 {
await self.performRollback([$(this).data('mr-edit')]);
mw.notify('Successfully rolled back 1 edit.', { type: 'success' });
if (confirmResult.leaveWarning) {
await self.leaveUserWarning(confirmResult.warningType);
}
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');
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();
}
if (revid && parentid && title) {
return { title: title, revid: revid, parentid: parentid, timestamp: new Date().toISOString() };
}
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() };
}
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.52); display: flex; alignfont-itemsfamily: center; justifysans-content: center; z-index: 9999serif;">
<divh4 style="backgroundmargin:0 #fff;0 padding:.4em 20px0;">Confirm border-radius: 4px; max-width: 400px; width: 90%;"rollback</h4>
<h4p style="margin-top:.2em 0;">Confirm Rollback${message}</h4p>
<p style="margin:.2em 0; color:#54595d;">Number of edits: <strong>${messagecount}</strong></p>
<p>Numberdiv ofstyle="margin-top:.6em; editspadding:.6em; ${count}</pborder:1px solid #a2a9b1; border-radius:2px; background:#f8f9fa;">
<divlabel style="text-aligndisplay: rightflex; marginalign-topitems:center; 15pxgap:.5em;">
<buttoninput idtype="confirm-actioncheckbox" styleid="padding: 5px 10px; backgroundleave-warning-color:checkbox"> #007bff;Leave color:a #fff;message border:on none;user's border-radius:talk 3px; cursor: pointer; margin-right: 5px;">Confirm</button>page
</label>
<button id="cancel-action" style="padding: 5px 10px; background-color: #aaa; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Cancel</button>
<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);
$modal.findpopulateTypes('#confirm-actionlevel1').on('click', function() {;
$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 });
resolve(false);
});
$modal.find('#cancel-action').on('click', function(){ $modal.remove(); resolve({ confirm: false }); });
});
},
Line 291 ⟶ 933:
? $('#custom-reason').val()
: $('#rollback-reason option:selected').text();
const summary = `[[Help:Revert a page|Reverted]] edit(s) by [[User:${userName}|${userName}]]: ${reason} ([[User:BZPN/MassRollback|MR]])`;
const api = new mw.Api();
 
// GrupujemyGroup edycjeedits wgby tytułupage stronytitle
const groups = {};
contributions.forEach(contrib => {
if (!groups[contrib.title]) {groups[contrib.title] = [];
groups[contrib.title] = [];
}
groups[contrib.title].push(contrib);
});
 
const promises = [];
// Dla każdej strony – sortuj wg daty malejąco i wykryj ciąg edycji
for (let title in groups) {
let group = groups[title].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
let latest = group[0];
// Rozpoczynamy ciąg od najnowszej edycji
let chain = [latest];
let currentParent = latest.parentid;
// Przeglądamy kolejne edycje dla tego samego tytułu
for (let i = 1; i < group.length; i++) {
const contrib = group[i];
// Jeżeli kolejna edycja jest dokładnie poprzednikiem poprzednio znalezionego elementu, dodajemy do ciągu
if (parseInt(contrib.revid, 10) === parseInt(currentParent, 10)) {
chain.push(contrib);
currentParent = contrib.parentid;
} else {
Line 322 ⟶ 956:
}
}
// Wykonaj rollback tylko, jeśli mamy przynajmniej jedną edycję (zawsze prawda)
promises.push(api.postWithToken('csrf', {
action: 'edit',
Line 335 ⟶ 968:
 
rollbackSelected: async function() {
// Pobieramy zaznaczone edycje z listy kontrybucji
const selected = $('.rollback-checkbox:checked').map(function() {
return {
Line 341 ⟶ 973:
revid: $(this).data('revid'),
parentid: $(this).data('parentid'),
// Wartość timestamp może nie być dostępna w tym widoku – można opcjonalnie dodać dodatkowy pobór danych
timestamp: $(this).closest('li').find('.mw-contributions-timestamp').text() || new Date().toISOString()
};
Line 350 ⟶ 981:
return;
}
const confirmedconfirmResult = await this.confirmAction('Are you sure you want to rollback the selected edits?', selected.length);
if (!confirmedconfirmResult || !confirmResult.confirm) return;
try {
await this.performRollback(selected);
mw.notify(`Successfully rolled back ${selected.length} edits.`, { type: 'success' });
if (confirmResult.leaveWarning) {
await this.leaveUserWarning(confirmResult.warningType);
}
if (confirmResult.reportVIP) {
await this.performVIPReport(confirmResult.vip);
}
location.reload();
} catch (error) {
Line 376 ⟶ 1,013:
return;
}
const confirmedconfirmResult = await this.confirmAction('Are you sure you want to rollback the selected filtered edits?', selected.length);
if (!confirmedconfirmResult || !confirmResult.confirm) return;
try {
await this.performRollback(selected);
mw.notify(`Successfully rolled back ${selected.length} filtered edits.`, { type: 'success' });
if (confirmResult.leaveWarning) {
await this.leaveUserWarning(confirmResult.warningType);
}
if (confirmResult.reportVIP) {
await this.performVIPReport(confirmResult.vip);
}
location.reload();
} catch (error) {
Line 405 ⟶ 1,048:
return;
}
const confirmedconfirmResult = await this.confirmAction('Are you sure you want to rollback ALL edits?', contributions.length);
if (!confirmedconfirmResult || !confirmResult.confirm) return;
await this.performRollback(contributions);
mw.notify(`Successfully rolled back all ${contributions.length} edits.`, { type: 'success' });
if (confirmResult.leaveWarning) {
await this.leaveUserWarning(confirmResult.warningType);
}
if (confirmResult.reportVIP) {
await this.performVIPReport(confirmResult.vip);
}
location.reload();
} catch (error) {
Line 414 ⟶ 1,063:
console.error(error);
}
},
};
 
performVIPReport: async function(vip) {
// Load stable MW modules and try Codex CSS-only styles if present (1.39+)
const api = new mw.Api();
(function loadModulesAndInit() {
const uid = mw.config.get('wgRelevantUserName');
var baseDeps = [
const isIP = /^(?:\d{1,3}\.){3}\d{1,3}$/.test(uid) || uid.includes(':');
'mediawiki.api',
 
'mediawiki.util',
'mediawiki.notify'const typeMap = {
final: 'vandalism after final warning',
];
postblock: 'vandalism after recent release of block',
var codexDep = null;
spambot: 'account is evidently a spambot or a compromised account',
try {
// Prefer the smaller setvandalonly: if'actions available,evidently elseindicate full;a elsevandalism-only skipaccount',
promoonly: 'account is being used only for promotional purposes'
if (mw.loader.getState('codex-search-styles')) {
codexDep = 'codex-search-styles'};
}const elsetypesText if= (mwvip.loadertypes || []).getStatemap(v => typeMap[v] || 'codex-stylesunknown reason')).join('; {');
 
codexDep = 'codex-styles';
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 += ':';
}
} catch if (etypesText) {reason /*+= ignore(reason ? ' ' : '') */+ }typesText;
if (vip.comment) reason += (reason ? '. ' : '') + vip.comment;
reason += '. ' + '~~'+'~~';
reason = reason.replace(/\r?\n/g, "\n*:");
 
const vandalTpl = isIP ? 'IPvandal' : 'vandal';
// Also load legacy mediawiki.ui when present to keep fallback look
const userParam = /=/.test(uid) ? `1=${uid}` : uid;
var legacyUI = [];
const entry = `\n*{{${vandalTpl}|${userParam}}} &ndash; ${reason}`;
if (mw.loader.getState('mediawiki.ui')) legacyUI.push('mediawiki.ui');
if (mw.loader.getState('mediawiki.ui.button')) legacyUI.push('mediawiki.ui.button');
 
var deps const title = baseDeps.concat(legacyUI)'Wikipedia:Vandalism in progress';
if (codexDep) deps.push(codexDep) 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) {
const api = new mw.Api();
const userName = mw.config.get('wgRelevantUserName');
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]])`
});
}
}
};
 
mw.loader.using$(depsdocument).thenready(function() {
$(function() { try { MassRollback.init(); } catch (e) { console.error(e); } });
}).catch(function() {;
// Final fallback: try to init anyway
$(function() { try { MassRollback.init(); } catch (e) { console.error(e); } });
});
})();
})(jQuery, mediaWiki);