Content deleted Content added
DreamRimmer (talk | contribs) + |
DreamRimmer (talk | contribs) + |
||
Line 1:
/**
* ANewsTools
* Administrators' newsletter related updates for a specific month
* @author DreamRimmer ([[en:User:DreanRimmer]])
*
* <nowiki>
*/
$(document).ready(function() {
function
$('#mw-content-text > p').remove();
$('#firstHeading').text('
var monthSelect = new OO.ui.DropdownWidget({
Line 28 ⟶ 34:
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({
Line 37 ⟶ 56:
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'),
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,
$('<div>').css({
'background': '#f8f9fa',
Line 55 ⟶ 98:
'border-radius': '3px'
}).append(
$('<h3>').text('Select Month, Year and
$('<div>').css({
'display': 'flex',
Line 64 ⟶ 107:
$('<div>').append(labels.monthLabel, monthSelect.$element),
$('<div>').append(labels.yearLabel, yearInput.$element),
$('<div>').append(labels.sectionLabel, sectionSelect.$element),
$('<div>').append(checkButton.$element)
)
Line 79 ⟶ 123:
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);
}
Line 127 ⟶ 271:
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 || '']);
}
});
Line 138 ⟶ 282:
}
function
if (!data.length) return '';
let html = `<
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>`;
let html = `<h4>${title}</h4><table class="wikitable sortable"><thead><tr><th>Date</th><th>User</th><th>Comment</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>`;
html += `<tr><td>${timestamp}</td><td>${userLink}</td><td>${parseWikilinks(item.comment || '', false)}</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>
`;
}
Line 162 ⟶ 361:
}
function
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': 'https://en.wikipedia.org/wiki/User:DreamRimmer' } });
const localApi = new mw.Api();
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 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) {
Line 171 ⟶ 515:
const month = parseInt(selectedItem.getData());
const year = parseInt(yearInput.getValue());
const section = sectionSelect.getMenu().findSelectedItem().getData();
if (year < 2005 || year > new Date().getFullYear() + 1) {
Line 176 ⟶ 521:
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));
Line 185 ⟶ 534:
const startLabel = formatDate(startDt);
const endLabel = formatDate(new Date(nextMonthDt.getTime() - 1000));
resultsContainer.empty();
Line 196 ⟶ 542:
checkButton.setLabel('Loading...');
const sections =
if (section === 'all'
sections.push('rights');
}
if (section === 'all'
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');
}
Promise.all(promises).then(results => {
resultsContainer.empty();
resultsContainer.append(headerHtml);
const content =
const toggle = document.getElementById(sectionId +
if (content.style.display ===
content.style.display = 'block';
toggle.textContent = '[hide]';
} else {
content.style.display = 'none';
toggle.textContent = '[show]';
}
};
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 ((section === 'all' || section === 'arbcom') && isCurrentOrLastMonth) {
const arbcomTasksHtml = createCollapsibleSection('ArbCom Open and Recently closed tasks', '<p><a href="https://en.wikipedia.org/wiki/Template:ArbComOpenTasks" target="_blank">ArbCom Open and Recently closed tasks</a></p>');
resultsContainer.append(arbcomTasksHtml);
}
if ((section === 'all' || section === 'rfc') && isCurrentOrLastMonth) {
const rfcHtml = createCollapsibleSection('Recent RfCs', '<p><a href="https://en.wikipedia.org/wiki/Wikipedia:Requests_for_comment/All" target="_blank">Recent RfCs</a></p>');
resultsContainer.append(rfcHtml);
}
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(`
}).finally(() => {
checkButton.setDisabled(false);
Line 252 ⟶ 672:
}
checkButton.on('click',
}
Line 258 ⟶ 678:
mw.util.addPortletLink(
'p-tb',
mw.util.getUrl('Special:BlankPage/
'
't-
'View
);
});
if (mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage' && mw.config.get('wgTitle').split('/', 2)[1] === '
$.when(
mw.loader.using([
Line 272 ⟶ 692:
$.ready
).then(function() {
});
}
|