/**
* AdminNewsletterTools
* Administrators' newsletter related updates for a specific month
* @author DreamRimmer ([[en:User:DreanRimmer]])
*
* <nowiki>
*/
$(document).ready(function() {
function initializeAdminNewsTools() {
$('#mw-content-text > p').remove();
$('#firstHeading').text('AdminNewsTools');
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
}),
sectionSelect = new OO.ui.DropdownWidget({
menu: {
items: [
new OO.ui.MenuOptionWidget({ data: 'all', label: 'All sections' }),
new OO.ui.MenuOptionWidget({ data: 'rights', label: 'Rights changes' }),
new OO.ui.MenuOptionWidget({ data: 'tech', label: 'Tech News' }),
new OO.ui.MenuOptionWidget({ data: 'centralized', label: 'CENT' }),
new OO.ui.MenuOptionWidget({ data: 'arbcom', label: 'ArbCom updates' }),
new OO.ui.MenuOptionWidget({ data: 'rfc', label: 'Recent RfCs' }),
new OO.ui.MenuOptionWidget({ data: 'misc', label: 'Miscellaneous' })
]
}
}),
checkButton = new OO.ui.ButtonWidget({
label: 'Check',
flags: ['primary', 'progressive']
}),
resultsContainer = $("<div>").hide();
var currentDate = new Date();
var currentMonth = String(currentDate.getMonth() + 1);
var monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
var quickLinks = $('<p>').css({
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center',
'margin': '10px 0',
'padding': '10px 15px',
'background': '#f8f9fa',
'border': '1px solid #a2a9b1',
'border-radius': '3px'
}).html(`
<a href="https://en.wikipedia.org/wiki/Wikipedia:Administrators%27_newsletter/${currentDate.getFullYear()}/${currentDate.getMonth() + 1}" target="_blank">Help with ${monthNames[currentDate.getMonth()]} ${currentDate.getFullYear()} issue</a>
<span>•</span>
<a href="https://en.wikipedia.org/wiki/Wikipedia:Administrators%27_newsletter/Archive" target="_blank">Archive</a>
<span>•</span>
<a href="https://en.wikipedia.org/wiki/Wikipedia:Administrators%27_newsletter/Subscribe" target="_blank">Subscribe</a>
<span>•</span>
<a href="https://en.wikipedia.org/wiki/Wikipedia:Administrators%27_newsletter/Write" target="_blank">Write</a>
<span>•</span>
<a href="https://en.wikipedia.org/wiki/Wikipedia_talk:Administrators%27_newsletter" target="_blank">Discuss</a>
`);
monthSelect.getMenu().selectItemByData(currentMonth);
sectionSelect.getMenu().selectItemByData('all');
var labels = {
monthLabel: $('<p>').text('Month:').css('font-weight', 'bold'),
yearLabel: $('<p>').text('Year:').css('font-weight', 'bold'),
sectionLabel: $('<p>').text('Section:').css('font-weight', 'bold'),
description: $('<p>').html('The <a href="https://en.wikipedia.org/wiki/Wikipedia:Administrators%27_newsletter" target="_blank">administrators\' newsletter</a> is a monthly update containing relevant information on administrative issues aimed at <a href="https://en.wikipedia.org/wiki/WP:ADMIN" target="_blank">administrators</a>. It is intended to help keep administrators up to date with changes to Wikipedia that they may otherwise miss. You can view various Administrators\' newsletter related updates for a specific month here. Please choose a month and year to get results.')
};
$('#mw-content-text').append(
labels.description,
quickLinks,
$('<div>').css({
'background': '#f8f9fa',
'border': '1px solid #a2a9b1',
'padding': '15px',
'margin-bottom': '20px',
'border-radius': '3px'
}).append(
$('<h3>').text('Select Month, Year and Section'),
$('<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(labels.sectionLabel, sectionSelect.$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 parseWikilinks(text, isMeta = false) {
if (!text) return '';
const baseWikiUrl = isMeta ? 'https://meta.wikimedia.org' : 'https://en.wikipedia.org';
try {
text = text.replace(/\[([^\s\]]+)\s+([^\]]+)\]/g, function(match, url, displayText) {
try {
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
return `<a href="${url}" target="_blank">${displayText}</a>`;
}
return match;
} catch (e) {
return match;
}
});
text = text.replace(/\[([^\s\]]+)\]/g, function(match, url) {
try {
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
return `<a href="${url}" target="_blank">${url}</a>`;
}
return match;
} catch (e) {
return match;
}
});
text = text.replace(/\[\[([^\]|]+)(\|([^\]]+))?\]\]/g, function(match, link, pipe, display) {
try {
const displayText = display || link;
let url;
if (link.startsWith('Special:')) {
if (link.startsWith('Special:Diff/')) {
const diffPart = link.replace('Special:Diff/', '');
const hashIndex = diffPart.indexOf('#');
const diffId = hashIndex !== -1 ? diffPart.substring(0, hashIndex) : diffPart;
const fragment = hashIndex !== -1 ? diffPart.substring(hashIndex) : '';
if (/^\d+$/.test(diffId)) {
url = `${baseWikiUrl}/w/index.php?diff=${diffId}${fragment}`;
} else {
return match;
}
} else if (link.startsWith('Special:Contributions/')) {
const user = link.replace('Special:Contributions/', '');
if (user.trim()) {
url = `${baseWikiUrl}/wiki/Special:Contributions/${encodeURIComponent(user)}`;
} else {
return match;
}
} else {
url = `${baseWikiUrl}/wiki/${encodeURIComponent(link)}`;
}
} else if (link.startsWith('User:') || link.startsWith('User talk:')) {
url = `${baseWikiUrl}/wiki/${encodeURIComponent(link)}`;
} else if (link.startsWith('Wikipedia:')) {
url = `${baseWikiUrl}/wiki/${encodeURIComponent(link)}`;
} else {
url = `${baseWikiUrl}/wiki/${encodeURIComponent(link)}`;
}
return `<a href="${url}" target="_blank">${displayText}</a>`;
} catch (e) {
return match;
}
});
return text;
} catch (e) {
return text;
}
}
function getWeeksInMonth(year, month) {
const weeks = [];
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
const weekNumber = getISOWeek(currentDate);
if (!weeks.includes(weekNumber)) {
weeks.push(weekNumber);
}
currentDate.setDate(currentDate.getDate() + 1);
}
return weeks;
}
function getISOWeek(date) {
const target = new Date(date.valueOf());
const dayNr = (date.getDay() + 6) % 7;
target.setDate(target.getDate() - dayNr + 3);
const jan4 = new Date(target.getFullYear(), 0, 4);
const dayDiff = (target - jan4) / 86400000;
return 1 + Math.ceil(dayDiff / 7);
}
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, entry.logid, entry.comment || '']);
}
if (oldGroups.has(grp) && !newGroups.has(grp)) {
removed.push([timestamp, userTarget, grp, actor, entry.logid, entry.comment || '']);
}
});
});
return { granted: granted.sort(), removed: removed.sort() };
}
function createRightsTable(title, data, isMeta = false) {
if (!data.length) return '';
let html = `<h4>${title}</h4><table class="wikitable sortable"><thead><tr><th>Timestamp</th><th>User</th><th>Right</th><th>By</th><th>Comment</th></tr></thead><tbody>`;
data.forEach(([ts, tgt, grp, by, logid, comment]) => {
const sign = title.includes('granted') ? '+' : '-';
const baseUrl = isMeta ? 'https://meta.wikimedia.org' : 'https://en.wikipedia.org';
const logUrl = `${baseUrl}/w/index.php?title=Special:Log&logid=${logid}`;
const timestampLink = `<a href="${logUrl}" target="_blank">${formatDateFromTimestamp(ts)}</a>`;
let userLink, byLink;
if (isMeta) {
const cleanUsername = tgt.replace('@enwiki', '');
const finalUsername = cleanUsername.startsWith('User:') ? cleanUsername.substring(5) : cleanUsername;
userLink = `<a href="https://en.wikipedia.org/wiki/User:${encodeURIComponent(finalUsername)}" target="_blank">${tgt}</a>`;
const finalByUsername = by.startsWith('User:') ? by.substring(5) : by;
byLink = `<a href="https://meta.wikimedia.org/wiki/User:${encodeURIComponent(finalByUsername)}" target="_blank">${by}</a>`;
} else {
const finalTgtUsername = tgt.startsWith('User:') ? tgt.substring(5) : tgt;
userLink = `<a href="https://en.wikipedia.org/wiki/User:${encodeURIComponent(finalTgtUsername)}" target="_blank">${tgt}</a>`;
const finalByUsername = by.startsWith('User:') ? by.substring(5) : by;
byLink = `<a href="https://en.wikipedia.org/wiki/User:${encodeURIComponent(finalByUsername)}" target="_blank">${by}</a>`;
}
html += `<tr><td>${timestampLink}</td><td>${userLink}</td><td>${sign}${grp}</td><td>${byLink}</td><td>${parseWikilinks(comment, isMeta)}</td></tr>`;
});
html += '</tbody></table>';
return html;
}
function createEditTable(title, data, showDiff = false) {
if (!data.length) return `<p>No ${title.toLowerCase()} found for this period.</p>`;
const headerLabel = title.includes('ArbCom topics') ? 'Topic' : 'Comment';
const userLabel = title.includes('ArbCom topics') ? 'Arbitrator/Clerk' : 'User';
const dateLabel = title.includes('ArbCom topics') ? 'Timestamp' : 'Date';
let html = `<h4>${title}</h4><table class="wikitable sortable"><thead><tr><th>${dateLabel}</th><th>${userLabel}</th><th>${headerLabel}</th></tr></thead><tbody>`;
data.forEach(item => {
const timestamp = showDiff && item.revid ?
`<a href="https://en.wikipedia.org/w/index.php?diff=${item.revid}" target="_blank">${formatDateFromTimestamp(item.timestamp)}</a>` :
formatDateFromTimestamp(item.timestamp);
const finalUsername = item.user.startsWith('User:') ? item.user.substring(5) : item.user;
const userLink = `<a href="https://en.wikipedia.org/wiki/User:${encodeURIComponent(finalUsername)}" target="_blank">${item.user}</a>`;
let cellContent = parseWikilinks(item.comment || '', false);
if (title.includes('ArbCom topics') && item.comment && item.revid) {
const sectionMatch = item.comment.match(/\/\*\s*([^*]+?)\s*\*\//);
if (sectionMatch) {
const sectionName = sectionMatch[1].trim();
const encodedSection = encodeURIComponent(sectionName.replace(/ /g, '_'));
cellContent = `<a href="https://en.wikipedia.org/w/index.php?diff=${item.revid}#${encodedSection}" target="_blank">${sectionName}</a>`;
}
}
html += `<tr><td>${timestamp}</td><td>${userLink}</td><td>${cellContent}</td></tr>`;
});
html += '</tbody></table>';
return html;
}
function createCollapsibleSection(title, content, extraLinks = '') {
const sectionId = title.replace(/\s+/g, '').toLowerCase();
return `
<div style="border: 1px solid #ddd; margin-bottom: 15px; border-radius: 3px;">
<div style="background: #f8f9fa; padding: 10px; border-bottom: 1px solid #ddd; cursor: pointer;" onclick="toggleSection('${sectionId}')">
<h3 style="margin: 0; display: inline-block;">${title}</h3>
<span id="${sectionId}-toggle" style="float: right;">[hide]</span>
${extraLinks}
</div>
<div id="${sectionId}" style="display: block; padding: 15px;">
${content}
</div>
</div>
`;
}
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 fetchAllRevisions(api, params, results = []) {
return api.get(params).then(response => {
const pages = response.query.pages || {};
Object.values(pages).forEach(page => {
if (page.revisions) {
results.push(...page.revisions);
}
});
if (response.continue) {
return fetchAllRevisions(api, { ...params, ...response.continue }, results);
}
return results;
});
}
const metaApi = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php', { headers: { 'Api-User-Agent': 'User:DreamRimmer/adminnewslettertools.js; https://en.wikipedia.org/wiki/User:DreamRimmer' } });
const localApi = new mw.Api({ headers: { 'Api-User-Agent': 'User:DreamRimmer/adminnewslettertools.js; https://en.wikipedia.org/wiki/User:DreamRimmer' } });
function fetchRightsChanges(startTs, endTs, startLabel, endLabel) {
return 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|ids',
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|ids',
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);
const tablesHtml = createRightsTable('Meta granted', metaResults.granted, true) +
createRightsTable('Meta removed', metaResults.removed, true) +
createRightsTable('Local granted', localResults.granted, false) +
createRightsTable('Local removed', localResults.removed, false);
return tablesHtml || '<p>No rights changes found for this period.</p>';
});
}
function fetchTechNews(year, month) {
const weeks = getWeeksInMonth(year, month);
const promises = weeks.map(week => {
return metaApi.get({
action: 'parse',
page: `Tech/News/${year}/${week}`,
format: 'json',
prop: 'text'
}).then(response => {
if (response.parse && response.parse.text) {
const html = response.parse.text['*'];
const match = html.match(/(\d{4}), week (\d+) \(([^)]+)\)/);
if (match) {
const weekDate = match[3];
const parsedDate = new Date(weekDate);
if (parsedDate.getMonth() === month - 1) {
return { week, date: weekDate, url: `https://meta.wikimedia.org/wiki/Tech/News/${year}/${week}` };
}
}
}
return null;
}).catch(() => null);
});
return Promise.all(promises).then(results => {
const validWeeks = results.filter(r => r !== null);
if (!validWeeks.length) {
return '<p>No tech news found for this period.</p>';
}
let html = '<h4>Tech News Links</h4><ul>';
validWeeks.forEach(({ week, date, url }) => {
html += `<li><a href="${url}" target="_blank">Tech/News/${year}/${week}</a> (${date})</li>`;
});
html += '</ul>';
return html;
});
}
function fetchCentralizedDiscussion(startTs, endTs) {
return fetchAllRevisions(localApi, {
action: 'query',
titles: 'Template:Centralized discussion',
prop: 'revisions',
rvstart: endTs,
rvend: startTs,
rvlimit: 'max',
rvprop: 'timestamp|user|comment|ids',
format: 'json'
}).then(revisions => {
return createEditTable('CENT edits', revisions, true);
});
}
function fetchArbComNotices(startTs, endTs) {
return fetchAllRevisions(localApi, {
action: 'query',
titles: 'Wikipedia:Arbitration Committee/Noticeboard',
prop: 'revisions',
rvstart: endTs,
rvend: startTs,
rvlimit: 'max',
rvprop: 'timestamp|user|comment|tags|ids',
format: 'json'
}).then(revisions => {
const filtered = revisions.filter(rev => rev.tags && rev.tags.includes('discussiontools-newtopic'));
return createEditTable('ArbCom topics', filtered, true);
});
}
function fetchRecentRfCs() {
const rfcPages = [
{ name: 'Biographies', url: 'Wikipedia:Requests_for_comment/Biographies' },
{ name: 'Economy, trade, and companies', url: 'Wikipedia:Requests_for_comment/Economy,_trade,_and_companies' },
{ name: 'History and geography', url: 'Wikipedia:Requests_for_comment/History_and_geography' },
{ name: 'Language and linguistics', url: 'Wikipedia:Requests_for_comment/Language_and_linguistics' },
{ name: 'Maths, science, and technology', url: 'Wikipedia:Requests_for_comment/Maths,_science,_and_technology' },
{ name: 'Art, architecture, literature, and media', url: 'Wikipedia:Requests_for_comment/Art,_architecture,_literature,_and_media' },
{ name: 'Politics, government, and law', url: 'Wikipedia:Requests_for_comment/Politics,_government,_and_law' },
{ name: 'Religion and philosophy', url: 'Wikipedia:Requests_for_comment/Religion_and_philosophy' },
{ name: 'Society, sports, and culture', url: 'Wikipedia:Requests_for_comment/Society,_sports,_and_culture' },
{ name: 'Wikipedia style and naming', url: 'Wikipedia:Requests_for_comment/Wikipedia_style_and_naming' },
{ name: 'Wikipedia policies and guidelines', url: 'Wikipedia:Requests_for_comment/Wikipedia_policies_and_guidelines' },
{ name: 'WikiProjects and collaborations', url: 'Wikipedia:Requests_for_comment/WikiProjects_and_collaborations' },
{ name: 'Wikipedia technical issues and templates', url: 'Wikipedia:Requests_for_comment/Wikipedia_technical_issues_and_templates' },
{ name: 'Wikipedia proposals', url: 'Wikipedia:Requests_for_comment/Wikipedia_proposals' },
{ name: 'Unsorted', url: 'Wikipedia:Requests_for_comment/Unsorted' },
{ name: 'User names', url: 'Wikipedia:Requests_for_comment/User_names' }
];
const promises = rfcPages.map(page => {
return localApi.get({
action: 'parse',
page: page.url,
format: 'json',
prop: 'wikitext'
}).then(response => {
if (response.parse && response.parse.wikitext) {
const wikitext = response.parse.wikitext['*'];
const rfcs = parseRfCs(wikitext, page.name);
return rfcs;
}
return [];
}).catch(() => []);
});
return Promise.all(promises).then(results => {
const allRfcs = results.flat();
console.log('Total RfCs found:', allRfcs.length);
console.log('RfC data:', allRfcs.slice(0, 3));
return createRfCDisplay(allRfcs);
});
}
function parseRfCs(wikitext, topicName) {
const rfcs = [];
const rfcEntryPattern = /'''\[\[([^\]|]+)(?:\|([^\]]+))?\]\]'''[\s\S]*?\{\{rfcquote\|text=([\s\S]*?)\}\}[\s\S]*?(\d{2}:\d{2}, \d{1,2} \w+ \d{4} \(UTC\))/g;
const now = new Date();
const currentUtcYear = now.getUTCFullYear();
const currentUtcMonth = now.getUTCMonth();
const currentUtcDate = now.getUTCDate();
const currentUtcHours = now.getUTCHours();
const currentUtcMinutes = now.getUTCMinutes();
const currentUtc = new Date(Date.UTC(currentUtcYear, currentUtcMonth, currentUtcDate, currentUtcHours, currentUtcMinutes, 0, 0));
let match;
while ((match = rfcEntryPattern.exec(wikitext)) !== null) {
let pageName, anchor, displayTitle;
const link = match[1];
displayTitle = match[2] || link;
if (link.includes('#')) {
[pageName, anchor] = link.split('#');
} else {
pageName = link;
anchor = '';
}
const timestampStr = match[4];
let daysDiff = null;
if (timestampStr) {
const tsMatch = timestampStr.match(/(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/);
if (tsMatch) {
const [_, hour, minute, day, monthText, year] = tsMatch;
const months = [
"January","February","March","April","May","June","July","August","September","October","November","December"
];
const monthNum = months.indexOf(monthText);
if (monthNum !== -1) {
const tsDate = new Date(Date.UTC(
parseInt(year,10),
monthNum,
parseInt(day,10),
parseInt(hour,10),
parseInt(minute,10),
0, 0
));
daysDiff = Math.floor((currentUtc - tsDate) / (1000 * 60 * 60 * 24));
}
}
}
const url = anchor ?
`https://en.wikipedia.org/wiki/${encodeURIComponent(pageName)}#${encodeURIComponent(anchor)}` :
`https://en.wikipedia.org/wiki/${encodeURIComponent(pageName)}`;
rfcs.push({
topic: topicName,
title: displayTitle || pageName,
url: url,
daysOld: daysDiff,
timestamp: timestampStr || ""
});
}
return rfcs;
}
function createRfCDisplay(rfcs) {
const currentDate = new Date().toLocaleDateString('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC'
}) + ' UTC';
const ageOptions = [3, 7, 10, 15, 20, 25, 30];
const topicNames = [
'Biographies', 'Economy, trade, and companies', 'History and geography',
'Language and linguistics', 'Maths, science, and technology',
'Art, architecture, literature, and media', 'Politics, government, and law',
'Religion and philosophy', 'Society, sports, and culture',
'Wikipedia style and naming', 'Wikipedia policies and guidelines',
'WikiProjects and collaborations', 'Wikipedia technical issues and templates',
'Wikipedia proposals', 'Unsorted', 'User names'
];
let html = `<p><small>Current date: ${currentDate}</small></p>`;
html += `<p>Age filter: <select id="rfc-age-filter">`;
ageOptions.forEach(age => {
const selected = age === 7 ? ' selected' : '';
html += `<option value="${age}"${selected}>${age} days old</option>`;
});
html += `</select></p>`;
function renderRfCTable(filteredRfcs) {
if (filteredRfcs.length > 0) {
let table = '<h4>Current RfCs</h4><table class="wikitable sortable"><thead><tr><th>Topic</th><th>Title</th><th>Days Old</th><th>Started</th></tr></thead><tbody>';
filteredRfcs.sort((a, b) => a.daysOld - b.daysOld).forEach(rfc => {
table += `<tr><td>${rfc.topic}</td><td><a href="${rfc.url}" target="_blank">${rfc.title}</a></td><td>${rfc.daysOld}</td><td>${rfc.timestamp}</td></tr>`;
});
table += '</tbody></table>';
return table;
} else {
return `<p>No RfCs found for selected age filter.</p>`;
}
}
html += `<div id="rfc-list-table"></div>`;
html += '<p><a href="https://en.wikipedia.org/wiki/Wikipedia:Requests_for_comment/All" target="_blank">All open RfCs</a></p>';
html += `<script>
window._rfcs = ${JSON.stringify(rfcs)};
window._ageOptions = ${JSON.stringify(ageOptions)};
function updateRfCTable() {
var rfcs = window._rfcs;
var age = parseInt(document.getElementById('rfc-age-filter').value, 10);
var filteredRfcs = rfcs.filter(function(rfc) {
return !isNaN(rfc.daysOld) && rfc.daysOld <= age;
});
document.getElementById('rfc-list-table').innerHTML = (${renderRfCTable.toString()})(filteredRfcs);
}
document.getElementById('rfc-age-filter').addEventListener('change', updateRfCTable);
updateRfCTable();
</script>`;
return html;
}
function checkPageExists(title) {
return localApi.get({
action: 'query',
titles: title,
format: 'json'
}).then(response => {
const pages = response.query.pages || {};
return !Object.values(pages).some(page => page.hasOwnProperty('missing'));
}).catch(() => false);
}
function fetchData() {
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());
const section = sectionSelect.getMenu().findSelectedItem().getData();
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 currentDate = new Date();
const isCurrentOrLastMonth = (year === currentDate.getFullYear() && month >= currentDate.getMonth()) ||
(year === currentDate.getFullYear() - 1 && month === 12 && currentDate.getMonth() === 0);
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));
resultsContainer.empty();
resultsContainer.append($('<p>').text('Loading...'));
resultsContainer.show();
checkButton.setDisabled(true);
checkButton.setLabel('Loading...');
const promises = [];
const sections = [];
if (section === 'all' || section === 'rights') {
promises.push(fetchRightsChanges(startTs, endTs, startLabel, endLabel));
sections.push('rights');
}
if (section === 'all' || section === 'tech') {
promises.push(fetchTechNews(year, month));
sections.push('tech');
}
if (section === 'all' || section === 'centralized') {
promises.push(fetchCentralizedDiscussion(startTs, endTs));
sections.push('centralized');
}
if (section === 'all' || section === 'arbcom') {
promises.push(fetchArbComNotices(startTs, endTs));
sections.push('arbcom');
}
if ((section === 'all' || section === 'rfc') && isCurrentOrLastMonth) {
promises.push(fetchRecentRfCs());
sections.push('rfc');
}
Promise.all(promises).then(results => {
resultsContainer.empty();
const headerHtml = `<h2>Results from ${startLabel} to ${endLabel}</h2>`;
resultsContainer.append(headerHtml);
window.toggleSection = function(sectionId) {
const content = document.getElementById(sectionId);
const toggle = document.getElementById(sectionId + '-toggle');
if (content.style.display === 'none') {
content.style.display = 'block';
toggle.textContent = '[hide]';
} else {
content.style.display = 'none';
toggle.textContent = '[show]';
}
};
let resultIndex = 0;
if (sections.includes('rights')) {
const rightsHtml = createCollapsibleSection('Rights changes', results[resultIndex]);
resultsContainer.append(rightsHtml);
resultIndex++;
}
if (sections.includes('tech')) {
const techHtml = createCollapsibleSection('Tech News', results[resultIndex]);
resultsContainer.append(techHtml);
resultIndex++;
}
if (sections.includes('centralized')) {
const centralizedHtml = createCollapsibleSection('CENT', results[resultIndex]);
resultsContainer.append(centralizedHtml);
resultIndex++;
}
if (sections.includes('arbcom')) {
const arbcomHtml = createCollapsibleSection('ArbCom updates', results[resultIndex]);
resultsContainer.append(arbcomHtml);
resultIndex++;
}
if (sections.includes('rfc')) {
const rfcHtml = createCollapsibleSection('Recent RfCs', results[resultIndex]);
resultsContainer.append(rfcHtml);
resultIndex++;
}
if ((section === 'all' || section === 'arbcom') && isCurrentOrLastMonth) {
const arbcomTasksHtml = createCollapsibleSection('Arbitration Committee open and recently closed cases', '<p><a href="https://en.wikipedia.org/wiki/Template:ArbComOpenTasks" target="_blank">Arbitration Committee open and recently closed cases</a></p>');
resultsContainer.append(arbcomTasksHtml);
}
if (section === 'all' || section === 'misc') {
const miscPromises = [];
let miscSections = [];
if (year >= 2025 && isCurrentOrLastMonth) {
miscSections.push('<h4>Backlog drive schedule</h4><p><a href="https://en.wikipedia.org/wiki/Wikipedia:Backlog_drive_schedule" target="_blank">Wikipedia:Backlog drive schedule</a></p>');
}
if ((year > 2024 || (year === 2024 && month >= 10)) && isCurrentOrLastMonth) {
miscSections.push('<h4>Administrator elections</h4><p><a href="https://en.wikipedia.org/wiki/Wikipedia:Administrator_elections" target="_blank">Administrator elections</a> updates</p>');
}
if (year >= 2006) {
miscPromises.push(
checkPageExists(`Wikipedia:Arbitration_Committee_Elections_December_${year}`)
.then(exists => exists ? `<h4>Arbitration Committee elections</h4><p><a href="https://en.wikipedia.org/wiki/Wikipedia:Arbitration_Committee_Elections_December_${year}" target="_blank">Arbitration Committee elections</a></p>` : '')
);
} else {
miscPromises.push(Promise.resolve(''));
}
if (year >= 2024 && isCurrentOrLastMonth) {
miscSections.push('<h4>UCoC election updates</h4><p><a href="https://meta.wikimedia.org/wiki/Universal_Code_of_Conduct/Coordinating_Committee/Election" target="_blank">UCoC election updates</a></p>');
}
if (miscPromises.length > 0) {
Promise.all(miscPromises).then(dynamicSections => {
let miscContent = miscSections.join('') + dynamicSections.join('');
if (miscContent) {
const miscHtml = createCollapsibleSection('Miscellaneous', miscContent);
resultsContainer.append(miscHtml);
}
});
} else {
let miscContent = miscSections.join('');
if (miscContent) {
const miscHtml = createCollapsibleSection('Miscellaneous', miscContent);
resultsContainer.append(miscHtml);
}
}
}
}).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', fetchData);
}
$.when(mw.loader.using('mediawiki.util'), $.ready).then(function() {
mw.util.addPortletLink(
'p-tb',
mw.util.getUrl('Special:BlankPage/AdminNewsTools'),
'Administrators\' newsletter tools',
't-adminnewstools',
'View various Administrators\' newsletter related updates for a specific month'
);
});
if (mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage' && mw.config.get('wgTitle').split('/', 2)[1] === 'AdminNewsTools') {
$.when(
mw.loader.using([
'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows'
]),
$.ready
).then(function() {
initializeAdminNewsTools();
});
}
});
//</nowiki>