User:Qwerfjkl/scripts/massXFD.js: Difference between revisions

Content deleted Content added
cleanup, bugfixes: adding nomination to page, filtering users to notify [Factotum]
Update with bug fix for CfD, cleaning up code significantly, and code for getting redirect data [Factotum]
Line 1:
// <nowiki>
// urgenthandle bugs:redirects
// toggle bug shows bad elements
// undefined everywhere - needs logging
 
 
// todo: make counter inline, remove progresss and progressElement from editPAge(), more dynamic reatelimit wait.
Line 567 ⟶ 564:
}
});
});
}
 
function getRedirectData(titles) {
var api = new mw.Api();
return api.get({
action: 'query',
titles,
redirects: 1,
format: 'json'
}).then(function (data) {
return data.query; // needs redirects and normalized
}).catch(function (error) {
console.error('Error occurred while fetching page author:', error);
return false;
});
}
Line 611 ⟶ 623:
});
}
 
 
// Function to create a list of page authors and filter duplicates
Line 631 ⟶ 644:
.then(response => {
response.forEach(user => {
console.log(user)
if (user
&& (!user.blockexpiry || user.blockexpiry !== "infinite" || 'blockpartial' in user)
&& !user.groups?.includes('bot')
&& !filteredAuthorList.includes('User talk:' + user.name))
filteredAuthorList.push('User talk:' + user.name);
Line 650 ⟶ 664:
}
 
// Function to prependcreate texta tolist aof page authors and filter duplicates
async function createRedirectTargetsList(titles) {
function editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry = false) {
try {
let queryBatchSize = 50;
let redirectTitles = titles.map(title => title.replace(/ /g, '_')); // Replace spaces with underscores
let redirectTargets = {};
for (let i = 0; i < redirectTitles.length; i += queryBatchSize) {
let batch = redirectTitles.slice(i, i + queryBatchSize);
let batchTitles = batch.join('|');
 
await getRedirectData(batchTitles)
.then(data => {
let normalized = {};
data.normalized.forEach(normalizedTitle => {
normalized[normalizedTitle.to] = normalizedTitle.from;
}
)
let redirects = data.redirects;
 
redirects.forEach(item => {
if (item.from in normalized) redirectTargets[normalized[item.from]] = item.to;
else redirectTargets[item.from] = item.to;
})
})
.catch(error => {
console.error("Error querying API:", error);
});
}
return redirectTargets;
} catch (error_1) {
console.error('Error occurred while fetching redirect targets', error_1);
return redirectTargets;
}
}
 
function editPage(options) {
console.log(options)
options.text = options.textToModify;
var api = new mw.Api();
 
Line 658 ⟶ 708:
 
 
messageElement.setLabel((options.retry) ? $('<span>').text('Retrying ').append($(makeLink(options.title))) : $('<span>').text('Editing ').append($(makeLink(options.title))));
options.progressElement.$element.append(messageElement.$element);
var container = $('.sticky-container');
container.scrollTop(container.prop("scrollHeight"));
if (options.retry) {
sleep(1000);
}
Line 668 ⟶ 718:
var requestData = {
action: 'edit',
//title: 'User:Qwerfjkl/sandbox/51',
summarytitle: options.title,
summary: options.summary,
format: 'json'
};
 
if (options.type === 'prepend') { // tagtagging
requestData.nocreate = 1; // don't create new page when tagging
// parse title
var targets = options.titlesDict[options.title];
 
for (let i = 0; i < targets.length; i++) {
// we add 1 to i in the replace function because placeholders start from $1 not $0
let placeholder = '$' + (i + 1);
options.text = options.text.replace(placeholder, targets[i]);
}
options.text = options.text.replace(/\$\d/g, ''); // remove unmatched |$x
requestData.prependtext = options.text.trim() + '\n\n';
 
 
} else if (options.type === 'append') { // user
requestData.appendtext = '\n\n' + options.text.trim();
} else if (options.type === 'text') {
requestData.text = options.text;
}
console.log(requestData)
return new Promise(function (resolve, reject) {
if (window.abortEdits) {
Line 702 ⟶ 754:
if (data.edit && data.edit.result === 'Success') {
messageElement.setType('success');
messageElement.setLabel($('<span>' + makeLink(options.title) + ' edited successfully</span><span class="massxfdundo" data-revid="' + data.edit.newrevid + '" data-title="' + options.title + '"></span>'));
 
resolve();
Line 708 ⟶ 760:
 
messageElement.setType('error');
messageElement.setLabel($('<span>Error occurred while editing ' + makeLink(options.title) + ': ' + data + '</span>'));
console.error('Error occurred while prepending text to page:', data);
 
reject();
}
}).catch(function (error) {
messageElement.setType('error');
messageElement.setLabel($('<span>Error occurred while editing ' + makeLink(options.title) + ': ' + error + '</span>'));
console.error('Error occurred while prepending text to page:', error); // handle: editconflict, ratelimit (retry)
 
if (error == 'editconflict') {
editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry = trueoptions).then(function () {
resolve();
});
} else if (error == 'ratelimited') {
options.progress.setDisabled(true);
 
handleRateLimitError(options.ratelimitMessage).then(function () {
options.progress.setDisabled(false);
editPage(title, text, summary, progressElement, ratelimitMessage, progress, type, titlesDict, retry = trueoptions).then(function () {
resolve();
});
Line 1,076 ⟶ 1,128:
 
 
startButton.on('click', async function () {
 
var isOld = nominationToggleOld.isSelected();
Line 1,107 ⟶ 1,159:
}
batches = actions.map(function ({ titleListInputField, prependTextInputField, label, actionInputField }) {
if (!(prependTextInputField.getValue().trim()) || (XFD === 'RFD' && !prependTextInputField.getValue().includes('${pageText}'))) {
prependTextInputField.setValidityFlag(false);
error = true;
Line 1,191 ⟶ 1,243:
}, []);
 
let authors = await createAuthorList(allTitles).then(function (authors) {;
if (XFD === 'RFD') var redirectTargets = await createRedirectTargetsList(allTitles);
function processContent(content, titles, textToModify, summary, type, doneMessage, headingLabel) {
if (!Arrayconsole.isArraylog(titles)redirectTargets) {;
function processContent(options) var titlesDict = titles;{
// let {content, titles, textToModify, summary, type, doneMessage, titlesheadingLabel} = Object.keys(titles);options
}console.log(options)
var fieldset = createFieldsetconsole.log(headingLabeloptions.titles);
if (!Array.isArray(options.titles)) {
options.titlesDict = options.titles;
options.titles = Object.keys(options.titles);
} else options.titlesDict = {}
var fieldset = createFieldset(options.headingLabel);
 
contentbodyContent.append(fieldset.$element);
 
var options.progressElement = createProgressElement();
fieldset.addItems([options.progressElement]);
 
var options.ratelimitMessage = createRatelimitMessage();
options.ratelimitMessage.toggle(false);
fieldset.addItems([options.ratelimitMessage]);
 
var progressObj = createProgressBar(`(0 / ${options.titles.length}, 0 errors)`); // with label
var options.progress = progressObj.progressBar;
var progressContainer = progressObj.fieldlayout;
// Add margin or padding to the progress bar widget
options.progress.$element.css('margin-top', '5px');
options.progress.pushPending();
fieldset.addItems([progressContainer]);
 
let resolvedCount = 0;
let rejectedCount = 0;
 
function updateCounter() {
progressContainer.setLabel(`(${resolvedCount} / ${options.titles.length}, ${rejectedCount} errors)`);
}
function updateProgress() {
var percentage = (resolvedCount + rejectedCount) / options.titles.length * 100;
options.progress.setProgress(percentage);
 
}
 
function trackPromise(promise) {
return new Promise((resolve, reject) => {
promise
.then(value => {
resolvedCount++;
updateCounter();
updateProgress();
resolve(value);
})
.catch(error => {
rejectedCount++;
updateCounter();
updateProgress();
resolve(error);
});
});
}
 
return new Promise(async function (resolve) {
var promises = [];
for (const title of titles) {
// RfD needs special handling here, because it wraps around the whole page content
if (XFD === 'RFD' && type === 'prepend') { // prepend implicitly means page tagging, not actually prepend in this case
text = await getWikitext(title);
textToModify = textToModify.replace('${pageText}', text);
type = 'text';
}
var promise = editPage(title, textToModify, summary, progressElement, ratelimitMessage, progress, type, titlesDict);
promises.push(trackPromise(promise));
await sleep(100); // space out calls
await massXFDratelimitPromise; // stop if ratelimit reached (global variable)
}
 
Promise.allSettled(promises)
.then(function () {
progress.toggle(false);
if (window.abortEdits) {
var abortMessage = createAbortMessage();
abortMessage.setLabel($('<span>Edits manually aborted. <a id="massxfdrevertlink" onclick="revertEdits()">Revert?</a></span>'));
 
content.append(abortMessage.$element);
} else {
var completedElement = createCompletedElement();
completedElement.setLabel(doneMessage);
completedElement.$element.css('margin-bottom', '16px');
content.append(completedElement.$element);
}
resolve();
})
.catch(function (error) => {
console.error("Error occurred during title processing:", error)rejectedCount++;
resolveupdateCounter();
updateProgress();
resolve(error);
})
.error(error => {
rejectedCount++;
updateCounter();
updateProgress();
resolve(error);
});
});
}
 
constreturn datenew =Promise(async newfunction Date(resolve); {
var promises = [];
for (const title of options.titles) {
// RfD needs special handling here, because it wraps around the whole page content
if (XFD === 'RFD' && options.type === 'prepend') { // prepend implicitly means page tagging, not actually prepend in this case
text = await getWikitext(title);
console.log(text)
console.log(options.textToModify)
options.textToModify = options.textToModify.replace('${pageText}', text);
options.type = 'text';
}
options.title = title
var promise = editPage(options);
promises.push(trackPromise(promise));
if (!window.abortEdits) await sleep(100); // space out calls - not needed if they're being rejected
await massXFDratelimitPromise; // stop if ratelimit reached (global variable)
}
 
const year = date Promise.getUTCFullYearallSettled(promises);
const month = date.toLocaleString('en', { month: 'long', timeZone: 'UTC'.then(function }(); {
const day = date options.progress.getUTCDatetoggle(false);
if (window.abortEdits) {
var abortMessage = createAbortMessage();
let revertEditsLink = $('<a id="massxfdrevertlink">Revert?</a>')
revertEditsLink.on('click', revertEdits)
abortMessage.setLabel($('<span>').append('Edits manually aborted. ').append(revertEditsLink));
 
var summaryDiscussionLink bodyContent.append(abortMessage.$element);
var discussionPage = `${config.baseDiscussionPage}${year} ${month } $else {day}`;
var completedElement = createCompletedElement();
completedElement.setLabel(options.doneMessage);
completedElement.$element.css('margin-bottom', '16px');
bodyContent.append(completedElement.$element);
}
resolve();
})
.catch(function (error) {
console.error("Error occurred during title processing:", error);
resolve();
});
});
}
 
const date = new if (isOld) summaryDiscussionLink = discussionLinkInputField.getValue().trimDate();
else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
 
const advSummaryyear = ' date.getUTCFullYear([[User:Qwerfjkl/scripts/massXFD.js|via MassXfD.js]])';
const month = date.toLocaleString('en', //{ WIPmonth: 'long', nottimeZone: 'UTC' finished});
const categorySummaryday = 'Tagging page for [[' + summaryDiscussionLink + ']]' + advSummarydate.getUTCDate();
const userSummary = 'Notifying user about [[' + summaryDiscussionLink + ']]' + advSummary;
const userNotification = `{{ subst: ${config.userNotificationTemplate} | ${summaryDiscussionLink} }} ~~~~`;
const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]` + advSummary;
 
var batchesToProcess = []summaryDiscussionLink;
var discussionPage = `${config.baseDiscussionPage}${year} ${month} ${day}`;
 
if (isOld) summaryDiscussionLink = discussionLinkInputField.getValue().trim();
else if (isNew) summaryDiscussionLink = `${discussionPage}#${newNomHeaderInputField.getValue().trim()}`;
 
const advSummary = ' ([[User:Qwerfjkl/scripts/massXFD.js|via MassXfD.js]])';
// WIP, not finished
const categorySummary = 'Tagging page for [[' + summaryDiscussionLink + ']]' + advSummary;
const userSummary = 'Notifying user about [[' + summaryDiscussionLink + ']]' + advSummary;
const userNotification = `{{ subst: ${config.userNotificationTemplate} | ${summaryDiscussionLink} }} ~~~~`;
const nominationSummary = `Adding mass nomination at [[#${newNomHeaderInputField.getValue().trim()}]]` + advSummary;
if (XFD === 'RFD') {
var redirectTargetNotification = `{{subst:Rfd notice|\${redirectTitle}|${summaryDiscussionLink}}}`
var redirectTargetNotificationSummary = `Notice of [[${summaryDiscussionLink}]]${advSummary}`
}
var batchesToProcess = [];
 
var newNomPromise = new Promise(function (resolve) {
if (isNew) {
nominationText = `==== ${newNomHeaderInputField.getValue().trim()} ====\n`;
for (const batch of batches) {
var action = batch.actionInputField.getValue().trim();
for (const page of Object.keys(batch.titles)) {
if (XFD == 'CFD') {
var targets = batch.titles[page].slice(); // copy array
var targetText = '';
if (targets.length) {
if (targets.length === 2) {
targetText = ` to [[:${targets[0]}]] and [[:${targets[1]}]]`;
}
else if (targets.length > 2) {
var lastTarget = targets.pop();
targetText = ' to [[:' + targets.join(']], [[:') + ']], and [[:' + lastTarget + ']]';
} else { // 1 target
targetText = ' to [[:' + targets[0] + ']]';
}
}
nominationText += `:* '''Propose ${action}''' {{${categoryTemplateDropdown.getValue()}|${page}}}${targetText}\n`;
} else {
nominationText += config.displayTemplate.replaceAll('${pageName}', page) + '\n';
}
nominationText += `:* '''Propose ${action}''' {{${categoryTemplateDropdown.getValue()}|${page}}}${targetText}\n`;
} else {
nominationText += config.displayTemplate.replaceAll('${pageName}', page) + '\n';
}
}
}
var rationale = rationaleInputField.getValue().trim().replace(/\n/, '<br />');
nominationText += `${XFD === 'CFD' ? ":'''Nominator's rationale:''' " : ''}${rationale} ~~~~`;
var newText;
 
getWikitext(discussionPage).then(function (wikitext) {
if (!wikitext.match(config.nominationReplacement[0])) {
var nominationErrorMessage = createNominationErrorMessage();
bodyContent.append(nominationErrorMessage.$element);
} else {
newText = wikitext.replace(...config.nominationReplacement).replace('${nominationText}', nominationText);
batchesToProcess.push({
contenttitles: bodyContent[discussionPage],
titlestextToModify: [discussionPage]newText,
textToModifysummary: newTextnominationSummary,
summarytype: nominationSummary'text',
typedoneMessage: 'textNomination added',
doneMessageheadingLabel: 'NominationCreating addednomination',
headingLabel: 'Creating nomination'});
});
resolve();
}
}).catch(function (error) {
console.error('An error occurred in fetching wikitext:', error);
resolve();
});
}).catch(function else resolve(error); {
console.error('An error occurred in fetching wikitext:', error);
resolve();
});
} else resolve();
});
newNomPromise.then(async function () {
batches.forEach(batch => {
batchesToProcess.push({
titles: batch.titles,
textToModify: batch.prependText,
summary: categorySummary,
type: 'prepend',
doneMessage: 'All pages edited.',
headingLabel: 'Editing nominated pages' + ((batches.length > 1) ? ' — ' + batch.label : '')
});
});
newNomPromise.thenif (asyncXFD function=== ('RFD') {
batchesbatchesToProcess.forEachpush(batch => {
batchesToProcesstitles: Object.pushvalues(redirectTargets).map(title => {
content:let bodyContent,page = new mw.Title(title)
titles:return batchpage.titles,getTalkPage().getPrefixedText()
textToModify: batch.prependText}),
summarytextToModify: categorySummaryredirectTargetNotification,
typesummary: 'prepend'redirectTargetNotificationSummary,
doneMessagetype: 'All pages edited.append',
headingLabeldoneMessage: 'EditingAll nominatedtarget talk pages' + ((batchesnotified.length > 1) ? ' — ' + batch.label : ''),
});headingLabel: 'Notifying targets'
});
}
if (notifyCheckbox.isSelected()) {
batchesToProcess.push({
contenttitles: bodyContentauthors,
titlestextToModify: authorsuserNotification,
textToModifysummary: userNotificationuserSummary,
summarytype: userSummary'append',
doneMessage: 'All users type: 'appendnotified.',
doneMessageheadingLabel: 'AllNotifying users notified.',
headingLabel: 'Notifying users'
});
}
let promise = Promise.resolve();
// abort handling is now only in the editPage() function
for (const batch of batchesToProcess) {
await processContent(...Object.values(batch));
}
 
promise.then(() => {
abortButton.setLabel('Revert');
// All done
}).catch(err => {
console.error('Error occurred:', err);
});
}
let promise = Promise.resolve();
// abort handling is now only in the editPage() function
for (const batch of batchesToProcess) {
await processContent(batch);
}
 
promise.then(() => {
abortButton.setLabel('Revert');
// All done
}).catch(err => {
console.error('Error occurred:', err);
});
});
 
});
});