Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
Documentation for this user script can be added at User:Barkeep49/rfxStarter.
/**
* RfX Starter User Script for Wikipedia
* Helps users submit RfA/RfB nominations by removing substitution-blocking comments and adding transclusions.
* Version 1.0.0
*/
(function() {
'use strict';
// --- Configuration ---
const config = {
pageName: mw.config.get('wgPageName'),
userName: mw.config.get('wgUserName') || 'YourUsername',
tagLine: ' (using [[User:Barkeep49/rfaStarter.js|rfaStarter]])',
apiDefaults: {
format: 'json',
formatversion: 2
}
};
// Determine RfX type and base page name
config.rfxType = config.pageName.includes('Requests_for_adminship') ? 'adminship' : 'bureaucratship';
config.baseRfxPage = `Wikipedia:Requests_for_${config.rfxType}`;
// Ensure the script only runs on valid RfX subpages (e.g., WP:RfA/Candidate, not WP:RfA)
if (!new RegExp(`^${config.baseRfxPage.replace(/ /g, '_')}/[^/]+$`).test(config.pageName)) {
return; // Exit if not on a valid RfX subpage
}
// --- State Variables ---
let basePageWikitextCache = {};
// --- Utility Functions ---
/** Escapes characters for use in regex, handling spaces/underscores. */
function escapeRegex(string) {
const spacedString = string.replace(/_/g, ' ');
const underscoredString = string.replace(/ /g, '_');
if (spacedString === underscoredString) {
return spacedString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const escapedSpaced = spacedString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedUnderscored = underscoredString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return `(?:${escapedUnderscored}|${escapedSpaced})`;
}
/** Generic API request helper. */
async function makeApiRequest(params, method = 'get', tokenType = null) {
const api = new mw.Api();
const fullParams = { ...config.apiDefaults, ...params };
try {
let response;
if (method === 'get') {
response = await api.get(fullParams);
} else if (method === 'post' && tokenType) {
response = await api.postWithToken(tokenType, fullParams);
} else if (method === 'post') {
response = await api.post(fullParams);
} else {
throw new Error(`Unsupported API method: ${method}`);
}
return response;
} catch (error) {
const errorCode = error?.error?.code || error?.textStatus || 'unknown';
const errorInfo = error?.error?.info || error?.xhr?.responseText || 'Unknown API error';
console.error(`RfA Starter API [${method.toUpperCase()} ${params.action}] Error:`, { params, errorCode, errorInfo, errorObj: error });
throw { code: errorCode, info: errorInfo, params: params };
}
}
/** Fetches the full wikitext of a given page, using cache. */
async function fetchPageWikitext(targetPageName) {
const normalizedPageName = targetPageName.replace(/ /g, '_');
if (basePageWikitextCache[normalizedPageName]) {
return basePageWikitextCache[normalizedPageName];
}
try {
const data = await makeApiRequest({
action: 'query',
prop: 'revisions',
titles: normalizedPageName,
rvslots: 'main',
rvprop: 'content'
});
const page = data.query.pages[0];
if (page && page.missing) {
basePageWikitextCache[normalizedPageName] = '';
return '';
}
if (page && page.revisions?.[0]?.slots?.main?.content) {
const wikitext = page.revisions[0].slots.main.content;
basePageWikitextCache[normalizedPageName] = wikitext;
return wikitext;
} else {
throw new Error(`Could not find wikitext content for page ${normalizedPageName}. Response: ${JSON.stringify(data)}`);
}
} catch (error) {
console.error(`RfA Starter: Error fetching wikitext for ${normalizedPageName}:`, error);
return null;
}
}
/** Posts an error message to Barkeep49's talk page. */
async function reportErrorToBarkeep49(errorType, errorDetails) {
try {
const talkPage = 'User talk:Barkeep49';
const sectionTitle = 'RfA Starter error report';
const timestamp = new Date().toISOString();
const messageContent = `'''Error Type:''' ${errorType}\n\n` +
`'''Page:''' [[${config.pageName}]]\n\n` +
`'''RfX Type:''' ${config.rfxType}\n\n` +
`'''User:''' [[User:${config.userName}]]\n\n` +
`'''Timestamp:''' ${timestamp}\n\n` +
`'''Error Details:'''\n${errorDetails}\n\n` +
`~~${'~'.repeat(4)}~~`;
const summary = `Reporting RfA Starter error${config.tagLine}`;
await makeApiRequest({
action: 'edit',
title: talkPage,
section: 'new',
sectiontitle: sectionTitle,
text: messageContent,
summary: summary
}, 'post', 'edit');
return { success: true };
} catch (error) {
console.error('RfA Starter: Failed to report error to Barkeep49:', error);
return { success: false, error: error.info || error.message };
}
}
// --- Core Functions ---
/** Removes the HTML comment that prevents template substitution. */
async function removeSubstitutionComment() {
try {
const wikitext = await fetchPageWikitext(config.pageName);
if (!wikitext) {
const errorMsg = 'Could not fetch page wikitext';
return { success: false, error: errorMsg, needsReporting: true };
}
let modifiedWikitext = wikitext;
let foundComment = false;
if (config.rfxType === 'adminship') {
// For RfA: Remove <!-- To substitute your RfA, remove this line (the HTML comment markup here inactivates the safesubst code)-->
const rfaCommentPattern = /<!--\s*To substitute your RfA, remove this line \(the HTML comment markup here inactivates the safesubst code\)\s*-->\s*\n?/g;
if (rfaCommentPattern.test(wikitext)) {
foundComment = true;
modifiedWikitext = wikitext.replace(rfaCommentPattern, '');
}
} else {
// For RfB: Remove comment around <!--subst:-->RfA/time... to make it subst:RfA/time...
// Pattern: <!--subst:--> followed by RfA/time
const rfbCommentPattern = /<!--subst:-->/g;
if (rfbCommentPattern.test(wikitext)) {
foundComment = true;
modifiedWikitext = wikitext.replace(rfbCommentPattern, 'subst:');
}
}
// Check if we found the comment pattern
if (!foundComment && modifiedWikitext === wikitext) {
const errorMsg = `Could not find substitution-blocking comment on ${config.pageName}. ` +
`Expected pattern for ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'} not found.`;
return { success: false, error: errorMsg, needsReporting: true };
}
// Only save if there was a change
if (modifiedWikitext !== wikitext) {
await makeApiRequest({
action: 'edit',
title: config.pageName,
text: modifiedWikitext,
summary: `Removing substitution-blocking comment${config.tagLine}`
}, 'post', 'edit');
return { success: true };
} else {
return { success: true, message: 'No substitution comment found (may already be removed)' };
}
} catch (error) {
const errorMsg = `${error.info || error.message} (${error.code || 'unknown'})`;
return { success: false, error: errorMsg, needsReporting: true };
}
}
/** Adds transclusion to the main RfA/RfB page. */
async function transcludeToMainPage() {
try {
const mainPageWikitext = await fetchPageWikitext(config.baseRfxPage);
if (!mainPageWikitext) {
const errorMsg = `Could not fetch main ${config.rfxType} page wikitext: ${config.baseRfxPage}`;
return { success: false, error: errorMsg, needsReporting: true };
}
// Check if already transcluded
const pageNameForRegex = escapeRegex(config.pageName);
const transclusionPattern = new RegExp(`\\{\\{${pageNameForRegex}\\}\\}`, 'i');
if (transclusionPattern.test(mainPageWikitext)) {
return { success: false, error: 'Page is already transcluded', needsReporting: false };
}
// Find the insertion point
let insertionIndex = -1;
const transclusionLine = `{{${config.pageName}}}\n----\n`;
let expectedMarker = '';
if (config.rfxType === 'adminship') {
// For RfA: Insert below <!--Please leave this horizontal rule and place RfA transclusion below-->
const rfaMarker = /<!--Please leave this horizontal rule and place RfA transclusion below-->/;
expectedMarker = '<!--Please leave this horizontal rule and place RfA transclusion below-->';
const match = mainPageWikitext.match(rfaMarker);
if (match) {
insertionIndex = match.index + match[0].length;
}
} else {
// For RfB: Insert below <!-- Please leave this horizontal rule -->
const rfbMarker = /<!--\s*Please leave this horizontal rule\s*-->/;
expectedMarker = '<!-- Please leave this horizontal rule -->';
const match = mainPageWikitext.match(rfbMarker);
if (match) {
insertionIndex = match.index + match[0].length;
}
}
if (insertionIndex === -1) {
const errorMsg = `Could not find insertion marker on ${config.baseRfxPage}. ` +
`Expected marker: "${expectedMarker}"`;
return { success: false, error: errorMsg, needsReporting: true };
}
// Insert the transclusion
const modifiedWikitext = mainPageWikitext.substring(0, insertionIndex) +
'\n' + transclusionLine +
mainPageWikitext.substring(insertionIndex);
await makeApiRequest({
action: 'edit',
title: config.baseRfxPage,
text: modifiedWikitext,
summary: `Adding transclusion for ${config.pageName}${config.tagLine}`
}, 'post', 'edit');
return { success: true };
} catch (error) {
const errorMsg = `${error.info || error.message} (${error.code || 'unknown'})`;
return { success: false, error: errorMsg, needsReporting: true };
}
}
// --- Event Handler ---
/** Handles the submit button click. */
async function handleSubmitClick(e) {
e.preventDefault();
const button = e.target;
const originalText = button.textContent;
// Show confirmation dialog
const confirmMessage = `This will:\n\n` +
`1. Remove the substitution-blocking comment from ${config.pageName}\n` +
`2. Add the transclusion {{${config.pageName}}} to ${config.baseRfxPage}\n\n` +
`Do you want to proceed?`;
if (!confirm(confirmMessage)) {
return; // User cancelled
}
button.disabled = true;
button.textContent = 'Processing...';
// Step 1: Remove substitution comment
button.textContent = 'Removing comment...';
const commentResult = await removeSubstitutionComment();
if (!commentResult.success) {
const errorMsg = `Error removing comment: ${commentResult.error}`;
button.textContent = errorMsg;
button.style.color = 'red';
button.disabled = false;
// Report error to Barkeep49 if needed
if (commentResult.needsReporting) {
button.textContent = errorMsg + ' (Reporting error...)';
await reportErrorToBarkeep49(
'Failed to remove substitution comment',
`Error: ${commentResult.error}\n\nPage: ${config.pageName}\nRfX Type: ${config.rfxType}`
);
button.textContent = errorMsg + ' (Error reported)';
}
return;
}
// Step 2: Add transclusion
button.textContent = 'Adding transclusion...';
const transcludeResult = await transcludeToMainPage();
if (!transcludeResult.success) {
const errorMsg = `Error adding transclusion: ${transcludeResult.error}`;
button.textContent = errorMsg;
button.style.color = 'red';
button.disabled = false;
// Report error to Barkeep49 if needed
if (transcludeResult.needsReporting) {
button.textContent = errorMsg + ' (Reporting error...)';
await reportErrorToBarkeep49(
'Failed to add transclusion',
`Error: ${transcludeResult.error}\n\nPage: ${config.pageName}\nMain Page: ${config.baseRfxPage}\nRfX Type: ${config.rfxType}`
);
button.textContent = errorMsg + ' (Error reported)';
}
return;
}
// Success
button.textContent = 'Success!';
button.style.color = 'green';
setTimeout(() => {
button.textContent = originalText;
button.style.color = '';
button.disabled = false;
}, 3000);
}
// --- Initialization ---
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function() {
// Create and add submit button
const buttonText = config.rfxType === 'adminship' ? 'Submit RfA' : 'Submit RfB';
const submitButton = document.createElement('a');
submitButton.id = 'rfa-starter-submit';
submitButton.textContent = buttonText;
submitButton.href = '#';
submitButton.style.color = '#0645ad';
submitButton.style.textDecoration = 'none';
submitButton.style.cursor = 'pointer';
submitButton.addEventListener('click', handleSubmitClick);
submitButton.addEventListener('mouseenter', function() {
this.style.textDecoration = 'underline';
});
submitButton.addEventListener('mouseleave', function() {
this.style.textDecoration = 'none';
});
const pageTools = document.querySelector('#p-tb ul');
if (pageTools) {
const li = document.createElement('li');
li.id = 'rfa-starter-launch-li';
li.appendChild(submitButton);
pageTools.appendChild(li);
}
});
console.log("RfA Starter: Script loaded.");
})();