User:DreamRimmer/massMessageLite.js
Appearance
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 user script seems to have a documentation page at User:DreamRimmer/massMessageLite. |
//WARNING: You take full responsibility for any actions taken using this script.
//You must read and understand all relevant Wikipedia policies and abide by them
//when using this tool; failure to do so may result in being blocked from editing.
//<nowiki>
$(document).ready(function() {
var isPaused = false, pauseResumeButton, minorEditCheckbox, createIfMissingCheckbox, talkMessageCheckInterval, lastTalkRevId = null, operatorPausedByNewMessage = false;
function initializeMassMessage() {
$('#mw-content-text > p').remove();
$('#firstHeading').text('massMessageLite');
$('#mw-content-text').empty()
.append(
$('<p>').html('<span style="font-weight: bold; color: red;">Warning:</span> <span style="font-weight: bold; color: black;">You take full responsibility for any actions taken using this script. You must read and understand all relevant <a href="/wiki/Wikipedia:Policies_and_guidelines" target="_blank">Wikipedia policies</a> and abide by them when using this tool; failure to do so may result in being <a href="/wiki/Wikipedia:Blocking_policy" target="_blank">blocked from editing</a>.</span>'),
$('<div>').css({'margin-bottom': '20px', 'font-size': '1.1em'}).html(
'Use the form below to send messages to a specified list. All fields are required. See <a href="/wiki/User:DreamRimmer/massMessageLite" target="_blank">User:DreamRimmer/massMessageLite</a> for more details.'
)
);
var frameContainer = $('<div>')
.addClass('mw-htmlform-ooui-wrapper oo-ui-layout oo-ui-panelLayout oo-ui-panelLayout-padded oo-ui-panelLayout-framed')
.css({'max-width': '950px', 'margin': '0 auto 2em auto', 'padding': '1em 1.5em'});
var deliveredPages = [], skippedPages = [], deliveredDiffs = {}, linksContainer, deliveredBox, skippedBox;
var pagesTextarea = new OO.ui.MultilineTextInputWidget({
placeholder: 'User talk:Example user\nUser talk:Example user2\nWikipedia talk:Example',
autosize: true,
rows: 10
}),
subjectInputField = new OO.ui.TextInputWidget({
placeholder: 'Subject of the message'
}),
messageTextarea = new OO.ui.MultilineTextInputWidget({
placeholder: 'Body of the message',
autosize: true,
rows: 10
}),
summaryInputField = new OO.ui.TextInputWidget({
placeholder: 'Edit summary'
}),
visualPreviewButton = new OO.ui.ButtonWidget({
label: 'Preview',
flags: ['primary']
}),
startButton = new OO.ui.ButtonWidget({
label: 'Send Message',
icon: 'alert',
flags: ['primary', 'progressive'],
disabled: true
}),
cancelButton = new OO.ui.ButtonWidget({
label: 'Cancel',
flags: ['primary', 'destructive'],
href: 'https:' + mw.config.get('wgServer')
}),
skipDuplicateCheckbox = new OO.ui.CheckboxInputWidget({
selected: false
}),
minorEditCheckbox = new OO.ui.CheckboxInputWidget({
selected: false
}),
createIfMissingCheckbox = new OO.ui.CheckboxInputWidget({
selected: false
}),
pauseResumeButton = new OO.ui.ButtonWidget({
label: 'Pause',
icon: new OO.ui.IconWidget({icon: 'pause', flags: ['progressive']}),
flags: ['progressive'],
disabled: true
}),
logContainer = $("<ul>").css({
'padding': '10px',
'margin-top': '10px',
'border': 'none',
'display': 'table-row',
'width': 'auto',
'font-size': 'small',
'transition': 'background 0.2s',
'overflow-y': 'auto',
'max-height': '260px',
'min-height': '80px'
}).addClass('mml-log-container').hide(),
previewContainer = $('<div>').css({
'padding': '10px',
'margin': '10px',
'overflow': 'auto',
'min-height': '200px',
'max-height': '400px',
'border': '1px solid #ccc',
'width': '65%',
'display': 'table-row'
}).hide();
$('<style>').prop('type', 'text/css').html(`
.mml-log-entry {
border-radius: 8px;
margin-bottom: 5px;
padding: 5px 7px 5px 3px;
transition: background 0.18s, box-shadow 0.18s;
background: #fff;
opacity: 0;
animation: mml-fadein 0.45s forwards;
}
.mml-log-entry:hover {
background: #e7f1ff;
box-shadow: 0 1px 7px rgba(120,160,255,0.07);
}
@keyframes mml-fadein {
from { opacity: 0; transform: translateY(10px);}
to { opacity: 1; transform: none;}
}
.mml-log-link {
color: #2a4dad !important;
text-decoration: none;
font-weight: 600;
cursor: pointer;
}
.mml-diff-link {
color: #0b8200 !important;
margin-left: 0.8em;
font-size: 90%;
vertical-align: middle;
text-decoration: underline;
font-weight: 500;
}
.mml-log-container {
scrollbar-width: thin;
scrollbar-color: #b8c6e5 #f8fafc;
}
.mml-log-container::-webkit-scrollbar {
width: 8px;
background: #f8fafc;
opacity: 0;
transition: opacity 0.3s;
}
.mml-log-container:hover::-webkit-scrollbar {
opacity: 1;
}
.mml-log-container::-webkit-scrollbar-thumb {
background: #b8c6e5;
border-radius: 6px;
}
`).appendTo(document.head);
linksContainer = $('<div>').css({'margin':'10px 0 0 0','padding':'5px 0 0 0','display':'none'});
deliveredBox = $('<div>').css({'display':'none','margin':'6px 0'}).append(
$('<textarea readonly style="width:700px;min-height:120px;max-height:400px;resize:vertical;font-family:monospace;font-size:13px;box-sizing:border-box;"></textarea>')
);
skippedBox = $('<div>').css({'display':'none','margin':'6px 0'}).append(
$('<textarea readonly style="width:700px;min-height:120px;max-height:400px;resize:vertical;font-family:monospace;font-size:13px;box-sizing:border-box;"></textarea>')
);
linksContainer.append(
$('<a href="#" style="margin-right:15px;font-weight:bold;" id="show-delivered-list">Show delivered pages</a>'),
$('<a href="#" style="font-weight:bold;" id="show-skipped-list">Show skipped pages</a>'),
deliveredBox,
skippedBox
);
var pagesCountInfo = $('<div id="pages-count-info" style="margin-bottom:10px;margin-top:10px;padding:5px 0 5px 0;font-weight:bold;color:blue;display:none;"></div>');
frameContainer.append(
$('<p>').text('Enter list of pages (one per line):').css('font-weight', 'bold'),
pagesCountInfo,
pagesTextarea.$element.css({'margin-bottom': '15px'}),
$('<p>').text('Subject:').css('font-weight', 'bold'),
subjectInputField.$element,
$('<p>').text('Message:').css('font-weight', 'bold'),
messageTextarea.$element,
$('<p>').text('Edit summary:').css('font-weight', 'bold'),
summaryInputField.$element,
$('<div>').css('padding', '10px'),
$('<label>').append(skipDuplicateCheckbox.$element, ' Skip pages with the same section name already present'),
$('<div>').css('padding', '10px'),
$('<label>').append(minorEditCheckbox.$element, ' Mark edits as minor'),
$('<div>').css('padding', '10px'),
$('<label>').append(createIfMissingCheckbox.$element, ' Create page if it does not exist'),
$('<div>').css('padding', '10px'),
$('<div>').append(visualPreviewButton.$element, startButton.$element, pauseResumeButton.$element, cancelButton.$element),
previewContainer,
'<br/>',
logContainer,
linksContainer
);
$('#mw-content-text').append(frameContainer);
function updatePagesCount() {
var lines = pagesTextarea.getValue().split("\n"), count = 0;
lines.forEach(function(page){ if(page.trim()) count++; });
if (count > 0) {
pagesCountInfo.text('You are about to send a message to ' + count + ' page(s).').show();
} else {
pagesCountInfo.hide();
}
}
pagesTextarea.on('change', updatePagesCount);
updatePagesCount();
function makeWikiLink(title) {
var url = mw.util.getUrl(title.replace(/ /g, '_'));
return $('<a>')
.addClass('mml-log-link')
.attr({href: url, target: '_blank', rel: 'noopener'})
.text(title);
}
function makeDiffLink(revid, label) {
var url = mw.util.getUrl('Special:Diff/' + revid);
return $('<a>')
.addClass('mml-diff-link')
.attr({href: url, target: '_blank', rel: 'noopener'})
.text(label || '[diff]');
}
function addLogEntry(icon, message, color, pageTitle, revidForDiff, customHtml) {
var logEntry = $('<li>').addClass('mml-log-entry').css({ 'color': color });
logEntry.append(new OO.ui.IconWidget({ icon: icon, flags: ['progressive'] }).$element.css({ 'margin-right': '5px', 'vertical-align': 'middle' }));
if (customHtml) {
logEntry.append($('<span>').html(customHtml));
} else if (pageTitle) {
var pageLink = makeWikiLink(pageTitle)[0].outerHTML;
var msgHtml = message.replace(
new RegExp(pageTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
pageLink
);
logEntry.append($('<span>').html(msgHtml));
if (revidForDiff) {
logEntry.append(makeDiffLink(revidForDiff));
}
} else {
logEntry.append($('<span>').text(message));
}
logContainer.append(logEntry).show();
setTimeout(function() {
logContainer.scrollTop(logContainer[0].scrollHeight);
}, 5);
if (typeof message === 'string' && message.match(/^Sleeping for \d+ seconds to avoid flooding recent changes/i)) {
setTimeout(function() {
logEntry.slideUp(600, function() { $(this).remove(); });
}, 5000);
}
}
function previewMessage() {
var subject = subjectInputField.getValue().trim(),
message = messageTextarea.getValue().trim();
if (!subject || !message) {
previewContainer.empty()
.append(
$('<h2>').text("Message preview:"),
$('<div>').css({
'padding': '20px',
'margin': '25px 0',
'font-family': 'inherit',
'font-size': '13px',
'background': '#fff',
'border': '1px solid #ccc',
}).text('Please fill required fields (subject and message) to preview.')
)
.show();
startButton.setDisabled(true);
return;
}
var wikitext = '== ' + subject + ' ==\n' + message;
var previewBox = $('<div>').css({
'padding': '20px',
'margin': '25px 0',
'overflow': 'auto',
'width': pagesTextarea.$element.width(),
'min-height': '350px',
'max-height': '500px',
'font-family': 'inherit',
'font-size': '13px',
'box-sizing': 'border-box',
'border': '1px solid #ccc',
'background': '#fff'
});
previewContainer.empty()
.append($('<h2>').text("Message preview:"))
.append(previewBox)
.show();
new mw.Api().post({
action: 'parse',
text: wikitext,
title: 'Preview',
contentmodel: 'wikitext',
pst: true,
format: 'json'
}).done(function(data) {
previewBox.html(data.parse.text['*']);
$('html, body').animate({
scrollTop: previewContainer.offset().top
}, 500);
}).fail(function() {
previewBox.html('<p>Error loading preview</p>');
});
startButton.setDisabled(false);
}
function setPauseResumeState(paused) {
if(paused) {
pauseResumeButton.setLabel('Resume')
.setIcon(new OO.ui.IconWidget({icon: 'play', flags: ['progressive']}))
.setFlags(['progressive']);
} else {
pauseResumeButton.setLabel('Pause')
.setIcon(new OO.ui.IconWidget({icon: 'pause', flags: ['progressive']}))
.setFlags(['progressive']);
}
}
pauseResumeButton.on('click', function() {
isPaused = !isPaused;
setPauseResumeState(isPaused);
if (!isPaused && !operatorPausedByNewMessage && typeof window.currentProcessNextPage === "function") window.currentProcessNextPage();
});
function resolveRedirect(pageTitle) {
return new Promise(function(resolve, reject) {
new mw.Api().get({
action: 'query',
titles: pageTitle,
redirects: 1
}).done(function(data) {
if (data.query && data.query.redirects && data.query.redirects.length > 0) {
resolve(data.query.redirects[0].to);
} else {
var pageId = Object.keys(data.query.pages)[0];
if (pageId === '-1') return resolve(pageTitle);
resolve(data.query.pages[pageId].title);
}
}).fail(function() {
resolve(pageTitle);
});
});
}
function getLastTalkPageRevId(callback) {
var userTalkPage = 'User talk:' + mw.config.get('wgUserName');
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: userTalkPage,
rvprop: 'ids|timestamp',
rvlimit: 1
}).done(function(data) {
var pageId = Object.keys(data.query.pages)[0];
if (pageId === '-1') return callback(null);
var rev = data.query.pages[pageId].revisions[0];
callback(rev.revid);
});
}
function checkTalkPageAndPauseIfNeeded(callback) {
var userTalkPage = 'User talk:' + mw.config.get('wgUserName');
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: userTalkPage,
rvprop: 'ids|timestamp',
rvlimit: 1
}).done(function(data) {
var pageId = Object.keys(data.query.pages)[0];
if (pageId === '-1') return callback(false);
var rev = data.query.pages[pageId].revisions[0];
if (lastTalkRevId === null) {
lastTalkRevId = rev.revid;
callback(false);
} else if (rev.revid !== lastTalkRevId) {
addLogEntry('alert', 'You have a new message on your talk page! Pausing delivery.', 'orange');
operatorPausedByNewMessage = true;
isPaused = true;
setPauseResumeState(true);
lastTalkRevId = rev.revid;
window.setTimeout(function() {
window.alert('There is a new message on your talk page. Please check it before continuing.');
}, 100);
callback(true);
} else {
callback(false);
}
});
}
function removePageFromTextarea(page) {
var lines = pagesTextarea.getValue().split("\n");
var filtered = lines.filter(function(line) { return line.trim() !== page; });
pagesTextarea.setValue(filtered.join("\n"));
updatePagesCount();
}
// Expand/collapse logic
linksContainer.on('click', '#show-delivered-list', function(e){
e.preventDefault();
if(deliveredBox.is(':visible')) {
deliveredBox.slideUp();
} else {
deliveredBox.find('textarea').val(deliveredPages.join('\n'));
deliveredBox.slideDown();
skippedBox.slideUp();
}
});
linksContainer.on('click', '#show-skipped-list', function(e){
e.preventDefault();
if(skippedBox.is(':visible')) {
skippedBox.slideUp();
} else {
skippedBox.find('textarea').val(skippedPages.join('\n'));
skippedBox.slideDown();
deliveredBox.slideUp();
}
});
function sendMessage() {
var pages = pagesTextarea.getValue().split("\n").map(function(page){return page.trim();}).filter(function(page){return page;}),
subject = subjectInputField.getValue().trim(),
message = messageTextarea.getValue().trim(),
summary = summaryInputField.getValue().trim() + " (using [[User:DreamRimmer/massMessageLite|massMessageLite]])",
wikitext = '== ' + subject + ' ==\n' + message,
skipDuplicate = skipDuplicateCheckbox.isSelected(),
minorEdit = minorEditCheckbox.isSelected(),
createIfMissing = createIfMissingCheckbox.isSelected(),
userGroups = mw.config.get('wgUserGroups'),
isSysop = userGroups.includes('sysop'),
maxMessagesPerMinute = isSysop ? 25 : 15,
sentMessages = 0,
startTime = new Date().getTime();
deliveredPages = [];
skippedPages = [];
deliveredDiffs = {};
linksContainer.hide();
deliveredBox.hide();
skippedBox.hide();
if (pages.length === 0 || subject === "" || message === "" || summary === "") {
alert("Error: Please fill in all fields");
return;
}
logContainer.empty().append($('<h2>').text("Delivery logs:")).css('margin-top', '5px');
logContainer.show();
var currentIndex = 0, processedPages = 0;
window.currentProcessNextPage = processNextPage;
function processNextPage() {
if (isPaused || operatorPausedByNewMessage) return;
if (currentIndex >= pages.length) {
pauseResumeButton.setDisabled(true);
showLinksIfAny();
return;
}
pauseResumeButton.setDisabled(false);
var page = pages[currentIndex];
resolveRedirect(page).then(function(resolvedPage) {
if (resolvedPage !== page) {
addLogEntry(
'articleRedirect',
'',
'blue',
null,
null,
makeWikiLink(page)[0].outerHTML + ' is a redirect. Using ' + makeWikiLink(resolvedPage)[0].outerHTML + '.'
);
}
page = resolvedPage;
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: page,
rvprop: 'content',
rvslots: '*'
}).done(function(data) {
var pageId = Object.keys(data.query.pages)[0];
if (pageId === '-1') {
if (createIfMissing) {
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: page,
text: wikitext,
summary: summary,
minor: minorEdit ? true : undefined
}).done(function(editdata) {
var revid = editdata && editdata.edit && editdata.edit.newrevid ? editdata.edit.newrevid : null;
addLogEntry('message', 'Page created and messaged ' + page + '.', 'green', page, revid);
deliveredPages.push(page);
if (revid) deliveredDiffs[page] = revid;
removePageFromTextarea(pages[currentIndex]);
sentMessages++;
currentIndex++;
processedPages++;
afterPageProcessed();
}).fail(function() {
addLogEntry('alert', 'Failed to create ' + page + '.', 'red', page);
skippedPages.push(page);
removePageFromTextarea(pages[currentIndex]);
currentIndex++;
processedPages++;
afterPageProcessed();
});
} else {
addLogEntry('alert', 'Page ' + page + ' does not exist.', 'red', page);
skippedPages.push(page);
removePageFromTextarea(pages[currentIndex]);
currentIndex++;
processedPages++;
afterPageProcessed();
}
return;
}
var content = data.query.pages[pageId].revisions[0].slots.main['*'];
if (content.includes("{{User:DreamRimmer/NoMassMessage}}")) {
addLogEntry('alert', page + ' was skipped; (opted-out of message delivery)', 'red', page);
skippedPages.push(page);
removePageFromTextarea(pages[currentIndex]);
currentIndex++;
processedPages++;
afterPageProcessed();
return;
}
if (skipDuplicate && content.includes('== ' + subject + ' ==')) {
addLogEntry('alert', 'Message already exists on ' + page + '.', 'red', page);
skippedPages.push(page);
removePageFromTextarea(pages[currentIndex]);
currentIndex++;
processedPages++;
afterPageProcessed();
return;
}
var newContent = content + "\n\n" + wikitext;
new mw.Api().postWithToken('csrf', {
action: 'edit',
title: page,
text: newContent,
summary: summary,
minor: minorEdit ? true : undefined
}).done(function(editdata) {
var revid = editdata && editdata.edit && editdata.edit.newrevid ? editdata.edit.newrevid : null;
addLogEntry('message', 'Message sent to ' + page + '.', 'green', page, revid);
deliveredPages.push(page);
if (revid) deliveredDiffs[page] = revid;
removePageFromTextarea(pages[currentIndex]);
sentMessages++;
currentIndex++;
processedPages++;
afterPageProcessed();
}).fail(function() {
addLogEntry('alert', 'Failed to send message to ' + page + '.', 'red', page);
skippedPages.push(page);
removePageFromTextarea(pages[currentIndex]);
currentIndex++;
processedPages++;
afterPageProcessed();
});
}).fail(function() {
addLogEntry('alert', 'Failed to retrieve content of ' + page + '.', 'red', page);
skippedPages.push(page);
removePageFromTextarea(pages[currentIndex]);
currentIndex++;
processedPages++;
afterPageProcessed();
});
});
}
function afterPageProcessed() {
if (processedPages % 5 === 0 && processedPages > 0) {
checkTalkPageAndPauseIfNeeded(function(paused){
if (!paused) checkRateLimitAndContinue();
});
} else {
checkRateLimitAndContinue();
}
}
function checkRateLimitAndContinue() {
var currentTime = new Date().getTime(),
elapsedTime = (currentTime - startTime) / 1000,
maxMessages = maxMessagesPerMinute,
sleepTime = (60 / maxMessages) * sentMessages - elapsedTime;
if (sleepTime > 0) {
addLogEntry('clock', 'Sleeping for ' + Math.ceil(sleepTime) + ' seconds to avoid flooding recent changes...', 'blue');
setTimeout(processNextPage, sleepTime * 1000);
} else {
processNextPage();
}
}
function showLinksIfAny() {
if (deliveredPages.length > 0 || skippedPages.length > 0) {
linksContainer.show();
deliveredBox.hide();
skippedBox.hide();
}
}
getLastTalkPageRevId(function(revId){
lastTalkRevId = revId;
processNextPage();
});
$('html, body').animate({
scrollTop: logContainer.offset().top
}, 500);
}
visualPreviewButton.on('click', previewMessage);
startButton.on('click', function() {
isPaused = false;
operatorPausedByNewMessage = false;
setPauseResumeState(false);
pauseResumeButton.setDisabled(false);
sendMessage();
});
}
function checkUserAccess() {
var username = mw.config.get('wgUserName');
new mw.Api().get({
action: 'query',
titles: 'User:DreamRimmer/massmessage.json',
prop: 'revisions',
rvprop: 'content'
}).done(function(data) {
var pageId = Object.keys(data.query.pages)[0];
if (pageId === '-1') {
alert("Error: Cannot retrieve access control list");
window.location.href = mw.config.get('wgServer');
return;
}
var content = data.query.pages[pageId].revisions[0]['*'],
accessControl = JSON.parse(content);
if (accessControl.blockedUsers.includes(username)) {
alert("You are blocked from using this script. Please contact User:DreamRimmer for more details.");
window.location.href = mw.config.get('wgServer');
return;
}
if (accessControl.allowedUsers.includes(username)) {
initializeMassMessage();
} else {
var userGroups = mw.config.get('wgUserGroups');
if (userGroups.includes('extendedconfirmed') || userGroups.includes('sysop')) {
initializeMassMessage();
} else {
alert("You do not have permission to use this script. Please contact User:DreamRimmer.");
window.location.href = mw.config.get('wgServer');
}
}
}).fail(function() {
alert("Error: Cannot retrieve access control list. Please contact User:DreamRimmer");
window.location.href = mw.config.get('wgServer');
});
}
$.when(mw.loader.using('mediawiki.util'), $.ready).then(function() {
mw.util.addPortletLink(
'p-tb',
mw.util.getUrl('Special:BlankPage/massMessageLite'),
'massMessageLite'
);
});
if (mw.config.get('wgCanonicalSpecialPageName') === 'Blankpage' && mw.config.get('wgTitle').split('/', 2)[1] === 'massMessageLite') {
$.when(mw.loader.using('oojs-ui-core'), $.ready).then(function() {
checkUserAccess();
});
}
});
//</nowiki>