//<nowiki>
$(document).ready(function() {
function initializeRightsChanges() {
$('#mw-content-text > p').remove();
$('#firstHeading').text('RightsChanges');
var monthSelect = new OO.ui.DropdownWidget({
menu: {
items: [
new OO.ui.MenuOptionWidget({ data: '1', label: 'January' }),
new OO.ui.MenuOptionWidget({ data: '2', label: 'February' }),
new OO.ui.MenuOptionWidget({ data: '3', label: 'March' }),
new OO.ui.MenuOptionWidget({ data: '4', label: 'April' }),
new OO.ui.MenuOptionWidget({ data: '5', label: 'May' }),
new OO.ui.MenuOptionWidget({ data: '6', label: 'June' }),
new OO.ui.MenuOptionWidget({ data: '7', label: 'July' }),
new OO.ui.MenuOptionWidget({ data: '8', label: 'August' }),
new OO.ui.MenuOptionWidget({ data: '9', label: 'September' }),
new OO.ui.MenuOptionWidget({ data: '10', label: 'October' }),
new OO.ui.MenuOptionWidget({ data: '11', label: 'November' }),
new OO.ui.MenuOptionWidget({ data: '12', label: 'December' })
]
}
}),
yearInput = new OO.ui.NumberInputWidget({
value: new Date().getFullYear(),
min: 2005,
max: new Date().getFullYear() + 1,
step: 1
}),
checkButton = new OO.ui.ButtonWidget({
label: 'Check',
flags: ['primary', 'progressive']
}),
resultsContainer = $("<div>").hide();
var currentDate = new Date();
var currentMonth = String(currentDate.getMonth() + 1);
monthSelect.getMenu().selectItemByData(currentMonth);
var labels = {
monthLabel: $('<p>').text('Month:').css('font-weight', 'bold'),
yearLabel: $('<p>').text('Year:').css('font-weight', 'bold'),
description: $('<p>').text('View user rights changes for English Wikipedia for a specific month.')
};
$('#mw-content-text').append(
labels.description,
'<br/>',
$('<div>').css({
'background': '#f8f9fa',
'border': '1px solid #a2a9b1',
'padding': '15px',
'margin-bottom': '20px',
'border-radius': '3px'
}).append(
$('<h3>').text('Select Month and Year'),
$('<div>').css({
'display': 'flex',
'gap': '15px',
'align-items': 'end',
'flex-wrap': 'wrap'
}).append(
$('<div>').append(labels.monthLabel, monthSelect.$element),
$('<div>').append(labels.yearLabel, yearInput.$element),
$('<div>').append(checkButton.$element)
)
),
resultsContainer
);
function formatDate(date) {
const options = { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: 'UTC' };
return date.toLocaleDateString('en-GB', options).replace(',', '') + ' UTC';
}
function formatDateFromTimestamp(timestamp) {
const date = new Date(timestamp);
const options = { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: 'UTC' };
return date.toLocaleDateString('en-GB', options).replace(',', '') + ' UTC';
}
function processLogEntries(entries, targetGroups) {
const granted = [];
const removed = [];
entries.forEach(entry => {
if (!entry.title) return;
const timestamp = entry.timestamp;
const userTarget = entry.title;
const actor = entry.user;
let oldGroups = new Set();
let newGroups = new Set();
if (entry.params) {
if (entry.params.oldgroups) {
oldGroups = new Set(entry.params.oldgroups);
}
if (entry.params.newgroups) {
newGroups = new Set(entry.params.newgroups);
}
if (entry.params['4::oldgroups']) {
oldGroups = new Set(entry.params['4::oldgroups']);
}
if (entry.params['5::newgroups']) {
newGroups = new Set(entry.params['5::newgroups']);
}
}
if (entry.comment) {
const commentMatch = entry.comment.match(/changed group membership for (.+?) from (.+?) to (.+)/);
if (commentMatch) {
const oldGroupsStr = commentMatch[2];
const newGroupsStr = commentMatch[3];
if (oldGroupsStr !== '(none)') {
oldGroups = new Set(oldGroupsStr.split(', '));
}
if (newGroupsStr !== '(none)') {
newGroups = new Set(newGroupsStr.split(', '));
}
}
}
targetGroups.forEach(grp => {
if (newGroups.has(grp) && !oldGroups.has(grp)) {
granted.push([timestamp, userTarget, grp, actor]);
}
if (oldGroups.has(grp) && !newGroups.has(grp)) {
removed.push([timestamp, userTarget, grp, actor]);
}
});
});
return { granted: granted.sort(), removed: removed.sort() };
}
function createTable(title, data) {
if (!data.length) return '';
let html = `<h3>${title}</h3><table class="wikitable sortable"><thead><tr><th>Date</th><th>User</th><th>Right</th><th>By</th></tr></thead><tbody>`;
data.forEach(([ts, tgt, grp, by]) => {
const sign = title.includes('granted') ? '+' : '-';
html += `<tr><td>${formatDateFromTimestamp(ts)}</td><td>${tgt}</td><td>${sign}${grp}</td><td>${by}</td></tr>`;
});
html += '</tbody></table>';
return html;
}
function fetchAllLogEvents(api, params, results = []) {
return api.get(params).then(response => {
results.push(...(response.query.logevents || []));
if (response.continue) {
return fetchAllLogEvents(api, { ...params, ...response.continue }, results);
}
return results;
});
}
function fetchRightsChanges() {
var selectedItem = monthSelect.getMenu().findSelectedItem();
if (!selectedItem) {
OO.ui.alert('Please select a month');
return;
}
const month = parseInt(selectedItem.getData());
const year = parseInt(yearInput.getValue());
if (year < 2005 || year > new Date().getFullYear() + 1) {
OO.ui.alert('Please enter a valid year between 2005 and ' + (new Date().getFullYear() + 1));
return;
}
const startDt = new Date(Date.UTC(year, month - 1, 1, 0, 0, 0));
const nextMonthDt = new Date(Date.UTC(month === 12 ? year + 1 : year, month % 12, 1, 0, 0, 0));
const startTs = startDt.toISOString();
const endTs = nextMonthDt.toISOString();
const startLabel = formatDate(startDt);
const endLabel = formatDate(new Date(nextMonthDt.getTime() - 1000));
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php', { headers: { 'Api-User-Agent': 'https://en.wikipedia.org/wiki/User:DreamRimmer' } });
const localApi = new mw.ForeignApi('https://en.wikipedia.org/w/api.php', { headers: { 'Api-User-Agent': 'https://en.wikipedia.org/wiki/User:DreamRimmer' } });
resultsContainer.empty();
resultsContainer.append($('<p>').text('Loading...'));
resultsContainer.show();
checkButton.setDisabled(true);
checkButton.setLabel('Loading...');
Promise.all([
fetchAllLogEvents(metaApi, {
action: 'query',
list: 'logevents',
letype: 'rights',
lestart: endTs,
leend: startTs,
lelimit: 'max',
leprop: 'timestamp|title|user|userid|details|params|type|comment',
format: 'json'
}),
fetchAllLogEvents(localApi, {
action: 'query',
list: 'logevents',
letype: 'rights',
lestart: endTs,
leend: startTs,
lelimit: 'max',
leprop: 'timestamp|title|user|userid|details|params|type|comment',
format: 'json'
})
]).then(([metaEntries, localEntries]) => {
const metaFiltered = metaEntries.filter(entry =>
entry.title && entry.title.endsWith('@enwiki')
);
const targetMeta = ['suppress', 'checkuser'];
const targetLocal = ['sysop', 'interface-admin', 'bureaucrat'];
const metaResults = processLogEntries(metaFiltered, targetMeta);
const localResults = processLogEntries(localEntries, targetLocal);
resultsContainer.empty();
$('<h2>').text(`Rights changes from ${startLabel} to ${endLabel}`).appendTo(resultsContainer);
const tablesHtml = createTable('Meta granted', metaResults.granted) +
createTable('Meta removed', metaResults.removed) +
createTable('Local granted', localResults.granted) +
createTable('Local removed', localResults.removed);
if (tablesHtml) {
resultsContainer.append(tablesHtml);
} else {
resultsContainer.append($('<p>').text('No rights changes found for this period.'));
}
}).catch(error => {
console.error('Error:', error);
resultsContainer.empty();
resultsContainer.append($('<p>').css('color', 'red').text(`error fetching data: ${error}`));
}).finally(() => {
checkButton.setDisabled(false);
checkButton.setLabel('Check');
});
}
checkButton.on('click', fetchRightsChanges);
}
$.when(mw.loader.using('mediawiki.util'), $.ready).then(function() {
mw.util.addPortletLink(
'p-tb',
mw.util.getUrl('Special:BlankPage/RightsChanges'),
'Rights Changes',
't-rightschanges',
'View rights changes for a specific month'
);
});
if (mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage' && mw.config.get('wgTitle').split('/', 2)[1] === 'RightsChanges') {
$.when(
mw.loader.using([
'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows'
]),
$.ready
).then(function() {
initializeRightsChanges();
});
}
});
//</nowiki>