User:WikiDwarf/AntiVandal.js: Difference between revisions

Fix
No edit summary
 
(14 intermediate revisions by the same user not shown)
Line 1: Line 1:
/**
/**
  * AntiVandal Bharatpedia patrol helper
  * AntiVandal - Bharatpedia patrol helper
*
* Original userscript for en.bharatpedia.org.
* Inspired by the general idea of icon-driven revert + notice tools,
* written from scratch for this wiki's API, templates, and noticeboards.
*
  * Host: User:WikiDwarf/AntiVandal.js
  * Host: User:WikiDwarf/AntiVandal.js
  * Install (Special:MyPage/common.js):
  * Version 1.1.1
*  mw.loader.load('/w/index.php?title=User:WikiDwarf/AntiVandal.js&action=raw&ctype=text/javascript');
*
* You are responsible for every action this script performs on your account.
  */
  */
(function () {
(function () {
'use strict';
'use strict';
if (window.AntiVandal && window.AntiVandal.loaded) {
if (window.AntiVandal && window.AntiVandal.loaded) {
return;
return;
}
}
var RK = (window.AntiVandal = window.AntiVandal || {});
var RK = (window.AntiVandal = window.AntiVandal || {});
RK.loaded = true;
RK.loaded = true;
RK.version = '1.1.0';
RK.version = '1.1.2';
 
var CONF = {
var CONF = {
storageKey: 'antivandal-prefs-v1',
storageKey: 'antivandal-prefs-v1',
Line 29: Line 18:
aivPage: 'Bharatpedia:Administrator intervention against vandalism',
aivPage: 'Bharatpedia:Administrator intervention against vandalism',
rfppPage: 'Bharatpedia:Requests for page protection',
rfppPage: 'Bharatpedia:Requests for page protection',
anPage: "Bharatpedia:Administrators noticeboard",
anPage: 'Bharatpedia:Administrators noticeboard',
privacyPage: 'Bharatpedia:Requests for oversight',
privacyPage: 'Bharatpedia:Requests for oversight',
docsPage: 'Bharatpedia:AntiVandal'
docsPage: 'Bharatpedia:AntiVandal',
welcomeCat: 'Category:Welcome templates'
};
};
var PRESETS = [
var PRESETS = [
{ id: 'vandalism', label: 'Vandalism', summary: 'Reverted unconstructive edit(s)', notice: 'uw-vandalism', level: true },
{ id: 'vandalism', label: 'Vandalism', summary: 'Reverted unconstructive edit(s)', notice: 'uw-vandalism', level: true },
Line 43: Line 32:
{ id: 'custom', label: 'Custom summary', summary: '', notice: '', level: false }
{ id: 'custom', label: 'Custom summary', summary: '', notice: '', level: false }
];
];
var NOTICES = [
var NOTICES = [
{ id: 'uw-vandalism', label: 'Vandalism', leveled: true },
{ id: 'uw-vandalism', label: 'Vandalism', leveled: true },
Line 60: Line 48:
{ id: 'welcometest', label: 'Welcome after a test edit', leveled: false }
{ id: 'welcometest', label: 'Welcome after a test edit', leveled: false }
];
];
function defaultPrefs() {
function defaultPrefs() {
return {
return { barPosition: 'top', useRollback: true, autoNotice: false };
barPosition: 'top',
useRollback: true,
autoNotice: false
};
}
}
function loadPrefs() {
function loadPrefs() {
try {
try {
Line 77: Line 59:
}
}
}
}
function savePrefs(p) {
function savePrefs(p) {
try {
try { localStorage.setItem(CONF.storageKey, JSON.stringify(p)); } catch (e) {}
localStorage.setItem(CONF.storageKey, JSON.stringify(p));
} catch (e) { /* ignore quota */ }
}
}
var prefs = loadPrefs();
var prefs = loadPrefs();
var api = null;
var api = null;
var hasRollback = false;
var hasRollback = false;
function toast(msg, kind) {
function toast(msg, kind) {
var el = document.getElementById('antivandal-toast');
var el = document.getElementById('antivandal-toast');
Line 93: Line 70:
el = document.createElement('div');
el = document.createElement('div');
el.id = 'antivandal-toast';
el.id = 'antivandal-toast';
el.setAttribute('role', 'status');
document.body.appendChild(el);
document.body.appendChild(el);
}
}
Line 100: Line 76:
el.style.display = 'block';
el.style.display = 'block';
clearTimeout(toast._t);
clearTimeout(toast._t);
toast._t = setTimeout(function () {
toast._t = setTimeout(function () { el.style.display = 'none'; }, 4200);
el.style.display = 'none';
}, 4200);
}
}
function pageName() {
function pageName() {
return mw.config.get('wgPageName');
return mw.config.get('wgPageName');
}
}
function relevantUser() {
function relevantUser() {
return mw.config.get('wgRelevantUserName') || null;
return mw.config.get('wgRelevantUserName') || null;
}
}
function talkTitleFor(user) {
function talkTitleFor(user) {
if (!user) {
if (!user) { return null; }
return null;
return 'User talk:' + user;
}
function actingUser() {
if (mw.user && typeof mw.user.getName === 'function' && mw.user.getName()) {
return mw.user.getName();
}
}
if (/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d)$/.test(user) ||
return mw.config.get('wgUserName') || '';
user.indexOf(':') !== -1 && user.match(/^[0-9a-fA-F:.]+$/)) {
}
return 'User talk:' + user;
function wikiSig() {
}
return '~~' + '~~';
return 'User talk:' + user;
}
}
function injectCss() {
function injectCss() {
if (document.getElementById('antivandal-css')) {
if (document.getElementById('antivandal-css')) { return; }
return;
}
var css = [
var css = [
'#antivandal-open.bpneo-title-btn{color:#c62828;}',
'#antivandal-open.bpneo-title-btn{color:#c62828;}',
'#antivandal-open.bpneo-title-btn i{color:#c62828;}',
'#antivandal-tools{display:none;align-items:center;gap:inherit;}',
'#antivandal-tools{display:none;align-items:center;gap:inherit;}',
'.bpneo-title-actions.av-open > :not(#antivandal-tools){display:none !important;}',
'.bpneo-title-actions.av-open > :not(#antivandal-tools){display:none !important;}',
Line 136: Line 106:
'#antivandal-tools .bpneo-title-btn.rk-on{color:#2a4a33;}',
'#antivandal-tools .bpneo-title-btn.rk-on{color:#2a4a33;}',
'#antivandal-more{position:absolute;right:0;top:36px;background:#fff;border:1px solid #a2a9b1;',
'#antivandal-more{position:absolute;right:0;top:36px;background:#fff;border:1px solid #a2a9b1;',
'box-shadow:0 2px 8px rgba(0,0,0,.18);min-width:200px;z-index:50;display:none;padding:4px 0;}',
'box-shadow:0 2px 8px rgba(0,0,0,.18);min-width:220px;z-index:50;display:none;padding:4px 0;}',
'#antivandal-more button{display:block;width:100%;text-align:left;border:0;background:none;',
'#antivandal-more button{display:block;width:100%;text-align:left;border:0;background:none;',
'padding:8px 12px;cursor:pointer;font:13px sans-serif;}',
'padding:8px 12px;cursor:pointer;font:13px sans-serif;}',
Line 145: Line 115:
'padding:8px 12px;cursor:pointer;font:13px sans-serif;}',
'padding:8px 12px;cursor:pointer;font:13px sans-serif;}',
'#antivandal-ctx button:hover{background:#eaecf0;}',
'#antivandal-ctx button:hover{background:#eaecf0;}',
'.rk-modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1100;display:flex;',
'.rk-modal-bg{position:fixed;inset:0;background:rgba(17,17,17,.4);z-index:1100;display:flex;',
'align-items:flex-start;justify-content:center;padding:8vh 16px 16px;}',
'align-items:center;justify-content:center;padding:16px;}',
'.rk-modal{background:#fff;color:#1b1b1b;max-width:560px;width:100%;border-radius:8px;',
'.rk-modal{background:#fff;color:#1b1b1b;max-width:480px;width:100%;border-radius:10px;',
'box-shadow:0 12px 40px rgba(0,0,0,.28);overflow:hidden;}',
'border-top:3px solid #c62828;box-shadow:0 8px 28px rgba(0,0,0,.2);overflow:hidden;}',
'.rk-modal h2{margin:0;padding:12px 16px;background:#1f2a22;color:#f4efe4;font-size:16px;}',
'.rk-modal .rk-head{display:flex;align-items:center;justify-content:space-between;',
'.rk-modal .rk-body{padding:14px 16px;}',
'padding:10px 14px;border-bottom:1px solid #eee;}',
'.rk-modal label{display:block;font-weight:600;margin:10px 0 4px;}',
'.rk-modal h2{margin:0;padding:0;background:transparent;color:#111;font-size:15px;font-weight:600;}',
'.rk-modal .rk-x{border:0;background:none;font-size:18px;line-height:1;cursor:pointer;color:#444;}',
'.rk-modal .rk-body{padding:12px 14px;font-size:13px;}',
'.rk-modal label{display:block;font-weight:600;margin:8px 0 4px;}',
'.rk-modal .rk-checkrow{display:flex;flex-wrap:wrap;gap:10px 18px;margin-top:10px;align-items:center;}',
'.rk-modal .rk-check{display:inline-flex;align-items:center;gap:6px;font-weight:400;margin:0;}',
'.rk-modal .rk-check input{margin:0;}',
'.rk-modal input[type=text],.rk-modal select,.rk-modal textarea{width:100%;box-sizing:border-box;',
'.rk-modal input[type=text],.rk-modal select,.rk-modal textarea{width:100%;box-sizing:border-box;',
'padding:7px 8px;font:14px sans-serif;}',
'padding:7px 8px;font:13px sans-serif;}',
'.rk-modal textarea{min-height:90px;}',
'.rk-modal textarea{min-height:70px;}',
'.rk-modal .rk-actions{display:flex;justify-content:flex-end;gap:8px;padding:10px 16px;background:#f3f1ea;}',
'.rk-modal .rk-actions{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;}',
'.rk-modal .rk-actions button{padding:7px 14px;border-radius:4px;border:1px solid #bbb;cursor:pointer;}',
'.rk-modal .rk-actions button{padding:6px 12px;border-radius:4px;border:1px solid #bbb;cursor:pointer;background:#fff;}',
'.rk-modal .rk-actions .rk-primary{background:#1f2a22;color:#fff;border-color:#1f2a22;}',
'.rk-modal .rk-actions .rk-primary{background:#c62828;color:#fff;border-color:#c62828;}',
'.rk-toast{position:fixed;right:16px;bottom:16px;z-index:1200;background:#1f2a22;color:#fff;',
'.rk-toast{position:fixed;right:16px;bottom:16px;z-index:1200;background:#1f2a22;color:#fff;',
'padding:10px 14px;border-radius:6px;max-width:360px;display:none;}',
'padding:10px 14px;border-radius:6px;max-width:360px;display:none;}',
Line 163: Line 139:
'.rk-toast-ok{background:#2a4a33;}',
'.rk-toast-ok{background:#2a4a33;}',
'#av-patrol{position:fixed;top:0;right:0;width:320px;height:100vh;background:#111;color:#eee;',
'#av-patrol{position:fixed;top:0;right:0;width:320px;height:100vh;background:#111;color:#eee;',
'z-index:1250;overflow:auto;box-shadow:-4px 0 18px rgba(0,0,0,.3);font:13px sans-serif;}',
'z-index:1250;overflow:auto;font:13px sans-serif;}',
'#av-patrol h3{margin:0;padding:12px 14px;background:#1f2a22;font-size:15px;}',
'#av-patrol h3{margin:0;padding:12px 14px;background:#1f2a22;}',
'#av-patrol .av-p-item{display:block;padding:8px 12px;border-bottom:1px solid #333;color:#eee;text-decoration:none;}',
'#av-patrol .av-p-item{display:block;padding:8px 12px;border-bottom:1px solid #333;color:#eee;text-decoration:none;}',
'#av-patrol .av-p-item:hover{background:#2a3a2e;}',
'#av-patrol .av-neg{background:rgba(180,40,40,.25);}',
'#av-patrol .av-pos{background:rgba(40,140,70,.2);}',
'.av-contrib-rev{margin-left:6px;font-size:12px;cursor:pointer;color:#c62828;border:0;background:none;}'
'.av-contrib-rev{margin-left:6px;font-size:12px;cursor:pointer;color:#c62828;border:0;background:none;}'
].join('');
].join('');
Line 176: Line 149:
document.head.appendChild(style);
document.head.appendChild(style);
}
}
function closeModal() {
function closeModal() {
var bg = document.getElementById('antivandal-modal-bg');
var bg = document.getElementById('antivandal-modal-bg');
if (bg) {
if (bg) { bg.parentNode.removeChild(bg); }
bg.parentNode.removeChild(bg);
}
}
}
function openModal(title, bodyNode, actions) {
function openModal(title, bodyNode, actions) {
closeModal();
closeModal();
Line 189: Line 158:
bg.id = 'antivandal-modal-bg';
bg.id = 'antivandal-modal-bg';
bg.className = 'rk-modal-bg';
bg.className = 'rk-modal-bg';
bg.addEventListener('click', function (e) {
bg.addEventListener('click', function (e) { if (e.target === bg) { closeModal(); } });
if (e.target === bg) {
closeModal();
}
});
var box = document.createElement('div');
var box = document.createElement('div');
box.className = 'rk-modal';
box.className = 'rk-modal';
var head = document.createElement('div');
head.className = 'rk-head';
var h = document.createElement('h2');
var h = document.createElement('h2');
h.textContent = title;
h.textContent = title;
box.appendChild(h);
var x = document.createElement('button');
x.type = 'button';
x.className = 'rk-x';
x.setAttribute('aria-label', 'Close');
x.textContent = 'x';
x.addEventListener('click', closeModal);
head.appendChild(h);
head.appendChild(x);
box.appendChild(head);
var body = document.createElement('div');
var body = document.createElement('div');
body.className = 'rk-body';
body.className = 'rk-body';
Line 209: Line 184:
b.type = 'button';
b.type = 'button';
b.textContent = a.label;
b.textContent = a.label;
if (a.primary) {
if (a.primary) { b.className = 'rk-primary'; }
b.className = 'rk-primary';
}
b.addEventListener('click', a.onClick);
b.addEventListener('click', a.onClick);
foot.appendChild(b);
foot.appendChild(b);
Line 218: Line 191:
bg.appendChild(box);
bg.appendChild(box);
document.body.appendChild(bg);
document.body.appendChild(bg);
return box;
}
}
function field(labelText, node) {
function field(labelText, node) {
var wrap = document.createElement('div');
var wrap = document.createElement('div');
Line 229: Line 200:
return wrap;
return wrap;
}
}
function getLastRev() {
function getLastRev() {
return api.get({
return api.get({
action: 'query',
action: 'query', prop: 'revisions', titles: pageName(),
prop: 'revisions',
rvlimit: 2, rvprop: 'ids|user|comment|timestamp', formatversion: 2
titles: pageName(),
}).then(function (data) {
rvlimit: 2,
var page = data.query && data.query.pages && data.query.pages[0];
rvprop: 'ids|user|comment|timestamp',
if (!page || page.missing || !page.revisions || !page.revisions.length) { return null; }
formatversion: 2
return { page: page, latest: page.revisions[0], previous: page.revisions[1] || null };
});
}
function doUndo(info, summary) {
if (!info.previous) {
throw new Error('Nothing to restore: that editor is the only author, or there is no earlier revision.');
}
return api.postWithEditToken({
action: 'edit', title: pageName(), undo: info.latest.revid, undoafter: info.previous.revid,
summary: summary + ' ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
}).then(function (res) {
if (res.error) { throw new Error(res.error.info || res.error.code); }
return { method: 'undo', data: res };
});
}
function revertAllByUser(summary, targetUser) {
return api.get({
action: 'query', prop: 'revisions', titles: pageName(),
rvlimit: 50, rvprop: 'ids|user', formatversion: 2
}).then(function (data) {
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var page = data.query && data.query.pages && data.query.pages[0];
if (!page || page.missing || !page.revisions || !page.revisions.length) {
var revs = (page && page.revisions) || [];
return null;
if (!revs.length) { throw new Error('Could not read this page history.'); }
if (revs[0].user !== targetUser) {
throw new Error('Latest edit is not by ' + targetUser + '.');
}
}
return {
var keep = null;
page: page,
for (var i = 0; i < revs.length; i++) {
latest: page.revisions[0],
if (revs[i].user !== targetUser) {
previous: page.revisions[1] || null
keep = revs[i];
};
break;
}
}
if (!keep) {
throw new Error('Nothing to restore: that editor is the only author in recent history.');
}
return api.postWithEditToken({
action: 'edit', title: pageName(),
undo: revs[0].revid, undoafter: keep.revid,
summary: summary + ' ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
}).then(function (res) {
if (res.error) { throw new Error(res.error.info || res.error.code); }
return { method: 'undo-all', data: res };
});
});
});
}
}
 
function revertWithSummary(summary, userToRevert, allByUser) {
function revertWithSummary(summary, userToRevert) {
return getLastRev().then(function (info) {
return getLastRev().then(function (info) {
if (!info) {
if (!info) { throw new Error('Could not read this page history.'); }
throw new Error('Could not read this page history.');
var targetUser = userToRevert || info.latest.user;
function errCode(err) {
return (err && (err.code || err.name)) || (String(err.message || err));
}
}
var latest = info.latest;
if (allByUser && prefs.useRollback && hasRollback) {
var targetUser = userToRevert || latest.user;
if (prefs.useRollback && hasRollback) {
return api.postWithToken('rollback', {
return api.postWithToken('rollback', {
action: 'rollback',
action: 'rollback', title: pageName(), user: targetUser,
title: pageName(),
summary: summary + ' ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
user: targetUser,
summary: summary + ' ([[' + CONF.docsPage + '|AntiVandal]])',
formatversion: 2
}).then(function (res) {
}).then(function (res) {
if (res.error) {
if (res.error) { throw res.error; }
throw new Error(res.error.info || res.error.code);
return { method: 'rollback', data: res };
}).catch(function (err) {
var code = errCode(err);
if (String(code).indexOf('onlyauthor') !== -1 ||
String(code).indexOf('alreadyrolled') !== -1 ||
String(err.info || '').indexOf('only author') !== -1) {
return revertAllByUser(summary, targetUser);
}
}
return { method: 'rollback', data: res };
throw new Error(err.info || err.message || code || String(err));
});
});
}
}
if (!info.previous) {
if (allByUser) {
throw new Error('No earlier revision to restore.');
return revertAllByUser(summary, targetUser);
}
}
return api.postWithEditToken({
return doUndo(info, summary);
action: 'edit',
title: pageName(),
undo: latest.revid,
undoafter: info.previous.revid,
summary: summary + ' ([[' + CONF.docsPage + '|AntiVandal]])',
formatversion: 2
}).then(function (res) {
if (res.error) {
throw new Error(res.error.info || res.error.code);
}
if (res.edit && res.edit.result !== 'Success') {
throw new Error('Edit did not succeed.');
}
return { method: 'undo', data: res };
});
});
});
}
}
function restoreRevision(revid) {
function restoreRevision(revid) {
return api.get({
return api.get({
action: 'query',
action: 'query', prop: 'revisions', revids: revid,
prop: 'revisions',
rvprop: 'ids|content|timestamp|user', rvslots: 'main', formatversion: 2
revids: revid,
rvprop: 'ids|content|timestamp|user',
rvslots: 'main',
formatversion: 2
}).then(function (data) {
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var page = data.query && data.query.pages && data.query.pages[0];
var rev = page && page.revisions && page.revisions[0];
var rev = page && page.revisions && page.revisions[0];
var text = rev && rev.slots && rev.slots.main && rev.slots.main.content;
var text = rev && rev.slots && rev.slots.main && rev.slots.main.content;
if (!text && text !== '') {
if (!text && text !== '') { throw new Error('Could not load that revision.'); }
throw new Error('Could not load that revision.');
}
return api.postWithEditToken({
return api.postWithEditToken({
action: 'edit',
action: 'edit', title: page.title || pageName(), text: text,
title: page.title || pageName(),
summary: 'Restored revision ' + revid + ' ([[Bharatpedia:AntiVandal|AntiVandal]])',
text: text,
summary: 'Restored revision ' + revid + ' ([[' + CONF.docsPage + '|AntiVandal]])',
basetimestamp: undefined,
formatversion: 2
formatversion: 2
});
});
});
});
}
}
function appendTalkNotice(user, templateName, article, extra) {
function appendTalkNotice(user, templateName, article, extra) {
var talk = talkTitleFor(user);
var talk = talkTitleFor(user);
var subst = '{{subst:' + templateName;
var subst = '{' + '{subst:' + templateName;
if (article) {
if (article) { subst += '|1=' + article.replace(/_/g, ' '); }
subst += '|1=' + article.replace(/_/g, ' ');
if (extra) { subst += '|2=' + extra; }
}
subst += '}}\n' + '~~' + '~~';
if (extra) {
subst += '|2=' + extra;
}
subst += '}} [[User:WikiDwarf|WikiDwarf]] ([[User talk:WikiDwarf|talk]]) 15:09, 21 September 2026 (IST)';
return api.postWithEditToken({
return api.postWithEditToken({
action: 'edit',
action: 'edit', title: talk, appendtext: '\n\n' + subst,
title: talk,
summary: 'Notice ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
appendtext: '\n\n' + subst,
summary: 'Notice regarding [[' + (article || pageName()).replace(/_/g, ' ') + ']] ([[' + CONF.docsPage + '|AntiVandal]])',
formatversion: 2
});
});
}
}
 
function detectTalkLevel(user) {
if (!user) { return Promise.resolve(1); }
return api.get({
action: 'query', prop: 'revisions', titles: talkTitleFor(user),
rvlimit: 1, rvprop: 'content', rvslots: 'main', formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var text = page && page.revisions && page.revisions[0] &&
page.revisions[0].slots && page.revisions[0].slots.main &&
page.revisions[0].slots.main.content || '';
var max = 0;
var re = /\{\{(?:subst:)?uw-[\w-]+([1-4])\b/gi;
var m;
while ((m = re.exec(text))) { max = Math.max(max, parseInt(m[1], 10)); }
return Math.min(4, max + 1) || 1;
}).catch(function () { return 1; });
}
function openRevertDialog() {
function openRevertDialog() {
getLastRev().then(function (info) {
getLastRev().then(function (info) {
Line 358: Line 346:
sel.addEventListener('change', function () {
sel.addEventListener('change', function () {
var p = PRESETS.filter(function (x) { return x.id === sel.value; })[0];
var p = PRESETS.filter(function (x) { return x.id === sel.value; })[0];
if (p && p.summary) {
if (p && p.summary) { sum.value = p.summary; }
sum.value = p.summary;
}
});
});
var alsoWarn = document.createElement('input');
var alsoWarn = document.createElement('input');
alsoWarn.type = 'checkbox';
alsoWarn.type = 'checkbox';
alsoWarn.id = 'rk-also-warn';
var warnLab = document.createElement('label');
var warnLab = document.createElement('label');
warnLab.htmlFor = 'rk-also-warn';
warnLab.className = 'rk-check';
warnLab.style.fontWeight = 'normal';
warnLab.appendChild(alsoWarn);
warnLab.textContent = ' Open notice dialog after revert';
warnLab.appendChild(document.createTextNode('Warn user after revert'));
var allBy = document.createElement('input');
allBy.type = 'checkbox';
allBy.checked = true;
var allLab = document.createElement('label');
allLab.className = 'rk-check';
allLab.appendChild(allBy);
allLab.appendChild(document.createTextNode('Revert all recent edits by this user when possible'));
var row = document.createElement('div');
var row = document.createElement('div');
row.style.marginTop = '10px';
row.className = 'rk-checkrow';
row.appendChild(alsoWarn);
row.appendChild(warnLab);
row.appendChild(warnLab);
 
row.appendChild(allLab);
wrap.appendChild(field('Editor to revert', userInput));
wrap.appendChild(field('Editor to revert', userInput));
wrap.appendChild(field('Reason', sel));
wrap.appendChild(field('Reason', sel));
wrap.appendChild(field('Edit summary', sum));
wrap.appendChild(field('Edit summary', sum));
wrap.appendChild(row);
wrap.appendChild(row);
openModal('Revert with AntiVandal', wrap, [
openModal('Revert with AntiVandal', wrap, [
{ label: 'Cancel', onClick: closeModal },
{ label: 'Cancel', onClick: closeModal },
{
{
label: 'Revert',
label: 'Revert', primary: true,
primary: true,
onClick: function () {
onClick: function () {
var summary = (sum.value || 'Reverted edit').trim();
var summary = (sum.value || 'Reverted edit').trim();
var who = userInput.value.trim();
var who = userInput.value.trim();
if (!who) {
if (!who) { toast('Name the editor.', 'err'); return; }
toast('Name the editor whose edit you are reverting.', 'err');
toast('Reverting...');
return;
revertWithSummary(summary, who, allBy.checked).then(function (res) {
}
toast('Reverting…');
revertWithSummary(summary, who).then(function (res) {
toast('Reverted with ' + res.method + '.', 'ok');
toast('Reverted with ' + res.method + '.', 'ok');
closeModal();
closeModal();
if (alsoWarn.checked) {
if (alsoWarn.checked) { openWarnDialog(who, sel.value); }
openWarnDialog(who, sel.value);
else { setTimeout(function () { location.reload(); }, 700); }
} else {
}).catch(function (err) { toast(err.message || String(err), 'err'); });
setTimeout(function () { location.reload(); }, 700);
}
}).catch(function (err) {
toast(err.message || String(err), 'err');
});
}
}
}
}
]);
]);
}).catch(function (err) {
}).catch(function (err) { toast(err.message || String(err), 'err'); });
toast(err.message || String(err), 'err');
}
function recentEditors(limit) {
return api.get({
action: 'query', prop: 'revisions', titles: pageName(),
rvlimit: 40, rvprop: 'user', formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var revs = (page && page.revisions) || [];
var seen = {};
var out = [];
revs.forEach(function (r) {
if (r.user && !seen[r.user]) {
seen[r.user] = true;
out.push(r.user);
}
});
return out.slice(0, limit || 5);
}).catch(function () { return []; });
}
function bindUserSuggest(input, listEl) {
var t = null;
input.addEventListener('input', function () {
clearTimeout(t);
var q = input.value.trim();
if (q.length < 2) { return; }
t = setTimeout(function () {
api.get({
action: 'query', list: 'allusers', aufrom: q, auprefix: q,
aulimit: 8, formatversion: 2
}).then(function (data) {
var names = (data.query && data.query.allusers || []).map(function (u) { return u.name; });
names.forEach(function (name) {
var exists = false;
for (var i = 0; i < listEl.options.length; i++) {
if (listEl.options[i].value === name) { exists = true; break; }
}
if (!exists) {
var o = document.createElement('option');
o.value = name;
listEl.appendChild(o);
}
});
}).catch(function () {});
}, 250);
});
});
}
}
function openWarnDialog(prefillUser, presetId) {
function openWarnDialog(prefillUser, presetId) {
var wrap = document.createElement('div');
var wrap = document.createElement('div');
var recentSel = document.createElement('select');
var opt0 = document.createElement('option');
opt0.value = '';
opt0.textContent = 'Recent editors on this page';
recentSel.appendChild(opt0);
var userInput = document.createElement('input');
var userInput = document.createElement('input');
userInput.type = 'text';
userInput.type = 'text';
userInput.setAttribute('list', 'av-user-suggest');
userInput.autocomplete = 'off';
userInput.value = prefillUser || relevantUser() || '';
userInput.value = prefillUser || relevantUser() || '';
var suggest = document.createElement('datalist');
suggest.id = 'av-user-suggest';
recentSel.addEventListener('change', function () {
if (recentSel.value) { userInput.value = recentSel.value; }
});
recentEditors(5).then(function (names) {
names.forEach(function (name) {
var o = document.createElement('option');
o.value = name;
o.textContent = name;
recentSel.appendChild(o);
var d = document.createElement('option');
d.value = name;
suggest.appendChild(d);
});
if (!userInput.value && names[0]) { userInput.value = names[0]; }
});
bindUserSuggest(userInput, suggest);
var articleInput = document.createElement('input');
var articleInput = document.createElement('input');
articleInput.type = 'text';
articleInput.type = 'text';
Line 423: Line 471:
var o = document.createElement('option');
var o = document.createElement('option');
o.value = n.id;
o.value = n.id;
o.textContent = n.label + (n.leveled ? ' (1–4)' : '');
o.textContent = n.label + (n.leveled ? ' (1-4)' : '');
sel.appendChild(o);
sel.appendChild(o);
});
});
if (presetId) {
if (presetId) {
var mapped = {
var mapped = { vandalism: 'uw-vandalism', blanking: 'uw-delete', test: 'uw-test', unsourced: 'uw-unsourced', spam: 'uw-spam', npov: 'uw-biog' }[presetId];
vandalism: 'uw-vandalism',
if (mapped) { sel.value = mapped; }
blanking: 'uw-delete',
test: 'uw-test',
unsourced: 'uw-unsourced',
spam: 'uw-spam',
npov: 'uw-biog'
}[presetId];
if (mapped) {
sel.value = mapped;
}
}
}
var level = document.createElement('select');
var level = document.createElement('select');
Line 447: Line 486:
});
});
var extra = document.createElement('textarea');
var extra = document.createElement('textarea');
extra.placeholder = 'Optional extra sentence for templates that accept a second parameter';
var hint = document.createElement('p');
 
hint.style.fontSize = '12px';
hint.textContent = 'Checking talk page for prior notices...';
function currentTemplate() {
function currentTemplate() {
var meta = NOTICES.filter(function (n) { return n.id === sel.value; })[0];
var meta = NOTICES.filter(function (n) { return n.id === sel.value; })[0];
if (meta && meta.leveled) {
return (meta && meta.leveled) ? sel.value + level.value : sel.value;
return sel.value + level.value;
}
return sel.value;
}
}
 
wrap.appendChild(field('Recent editors', recentSel));
wrap.appendChild(field('User', userInput));
wrap.appendChild(field('User (type to search)', userInput));
var signLine = document.createElement('p');
signLine.style.fontSize = '12px';
signLine.textContent = 'Will sign as ' + actingUser();
wrap.appendChild(signLine);
wrap.appendChild(suggest);
wrap.appendChild(field('Related article', articleInput));
wrap.appendChild(field('Related article', articleInput));
wrap.appendChild(field('Notice', sel));
wrap.appendChild(field('Notice', sel));
wrap.appendChild(field('Level (if the template has levels)', level));
wrap.appendChild(field('Level', level));
wrap.appendChild(field('Extra note', extra));
wrap.appendChild(field('Extra note', extra));
var hint = document.createElement('p');
hint.style.fontSize = '12px';
hint.style.color = '#555';
hint.textContent = "Checking last month talk-page notices...";
wrap.appendChild(hint);
wrap.appendChild(hint);
detectTalkLevel(userInput.value.trim()).then(function (n) {
detectTalkLevel(userInput.value.trim()).then(function (n) {
level.value = String(n);
level.value = String(n);
hint.textContent = "Suggested level " + n + " from this month talk page (you can change it).";
hint.textContent = 'Suggested level ' + n + ' from the talk page.';
});
userInput.addEventListener('change', function () {
detectTalkLevel(userInput.value.trim()).then(function (n) {
level.value = String(n);
hint.textContent = "Suggested level " + n + " from this month talk page.";
});
});
});
openModal('Post a talk-page notice', wrap, [
openModal('Post a talk-page notice', wrap, [
{ label: 'Cancel', onClick: closeModal },
{ label: 'Cancel', onClick: closeModal },
{
{
label: 'Post notice',
label: 'Post notice', primary: true,
primary: true,
onClick: function () {
onClick: function () {
var user = userInput.value.trim();
var user = userInput.value.trim();
if (!user) {
if (!user) { toast('Enter a username or IP.', 'err'); return; }
toast('Enter a username or IP.', 'err');
return;
}
var tpl = currentTemplate();
var tpl = currentTemplate();
toast('Posting {{' + tpl + '}}…');
toast('Posting...');
appendTalkNotice(user, tpl, articleInput.value.trim(), extra.value.trim()).then(function (res) {
appendTalkNotice(user, tpl, articleInput.value.trim(), extra.value.trim()).then(function (res) {
if (res.error) {
if (res.error) { throw new Error(res.error.info || res.error.code); }
throw new Error(res.error.info || res.error.code);
toast('Notice posted.', 'ok');
}
toast('Notice posted on User talk:' + user, 'ok');
closeModal();
closeModal();
}).catch(function (err) {
}).catch(function (err) { toast(err.message || String(err), 'err'); });
toast(err.message || String(err), 'err');
});
}
}
}
}
]);
]);
}
}
function openPrefs() {
function openPrefs() {
var wrap = document.createElement('div');
var wrap = document.createElement('div');
var pos = document.createElement('select');
['top', 'bottom'].forEach(function (v) {
var o = document.createElement('option');
o.value = v;
o.textContent = v === 'top' ? 'Under the page title' : 'Stick to the bottom';
pos.appendChild(o);
});
pos.value = prefs.barPosition;
var rb = document.createElement('input');
var rb = document.createElement('input');
rb.type = 'checkbox';
rb.type = 'checkbox';
Line 520: Line 534:
var rbLab = document.createElement('label');
var rbLab = document.createElement('label');
rbLab.style.fontWeight = 'normal';
rbLab.style.fontWeight = 'normal';
rbLab.textContent = ' Prefer software rollback when this account has the right';
rbLab.textContent = ' Prefer software rollback when available';
var row = document.createElement('div');
var row = document.createElement('div');
row.appendChild(rb);
row.appendChild(rb);
row.appendChild(rbLab);
row.appendChild(rbLab);
wrap.appendChild(field('Toolbar placement', pos));
wrap.appendChild(row);
wrap.appendChild(row);
openModal('AntiVandal preferences', wrap, [
openModal('AntiVandal preferences', wrap, [
{ label: 'Close', onClick: closeModal },
{ label: 'Close', onClick: closeModal },
{
{
label: 'Save',
label: 'Save', primary: true,
primary: true,
onClick: function () {
onClick: function () {
prefs.barPosition = pos.value;
prefs.useRollback = rb.checked;
prefs.useRollback = rb.checked;
savePrefs(prefs);
savePrefs(prefs);
closeModal();
closeModal();
toast('Preferences saved. Reload to move the bar.', 'ok');
toast('Saved.', 'ok');
}
}
}
}
]);
]);
}
}
function goLatest() {
function goLatest() {
location.href = mw.util.getUrl(pageName(), { diff: 'cur', oldid: 'prev' });
location.href = mw.util.getUrl(pageName(), { diff: 'cur', oldid: 'prev' });
}
}
function reportAiv() {
function reportAiv() {
getLastRev().then(function (info) {
getLastRev().then(function (info) {
var user = (info && info.latest && info.latest.user) || relevantUser() || '';
var user = (info && info.latest && info.latest.user) || relevantUser() || '';
var wrap = document.createElement('div');
var wrap = document.createElement('div');
var p = document.createElement('p');
p.textContent = 'This opens the AIV noticeboard. Paste the suggested line, or edit it first.';
var ta = document.createElement('textarea');
var ta = document.createElement('textarea');
ta.value = '* {{vandal|' + user + '}} – repeated unconstructive editing on [[' +
ta.value = '* {' + '{vandal|' + user + '}} - unconstructive editing on [[' +
pageName().replace(/_/g, ' ') + ']]. [[User:WikiDwarf|WikiDwarf]] ([[User talk:WikiDwarf|talk]]) 15:09, 21 September 2026 (IST)';
pageName().replace(/_/g, ' ') + ']]. ' + wikiSig();
wrap.appendChild(p);
wrap.appendChild(ta);
wrap.appendChild(ta);
openModal('Report at AIV', wrap, [
openModal('Report at AIV', wrap, [
{ label: 'Cancel', onClick: closeModal },
{ label: 'Cancel', onClick: closeModal },
{
{
label: 'Open AIV',
label: 'Open AIV', primary: true,
primary: true,
onClick: function () {
onClick: function () {
try {
sessionStorage.setItem('antivandal-aiv-draft', ta.value);
} catch (e) { /* ignore */ }
window.open(mw.util.getUrl(CONF.aivPage, { action: 'edit', section: 'new' }), '_blank');
window.open(mw.util.getUrl(CONF.aivPage, { action: 'edit', section: 'new' }), '_blank');
}
}
Line 572: Line 574:
});
});
}
}
function requestProtection() {
function requestProtection() {
window.open(mw.util.getUrl(CONF.rfppPage, { action: 'edit', section: 'new' }), '_blank');
var wrap = document.createElement('div');
var art = document.createElement('input');
art.type = 'text';
art.value = pageName().replace(/_/g, ' ');
var level = document.createElement('select');
['semi-protection', 'extended confirmed', 'full protection', 'move protection', 'unprotection'].forEach(function (v) {
var o = document.createElement('option');
o.value = v;
o.textContent = v;
level.appendChild(o);
});
var reason = document.createElement('textarea');
reason.placeholder = 'Why this page needs that level';
var current = document.createElement('p');
current.textContent = 'Checking current protection...';
wrap.appendChild(field('Article', art));
wrap.appendChild(field('Requested level', level));
wrap.appendChild(field('Reason', reason));
wrap.appendChild(current);
api.get({
action: 'query', titles: pageName(), prop: 'info', inprop: 'protection', formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var prot = (page && page.protection) || [];
current.textContent = prot.length
? ('Current: ' + prot.map(function (p) { return p.type + '=' + p.level; }).join(', '))
: 'Current: not protected';
}).catch(function () { current.textContent = ''; });
function pageTitle() { return art.value.trim(); }
function requestBody() {
var title = pageTitle();
return '* {' + '{pagelinks|' + title + '}}\n' +
reason.value.trim() + ' ' + wikiSig() + '\n';
}
function draft() {
return '=== [[' + pageTitle() + ']] ===\n' + requestBody();
}
openModal('Request page protection', wrap, [
{ label: 'Cancel', onClick: closeModal },
{
label: 'Open board with draft',
onClick: function () {
try {
sessionStorage.setItem('antivandal-rfpp-subject', '[[' + art.value.trim() + ']]');
sessionStorage.setItem('antivandal-rfpp-draft', requestBody());
} catch (e) {}
window.open(mw.util.getUrl(CONF.rfppPage, { action: 'edit', section: 'new' }), '_blank');
}
},
{
label: 'Post request', primary: true,
onClick: function () {
if (!art.value.trim() || !reason.value.trim()) {
toast('Article and reason are required.', 'err');
return;
}
toast('Posting request...');
api.postWithEditToken({
action: 'edit', title: CONF.rfppPage, appendtext: '\n' + draft(),
summary: 'Protection request for [[' + art.value.trim() + ']] ([[Bharatpedia:AntiVandal|AntiVandal]])',
formatversion: 2
}).then(function (res) {
if (res.error) { throw new Error(res.error.info || res.error.code); }
toast('Request posted.', 'ok');
closeModal();
}).catch(function (err) { toast(err.message || String(err), 'err'); });
}
}
]);
}
function openAdminReport(kind) {
var title = kind === 'privacy' ? CONF.privacyPage : CONF.anPage;
window.open(mw.util.getUrl(title, { action: 'edit', section: 'new' }), '_blank');
}
}
function openNewMessage(prefillUser) {
function openNewMessage(prefillUser) {
var wrap = document.createElement('div');
var wrap = document.createElement('div');
Line 585: Line 657:
subj.type = 'text';
subj.type = 'text';
var body = document.createElement('textarea');
var body = document.createElement('textarea');
body.placeholder = 'Wikitext of the new section';
wrap.appendChild(field('User', userInput));
wrap.appendChild(field('User', userInput));
var signLine2 = document.createElement('p');
signLine2.style.fontSize = '12px';
signLine2.textContent = 'Will sign as ' + actingUser();
wrap.appendChild(signLine2);
wrap.appendChild(field('Subject', subj));
wrap.appendChild(field('Subject', subj));
wrap.appendChild(field('Message', body));
wrap.appendChild(field('Message', body));
Line 592: Line 667:
{ label: 'Cancel', onClick: closeModal },
{ label: 'Cancel', onClick: closeModal },
{
{
label: 'Send',
label: 'Send', primary: true,
primary: true,
onClick: function () {
onClick: function () {
var user = userInput.value.trim();
var user = userInput.value.trim();
if (!user || !subj.value.trim()) {
if (!user || !subj.value.trim()) { toast('Need a user and a subject.', 'err'); return; }
toast('Need a user and a subject.', 'err');
var text = '\n\n== ' + subj.value.trim() + ' ==\n' + body.value + '\n' + wikiSig();
return;
toast('Sending...');
}
var text = '\n\n== ' + subj.value.trim() + ' ==\n' + body.value + '\n[[User:WikiDwarf|WikiDwarf]] ([[User talk:WikiDwarf|talk]]) 15:09, 21 September 2026 (IST)';
toast('Sending…');
api.postWithEditToken({
api.postWithEditToken({
action: 'edit',
action: 'edit', title: talkTitleFor(user), appendtext: text,
title: talkTitleFor(user),
summary: 'New message ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
appendtext: text,
summary: 'New message ([[' + CONF.docsPage + '|AntiVandal]])',
formatversion: 2
}).then(function (res) {
}).then(function (res) {
if (res.error) {
if (res.error) { throw new Error(res.error.info || res.error.code); }
throw new Error(res.error.info || res.error.code);
}
toast('Message posted.', 'ok');
toast('Message posted.', 'ok');
closeModal();
closeModal();
}).catch(function (err) {
}).catch(function (err) { toast(err.message || String(err), 'err'); });
toast(err.message || String(err), 'err');
});
}
}
}
}
]);
]);
}
}
 
function welcomeTemplates() {
var alertTimer = null;
var alertLastRev = null;
function toggleAlert(btn) {
if (alertTimer) {
clearInterval(alertTimer);
alertTimer = null;
btn.classList.remove('rk-on');
toast('Alert on change off.');
return;
}
btn.classList.add('rk-on');
toast('Watching this page for new edits.');
getLastRev().then(function (info) {
alertLastRev = info && info.latest ? info.latest.revid : null;
});
alertTimer = setInterval(function () {
getLastRev().then(function (info) {
var id = info && info.latest ? info.latest.revid : null;
if (id && alertLastRev && id !== alertLastRev) {
alertLastRev = id;
if (window.Notification && Notification.permission === 'granted') {
new Notification('AntiVandal', { body: pageName().replace(/_/g, ' ') + ' was edited.' });
}
toast('New edit on this page.', 'ok');
goLatest();
} else if (id) {
alertLastRev = id;
}
});
}, 12000);
if (window.Notification && Notification.permission === 'default') {
Notification.requestPermission();
}
}
 
function openMoreMenu(anchor) {
var menu = document.getElementById('antivandal-more');
if (!menu) {
return;
}
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
}
 
function bindUserContextMenu() {
document.addEventListener('contextmenu', function (e) {
if (!e.shiftKey) {
return;
}
var a = e.target.closest ? e.target.closest('a') : null;
if (!a || !a.href) {
return;
}
var m = a.href.match(/[?&]title=User:([^&#]+)/i) || a.href.match(/\/wiki\/User:([^?#]+)/);
if (!m) {
return;
}
e.preventDefault();
var user = decodeURIComponent(m[1]).replace(/_/g, ' ').split('/')[0];
var ctx = document.getElementById('antivandal-ctx');
if (!ctx) {
ctx = document.createElement('div');
ctx.id = 'antivandal-ctx';
document.body.appendChild(ctx);
}
ctx.innerHTML = '';
function item(label, fn) {
var b = document.createElement('button');
b.type = 'button';
b.textContent = label;
b.addEventListener('click', function () {
ctx.style.display = 'none';
fn();
});
ctx.appendChild(b);
}
item('User page', function () { location.href = mw.util.getUrl('User:' + user); });
item('Talk page', function () { location.href = mw.util.getUrl('User talk:' + user); });
item('Contributions', function () { location.href = mw.util.getUrl('Special:Contributions/' + user); });
item('New message', function () { openNewMessage(user); });
item('Notice', function () { openWarnDialog(user); });
item('Quick template', function () { openQuickTemplates(user); });
ctx.style.left = e.pageX + 'px';
ctx.style.top = e.pageY + 'px';
ctx.style.display = 'block';
});
document.addEventListener('click', function () {
var ctx = document.getElementById('antivandal-ctx');
if (ctx) {
ctx.style.display = 'none';
}
var more = document.getElementById('antivandal-more');
if (more && more.style.display === 'block') {
more.style.display = 'none';
}
});
}
 
function addDiffRestore() {
if (mw.config.get('wgDiffNewId') || mw.config.get('wgDiffOldId') ||
mw.config.get('wgAction') === 'history') {
var oldid = mw.util.getParamValue('oldid') || mw.config.get('wgRevisionId');
if (!oldid) {
return;
}
var bar = document.getElementById('antivandal-bar');
if (!bar) {
return;
}
var b = document.createElement('button');
b.type = 'button';
b.textContent = 'Restore this revision';
b.addEventListener('click', function () {
if (!window.confirm('Replace the current page text with revision ' + oldid + '?')) {
return;
}
toast('Restoring…');
restoreRevision(oldid).then(function (res) {
if (res.error) {
throw new Error(res.error.info || res.error.code);
}
toast('Revision restored.', 'ok');
setTimeout(function () { location.href = mw.util.getUrl(pageName()); }, 600);
}).catch(function (err) {
toast(err.message || String(err), 'err');
});
});
bar.appendChild(b);
}
}
 
function currentMonthHeading() {
var months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
var d = new Date();
return months[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
}
 
function detectTalkLevel(user) {
if (!user) {
return Promise.resolve(1);
}
return api.get({
return api.get({
action: 'query',
action: 'query', list: 'categorymembers',
prop: 'revisions',
cmtitle: CONF.welcomeCat, cmnamespace: 10,
titles: talkTitleFor(user),
cmtype: 'page', cmlimit: 50, formatversion: 2
rvlimit: 1,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
}).then(function (data) {
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var members = (data.query && data.query.categorymembers) || [];
var text = page && page.revisions && page.revisions[0] &&
var out = members.map(function (m) {
page.revisions[0].slots && page.revisions[0].slots.main &&
var title = (m.title || '').replace(/^Template:/i, '');
page.revisions[0].slots.main.content || '';
return { title: title, label: title.replace(/_/g, ' ') };
var month = currentMonthHeading();
}).filter(function (x) { return x.title; });
var slice = text;
if (!out.length) {
var idx = text.lastIndexOf(month);
out = [
if (idx !== -1) {
{ title: 'Welcome', label: 'Welcome' },
slice = text.slice(idx);
{ title: 'Welcometest', label: 'Welcome after a test edit' }
}
];
var max = 0;
var re = /\{\{(?:subst:)?uw-[\w-]+([1-4])\b/gi;
var m;
while ((m = re.exec(slice))) {
max = Math.max(max, parseInt(m[1], 10));
}
}
return Math.min(4, max + 1) || 1;
return out;
}).catch(function () {
}).catch(function () {
return 1;
return [
{ title: 'Welcome', label: 'Welcome' },
{ title: 'Welcometest', label: 'Welcome after a test edit' }
];
});
});
}
}
 
function openWelcomeUser(prefillUser) {
function loadPacks() {
try {
var raw = localStorage.getItem(CONF.packKey);
if (raw) {
return JSON.parse(raw);
}
} catch (e) { /* ignore */ }
return [{
name: 'Starter',
items: [
{ label: 'Welcome', wikitext: 'Hello, WikiDwarf/AntiVandal.js, and welcome to Bharatpedia! Thank you for [[Special:Contributions/WikiDwarf/AntiVandal.js|your contributions]]. I hope you like this place and decide to stay. We are working hard to make this one of the most popular websites in the world, and it's only possible through the contributions of editors like you. This website is not just a collection of articles, it's an active community. The real fun here is contributing to Bharatpedia, but don't feel hurt if some of your first few edits get removed, as there are some central guidelines you may not be familiar with.
 
Some good advice: be bold in your editing, and use the talk pages to discuss with other editors. Be kind to others, because there's a lot you can learn from them, and there's lots they can help you with.
 
There's lots of resources to help you become a great editor, from our basic introduction to our in-depth manual. But if you have any questions or problems, no matter what they are, leave me a message on [[User talk:WikiDwarf|my talk page]], or ask a question on your talk page. Oh, and please [[Bharatpedia:Signatures|sign your name]] on talk pages using four tildes (<nowiki>~~~~</nowiki>); this will automatically produce your username and the date. Again, welcome!
' },
{ label: 'Welcome (test)', wikitext: '{{subst:welcometest}}' },
{ label: 'Please discuss', wikitext: 'Please use the article talk page to discuss changes rather than repeating them. [[User:WikiDwarf|WikiDwarf]] ([[User talk:WikiDwarf|talk]]) 15:09, 21 September 2026 (IST)' }
]
}];
}
 
function savePacks(packs) {
try {
localStorage.setItem(CONF.packKey, JSON.stringify(packs));
} catch (e) { /* ignore */ }
}
 
function openQuickTemplates(prefillUser) {
var packs = loadPacks();
var wrap = document.createElement('div');
var wrap = document.createElement('div');
var userInput = document.createElement('input');
var userInput = document.createElement('input');
userInput.type = 'text';
userInput.type = 'text';
userInput.value = prefillUser || relevantUser() || '';
userInput.value = prefillUser || relevantUser() || '';
wrap.appendChild(field('User talk target', userInput));
var sel = document.createElement('select');
var list = document.createElement('div');
var loading = document.createElement('option');
packs.forEach(function (pack) {
loading.value = '';
var h = document.createElement('p');
loading.textContent = 'Loading welcome templates...';
h.style.fontWeight = '700';
sel.appendChild(loading);
h.textContent = pack.name;
wrap.appendChild(field('User', userInput));
list.appendChild(h);
var signLine = document.createElement('p');
(pack.items || []).forEach(function (item) {
signLine.style.fontSize = '12px';
var b = document.createElement('button');
signLine.textContent = 'Will sign as ' + actingUser();
b.type = 'button';
wrap.appendChild(signLine);
b.textContent = item.label;
wrap.appendChild(field('Welcome template', sel));
b.style.margin = '0 6px 6px 0';
welcomeTemplates().then(function (list) {
b.addEventListener('click', function () {
sel.innerHTML = '';
var user = userInput.value.trim();
list.forEach(function (item) {
if (!user) {
var o = document.createElement('option');
toast('Enter a user.', 'err');
o.value = item.title;
return;
o.textContent = item.label;
}
sel.appendChild(o);
toast('Posting template…');
api.postWithEditToken({
action: 'edit',
title: talkTitleFor(user),
appendtext: '\n\n' + item.wikitext + '\n[[User:WikiDwarf|WikiDwarf]] ([[User talk:WikiDwarf|talk]]) 15:09, 21 September 2026 (IST)',
summary: 'Template: ' + item.label + ' ([[' + CONF.docsPage + '|AntiVandal]])',
formatversion: 2
}).then(function (res) {
if (res.error) {
throw new Error(res.error.info || res.error.code);
}
toast('Posted “' + item.label + '”.', 'ok');
closeModal();
}).catch(function (err) {
toast(err.message || String(err), 'err');
});
});
list.appendChild(b);
});
});
});
});
wrap.appendChild(list);
openModal('Welcome new user', wrap, [
var add = document.createElement('textarea');
{ label: 'Cancel', onClick: closeModal },
add.placeholder = 'Add a pack as JSON: {"name":"Mine","items":[{"label":"Hi","wikitext":"Hello"}]}';
wrap.appendChild(field('Install a pack (JSON)', add));
openModal('Quick templates', wrap, [
{ label: 'Close', onClick: closeModal },
{
{
label: 'Install pack',
label: 'Post welcome', primary: true,
primary: true,
onClick: function () {
onClick: function () {
try {
var user = userInput.value.trim();
var pack = JSON.parse(add.value);
var tpl = sel.value;
if (!pack.name || !pack.items) {
if (!user || !tpl) { toast('Need a user and a template.', 'err'); return; }
throw new Error('Need name and items');
toast('Posting welcome...');
}
api.postWithEditToken({
packs.push(pack);
action: 'edit', title: talkTitleFor(user),
savePacks(packs);
appendtext: '\n\n{' + '{subst:' + tpl + '}}\n' + wikiSig(),
toast('Pack saved in this browser.', 'ok');
summary: 'Welcome ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
}).then(function (res) {
if (res.error) { throw new Error(res.error.info || res.error.code); }
toast('Welcome posted.', 'ok');
closeModal();
closeModal();
openQuickTemplates(userInput.value);
}).catch(function (err) { toast(err.message || String(err), 'err'); });
} catch (err) {
toast(err.message || 'Invalid JSON', 'err');
}
}
}
}
}
]);
]);
}
}
 
function openQuickTemplates(prefillUser) {
function togglePatrol() {
openWelcomeUser(prefillUser);
var box = document.getElementById('av-patrol');
if (box) {
box.parentNode.removeChild(box);
if (togglePatrol._t) {
clearInterval(togglePatrol._t);
togglePatrol._t = null;
}
return;
}
box = document.createElement('div');
box.id = 'av-patrol';
box.innerHTML = '<h3>AntiVandal patrol <button type="button" id="av-patrol-x" style="float:right">×</button></h3><div id="av-patrol-list"></div>';
document.body.appendChild(box);
document.getElementById('av-patrol-x').addEventListener('click', togglePatrol);
function refresh() {
api.get({
action: 'query',
list: 'recentchanges',
rcprop: 'title|ids|sizes|user|comment|timestamp',
rclimit: 25,
rctype: 'edit|new',
formatversion: 2
}).then(function (data) {
var list = document.getElementById('av-patrol-list');
if (!list) {
return;
}
list.innerHTML = '';
(data.query && data.query.recentchanges || []).forEach(function (rc) {
var a = document.createElement('a');
a.className = 'av-p-item ' + ((rc.newlen - rc.oldlen) < 0 ? 'av-neg' : 'av-pos');
a.href = mw.util.getUrl(rc.title, { diff: rc.revid, oldid: rc.old_revid || 'prev' });
a.innerHTML = '';
var t = document.createElement('strong');
t.textContent = rc.title;
a.appendChild(t);
a.appendChild(document.createElement('br'));
a.appendChild(document.createTextNode(rc.user + ' · ' + (rc.newlen - rc.oldlen)));
list.appendChild(a);
});
}).catch(function (err) {
toast(err.message || String(err), 'err');
});
}
refresh();
togglePatrol._t = setInterval(refresh, 8000);
}
}
function openMat() {
function openMat() {
toast('Loading history…');
toast('Loading history...');
api.get({
api.get({
action: 'query',
action: 'query', prop: 'revisions', titles: pageName(),
prop: 'revisions',
rvlimit: 30, rvprop: 'ids|user|comment|timestamp', formatversion: 2
titles: pageName(),
rvlimit: 30,
rvprop: 'ids|user|comment|timestamp',
formatversion: 2
}).then(function (data) {
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var page = data.query && data.query.pages && data.query.pages[0];
Line 957: Line 771:
var users = [];
var users = [];
revs.forEach(function (r) {
revs.forEach(function (r) {
if (r.user && !seen[r.user]) {
if (r.user && !seen[r.user]) { seen[r.user] = true; users.push(r.user); }
seen[r.user] = true;
users.push(r.user);
}
});
});
var wrap = document.createElement('div');
var wrap = document.createElement('div');
var p = document.createElement('p');
p.textContent = 'Tick editors from the last 30 revisions, pick one notice, then post. Review the list before you commit.';
wrap.appendChild(p);
var boxes = [];
var boxes = [];
users.forEach(function (u) {
users.forEach(function (u) {
Line 987: Line 795:
sel.appendChild(o);
sel.appendChild(o);
});
});
wrap.appendChild(field('Notice to post', sel));
wrap.appendChild(field('Notice', sel));
openModal('Multiple notices', wrap, [
openModal('Multiple notices', wrap, [
{ label: 'Cancel', onClick: closeModal },
{ label: 'Cancel', onClick: closeModal },
{
{
label: 'Post to checked',
label: 'Post to checked', primary: true,
primary: true,
onClick: function () {
onClick: function () {
var chosen = boxes.filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
var chosen = boxes.filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
if (!chosen.length) {
if (!chosen.length) { toast('None selected.', 'err'); return; }
toast('No users selected.', 'err');
if (!window.confirm('Post to ' + chosen.length + ' talk pages?')) { return; }
return;
}
if (!window.confirm('Post ' + sel.value + ' to ' + chosen.length + ' talk page(s)?')) {
return;
}
var chain = Promise.resolve();
var chain = Promise.resolve();
chosen.forEach(function (u) {
chosen.forEach(function (u) {
Line 1,008: Line 810:
});
});
});
});
toast('Posting…');
chain.then(function () { toast('Done.', 'ok'); closeModal(); })
chain.then(function () {
.catch(function (err) { toast(err.message || String(err), 'err'); });
toast('Notices posted.', 'ok');
closeModal();
}).catch(function (err) {
toast(err.message || String(err), 'err');
});
}
}
}
}
]);
]);
}).catch(function (err) {
}).catch(function (err) { toast(err.message || String(err), 'err'); });
toast(err.message || String(err), 'err');
});
}
}
function openPendingReview() {
function openPendingReview() {
api.get({
api.get({ action: 'query', list: 'oldreviewedpages', orlimit: 15, formatversion: 2 }).then(function (data) {
action: 'query',
list: 'oldreviewedpages',
orlimit: 15,
formatversion: 2
}).then(function (data) {
var pages = (data.query && data.query.oldreviewedpages) || [];
var pages = (data.query && data.query.oldreviewedpages) || [];
if (!pages.length) {
if (!pages.length) { toast('No pending-review queue on this wiki.', 'err'); return; }
toast('No pending-review queue on this wiki (or it is empty).', 'err');
return;
}
var wrap = document.createElement('div');
var wrap = document.createElement('div');
pages.forEach(function (p) {
pages.forEach(function (p) {
var a = document.createElement('a');
var a = document.createElement('a');
a.href = mw.util.getUrl(p.title, { diff: 'cur', oldid: 'prev' });
a.href = mw.util.getUrl(p.title);
a.textContent = p.title;
a.textContent = p.title;
a.style.display = 'block';
a.style.display = 'block';
Line 1,044: Line 830:
});
});
openModal('Pending review', wrap, [{ label: 'Close', onClick: closeModal }]);
openModal('Pending review', wrap, [{ label: 'Close', onClick: closeModal }]);
}).catch(function () {
}).catch(function () { toast('Pending changes is not available here.', 'err'); });
toast('FlaggedRevs / pending changes is not available on this wiki.', 'err');
});
}
 
function openAdminReport(kind) {
var title = kind === 'privacy' ? CONF.privacyPage : CONF.anPage;
var wrap = document.createElement('div');
var p = document.createElement('p');
p.textContent = kind === 'privacy'
? 'Opens the local suppression / privacy request board. Do not post private data on a public page if a private route exists.'
: "Opens the administrators noticeboard with a draft about this page.";
var ta = document.createElement('textarea');
ta.value = 'Regarding [[' + pageName().replace(/_/g, ' ') + ']]. ';
wrap.appendChild(p);
wrap.appendChild(ta);
openModal(kind === 'privacy' ? 'Privacy / suppression' : 'Report to administrators', wrap, [
{ label: 'Cancel', onClick: closeModal },
{
label: 'Open board',
primary: true,
onClick: function () {
try {
sessionStorage.setItem('antivandal-report-draft', ta.value);
} catch (e) { /* ignore */ }
window.open(mw.util.getUrl(title, { action: 'edit', section: 'new' }), '_blank');
}
}
]);
}
}
function bindContribsRollback() {
function bindContribsRollback() {
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') {
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') { return; }
return;
var list = document.querySelector('.mw-contributions-list');
}
if (!list) { return; }
var list = document.querySelector('.mw-contributions-list') || document.querySelector('#bodyContent ul');
if (!list) {
return;
}
list.querySelectorAll('li').forEach(function (li) {
list.querySelectorAll('li').forEach(function (li) {
if (li.querySelector('.av-contrib-rev')) {
if (li.querySelector('.av-contrib-rev')) { return; }
return;
}
var diff = li.querySelector('a[href*="diff="]');
var diff = li.querySelector('a[href*="diff="]');
if (!diff) {
if (!diff) { return; }
return;
}
var b = document.createElement('button');
var b = document.createElement('button');
b.type = 'button';
b.type = 'button';
Line 1,099: Line 848:
var href = diff.getAttribute('href') || '';
var href = diff.getAttribute('href') || '';
var titleMatch = href.match(/title=([^&]+)/) || href.match(/\/wiki\/([^?]+)/);
var titleMatch = href.match(/title=([^&]+)/) || href.match(/\/wiki\/([^?]+)/);
var title = titleMatch ? decodeURIComponent(titleMatch[1].replace(/\+/g, ' ')).replace(/_/g, ' ') : null;
var title = titleMatch ? decodeURIComponent(titleMatch[1].replace(/\+/g, ' ')) : null;
if (!title) {
toast('Could not read that title.', 'err');
return;
}
var user = mw.config.get('wgRelevantUserName');
var user = mw.config.get('wgRelevantUserName');
if (!window.confirm('Revert the latest edit by ' + user + ' on ' + title + '?')) {
if (!title || !user) { toast('Could not read that row.', 'err'); return; }
return;
if (!window.confirm('Revert latest edit by ' + user + ' on ' + title + '?')) { return; }
}
api.get({
var prevPage = pageName;
action: 'query', prop: 'revisions', titles: title.replace(/ /g, '_'),
pageName = function () { return title.replace(/ /g, '_'); };
rvlimit: 2, rvprop: 'ids|user', formatversion: 2
toast('Reverting…');
}).then(function (data) {
revertWithSummary('Reverted unconstructive edit(s)', user).then(function () {
var page = data.query && data.query.pages && data.query.pages[0];
pageName = prevPage;
var latest = page && page.revisions && page.revisions[0];
toast('Reverted on “' + title + '”.', 'ok');
if (!latest) { throw new Error('No revision'); }
}).catch(function (err) {
if (prefs.useRollback && hasRollback) {
pageName = prevPage;
return api.postWithToken('rollback', {
toast(err.message || String(err), 'err');
action: 'rollback', title: title, user: user,
});
summary: 'Reverted unconstructive edit(s) ([[Bharatpedia:AntiVandal|AntiVandal]])',
formatversion: 2
});
}
var prev = page.revisions[1];
if (!prev) { throw new Error('No earlier revision'); }
return api.postWithEditToken({
action: 'edit', title: title, undo: latest.revid, undoafter: prev.revid,
summary: 'Reverted unconstructive edit(s) ([[Bharatpedia:AntiVandal|AntiVandal]])',
formatversion: 2
});
}).then(function () { toast('Reverted.', 'ok'); })
.catch(function (err) { toast(err.message || String(err), 'err'); });
});
});
li.appendChild(b);
li.appendChild(b);
});
});
}
}
 
function bindUserContextMenu() {
document.addEventListener('contextmenu', function (e) {
if (!e.shiftKey) { return; }
var a = e.target.closest ? e.target.closest('a') : null;
if (!a || !a.href) { return; }
var m = a.href.match(/[?&]title=User:([^&#]+)/i) || a.href.match(/\/wiki\/User:([^?#]+)/);
if (!m) { return; }
e.preventDefault();
var user = decodeURIComponent(m[1]).replace(/_/g, ' ').split('/')[0];
var ctx = document.getElementById('antivandal-ctx');
if (!ctx) {
ctx = document.createElement('div');
ctx.id = 'antivandal-ctx';
document.body.appendChild(ctx);
}
ctx.innerHTML = '';
function item(label, fn) {
var b = document.createElement('button');
b.type = 'button';
b.textContent = label;
b.addEventListener('click', function () { ctx.style.display = 'none'; fn(); });
ctx.appendChild(b);
}
item('User page', function () { location.href = mw.util.getUrl('User:' + user); });
item('Talk page', function () { location.href = mw.util.getUrl('User talk:' + user); });
item('Contributions', function () { location.href = mw.util.getUrl('Special:Contributions/' + user); });
item('New message', function () { openNewMessage(user); });
ctx.style.left = e.pageX + 'px';
ctx.style.top = e.pageY + 'px';
ctx.style.display = 'block';
});
document.addEventListener('click', function () {
var ctx = document.getElementById('antivandal-ctx');
if (ctx) { ctx.style.display = 'none'; }
var more = document.getElementById('antivandal-more');
if (more && more.style.display === 'block') { more.style.display = 'none'; }
});
}
function neoBtn(iconClass, tip, fn) {
function neoBtn(iconClass, tip, fn) {
var b = document.createElement('button');
var b = document.createElement('button');
Line 1,141: Line 935:
return b;
return b;
}
}
function buildBar() {
function buildBar() {
if (document.getElementById('antivandal-open')) {
if (document.getElementById('antivandal-open')) { return true; }
return true;
var row = document.querySelector('.bpneo-title-actions');
}
if (!row) { return false; }
var row = document.querySelector('.bpneo-title-actions[data-bpneo-actions]') ||
document.querySelector('.bpneo-title-actions');
if (!row) {
return false;
}
 
var openBtn = neoBtn('ri-error-warning-fill', 'AntiVandal tools');
var openBtn = neoBtn('ri-error-warning-fill', 'AntiVandal tools');
openBtn.id = 'antivandal-open';
openBtn.id = 'antivandal-open';
openBtn.style.color = '#c62828';
openBtn.style.color = '#c62828';
openBtn.style.display = 'inline-flex';
if (!openBtn.querySelector('i') || !openBtn.querySelector('i').offsetParent) {
openBtn.textContent = '!';
openBtn.style.fontWeight = '700';
}
var tools = document.createElement('span');
var tools = document.createElement('span');
tools.id = 'antivandal-tools';
tools.id = 'antivandal-tools';
 
tools.appendChild(neoBtn('ri-close-line', 'Close AntiVandal', function () {
var closeBtn = neoBtn('ri-close-line', 'Close AntiVandal', function () {
row.classList.remove('av-open');
row.classList.remove('av-open');
});
}));
tools.appendChild(closeBtn);
tools.appendChild(neoBtn('ri-arrow-go-back-line', 'Revert last editor', openRevertDialog));
tools.appendChild(neoBtn('ri-arrow-go-back-line', 'Revert last editor', openRevertDialog));
tools.appendChild(neoBtn('ri-alarm-warning-line', 'Post a notice', function () { openWarnDialog(); }));
tools.appendChild(neoBtn('ri-history-line', 'Restore this revision', function () {
tools.appendChild(neoBtn('ri-mail-send-line', 'New talk-page message', function () { openNewMessage(); }));
var oldid = mw.util.getParamValue('oldid') || mw.config.get('wgRevisionId');
if (!oldid) { toast('Open a specific revision first.', 'err'); return; }
if (!window.confirm('Replace current text with revision ' + oldid + '?')) { return; }
restoreRevision(oldid).then(function () {
toast('Restored.', 'ok');
setTimeout(function () { location.href = mw.util.getUrl(pageName()); }, 600);
}).catch(function (err) { toast(err.message || String(err), 'err'); });
}));
tools.appendChild(neoBtn('ri-refresh-line', 'Latest revision', goLatest));
tools.appendChild(neoBtn('ri-refresh-line', 'Latest revision', goLatest));
var alertBtn = neoBtn('ri-radar-line', 'Alert on change', function () { toggleAlert(alertBtn); });
tools.appendChild(neoBtn('ri-alarm-warning-line', 'Warn user', function () { openWarnDialog(); }));
tools.appendChild(alertBtn);
tools.appendChild(neoBtn('ri-user-add-line', 'Welcome new user', function () { openWelcomeUser(); }));
tools.appendChild(neoBtn('ri-shield-keyhole-line', 'Request protection', requestProtection));
tools.appendChild(neoBtn('ri-shield-keyhole-line', 'Request protection', requestProtection));
tools.appendChild(neoBtn('ri-file-copy-line', 'Quick templates', function () { openQuickTemplates(); }));
tools.appendChild(neoBtn('ri-pulse-line', 'Live patrol', togglePatrol));
var moreWrap = document.createElement('span');
var moreWrap = document.createElement('span');
moreWrap.style.position = 'relative';
moreWrap.style.position = 'relative';
Line 1,194: Line 977:
menu.appendChild(mb);
menu.appendChild(mb);
}
}
menuItem('New talk-page message', function () { openNewMessage(); });
menuItem('Report at AIV', reportAiv);
menuItem('Report at AIV', reportAiv);
menuItem('Report to administrators', function () { openAdminReport('admin'); });
menuItem('Report to administrators', function () { openAdminReport('admin'); });
menuItem('Privacy / suppression request', function () { openAdminReport('privacy'); });
menuItem('Privacy request', function () { openAdminReport('privacy'); });
menuItem('Multiple notices (this page)', openMat);
menuItem('Multiple notices', openMat);
menuItem('Pending review queue', openPendingReview);
menuItem('Pending review', openPendingReview);
menuItem('Restore this revision', function () {
var oldid = mw.util.getParamValue('oldid') || mw.config.get('wgRevisionId');
if (!oldid) {
toast('Open a specific revision first.', 'err');
return;
}
if (!window.confirm('Replace the current page text with revision ' + oldid + '?')) {
return;
}
toast('Restoring…');
restoreRevision(oldid).then(function (res) {
if (res.error) {
throw new Error(res.error.info || res.error.code);
}
toast('Revision restored.', 'ok');
setTimeout(function () { location.href = mw.util.getUrl(pageName()); }, 600);
}).catch(function (err) {
toast(err.message || String(err), 'err');
});
});
menuItem('Preferences', openPrefs);
menuItem('Preferences', openPrefs);
moreBtn.addEventListener('click', function (e) {
moreBtn.addEventListener('click', function (e) {
Line 1,228: Line 992:
moreWrap.appendChild(menu);
moreWrap.appendChild(menu);
tools.appendChild(moreWrap);
tools.appendChild(moreWrap);
openBtn.addEventListener('click', function (e) {
openBtn.addEventListener('click', function (e) {
e.preventDefault();
e.preventDefault();
Line 1,234: Line 997:
row.classList.add('av-open');
row.classList.add('av-open');
});
});
var slot = document.getElementById('antivandal-slot');
var slot = document.getElementById('antivandal-slot');
var editLink = row.querySelector('a.bpneo-title-btn[title="Edit"]') ||
var editLink = row.querySelector('a.bpneo-title-btn[title="Edit"]') ||
row.querySelector('a.bpneo-title-btn[href*="action=edit"]') ||
row.querySelector('a.bpneo-title-btn[href*="action=edit"]');
row.querySelector('a.bpneo-title-btn .ri-edit-line') &&
if (slot) { slot.replaceWith(openBtn); }
row.querySelector('a.bpneo-title-btn .ri-edit-line').closest('a');
else if (editLink) { row.insertBefore(openBtn, editLink); }
if (slot) {
else { row.insertBefore(openBtn, row.firstChild); }
slot.replaceWith(openBtn);
} else if (editLink) {
row.insertBefore(openBtn, editLink);
} else {
row.insertBefore(openBtn, row.firstChild);
}
row.appendChild(tools);
row.appendChild(tools);
bindUserContextMenu();
bindUserContextMenu();
return true;
return true;
}
}
function init() {
function init() {
if (!mw.config.get('wgUserName')) { return; }
injectCss();
api = new mw.Api({ parameters: { formatversion: 2 } });
var rights = mw.config.get('wgUserGroups') || [];
hasRollback = rights.indexOf('rollbacker') !== -1 ||
rights.indexOf('sysop') !== -1 ||
rights.indexOf('steward') !== -1;
if (!buildBar()) {
var tries = 0;
var t = setInterval(function () {
tries += 1;
if (buildBar() || tries > 25) { clearInterval(t); }
}, 200);
}
try { bindContribsRollback(); } catch (e) {}
fillEditDrafts();
}
function fillEditDrafts() {
if (mw.config.get('wgAction') !== 'edit') { return; }
var page = mw.config.get('wgPageName') || '';
var box = document.getElementById('wpTextbox1');
var subj = document.getElementById('wpSummary');
if (!box) { return; }
try {
try {
if (!mw.config.get('wgUserName')) {
if (page === CONF.rfppPage.replace(/ /g, '_')) {
return;
var d = sessionStorage.getItem('antivandal-rfpp-draft');
var s = sessionStorage.getItem('antivandal-rfpp-subject');
if (d && !box.value.trim()) { box.value = d; }
if (s && subj && !subj.value.trim()) { subj.value = s; }
sessionStorage.removeItem('antivandal-rfpp-draft');
sessionStorage.removeItem('antivandal-rfpp-subject');
}
}
injectCss();
if (page === CONF.aivPage.replace(/ /g, '_')) {
api = new mw.Api({ parameters: { formatversion: 2 } });
var a = sessionStorage.getItem('antivandal-aiv-draft');
var rights = mw.config.get('wgUserGroups') || [];
if (a && !box.value.trim()) { box.value = a; }
hasRollback = rights.indexOf('rollbacker') !== -1 ||
sessionStorage.removeItem('antivandal-aiv-draft');
rights.indexOf('sysop') !== -1 ||
rights.indexOf('steward') !== -1;
if (!buildBar()) {
var tries = 0;
var t = setInterval(function () {
tries += 1;
if (buildBar() || tries > 25) {
clearInterval(t);
}
}, 200);
}
}
try {
} catch (e) {}
bindContribsRollback();
} catch (e2) { /* contribs page only */ }
} catch (err) {
console.error('AntiVandal init', err);
}
}
}
if (document.readyState === 'loading') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('DOMContentLoaded', function () {

Latest revision as of 20:24, 21 September 2026

/**
 * AntiVandal - Bharatpedia patrol helper
 * Host: User:WikiDwarf/AntiVandal.js
 * Version 1.1.1
 */
(function () {
	'use strict';
	if (window.AntiVandal && window.AntiVandal.loaded) {
		return;
	}
	var RK = (window.AntiVandal = window.AntiVandal || {});
	RK.loaded = true;
	RK.version = '1.1.2';
	var CONF = {
		storageKey: 'antivandal-prefs-v1',
		packKey: 'antivandal-packs-v1',
		tag: 'AntiVandal',
		aivPage: 'Bharatpedia:Administrator intervention against vandalism',
		rfppPage: 'Bharatpedia:Requests for page protection',
		anPage: 'Bharatpedia:Administrators noticeboard',
		privacyPage: 'Bharatpedia:Requests for oversight',
		docsPage: 'Bharatpedia:AntiVandal',
		welcomeCat: 'Category:Welcome templates'
	};
	var PRESETS = [
		{ id: 'vandalism', label: 'Vandalism', summary: 'Reverted unconstructive edit(s)', notice: 'uw-vandalism', level: true },
		{ id: 'blanking', label: 'Blanking', summary: 'Reverted unexplained content removal', notice: 'uw-delete', level: true },
		{ id: 'test', label: 'Test edit', summary: 'Reverted test edit', notice: 'uw-test', level: true },
		{ id: 'unsourced', label: 'Unsourced', summary: 'Reverted unsourced or poorly sourced addition', notice: 'uw-unsourced', level: true },
		{ id: 'spam', label: 'Spam / promo', summary: 'Reverted promotional or spam content', notice: 'uw-spam', level: true },
		{ id: 'npov', label: 'POV / attack', summary: 'Reverted non-neutral or attack content', notice: 'uw-biog', level: true },
		{ id: 'custom', label: 'Custom summary', summary: '', notice: '', level: false }
	];
	var NOTICES = [
		{ id: 'uw-vandalism', label: 'Vandalism', leveled: true },
		{ id: 'uw-test', label: 'Test edit', leveled: true },
		{ id: 'uw-delete', label: 'Removing content', leveled: true },
		{ id: 'uw-unsourced', label: 'Unsourced content', leveled: true },
		{ id: 'uw-error', label: 'Factual error / hoax', leveled: true },
		{ id: 'uw-spam', label: 'Spam / advertising', leveled: true },
		{ id: 'uw-advert', label: 'Promotional tone', leveled: true },
		{ id: 'uw-biog', label: 'BLP / biography issue', leveled: true },
		{ id: 'uw-defamatory', label: 'Defamatory content', leveled: true },
		{ id: 'uw-joke', label: 'Joke edit', leveled: true },
		{ id: 'uw-notvoted', label: 'Not a vote / forum', leveled: false },
		{ id: 'uw-chat', label: 'Talk page as chat', leveled: false },
		{ id: 'welcome', label: 'Welcome (constructive new editor)', leveled: false },
		{ id: 'welcometest', label: 'Welcome after a test edit', leveled: false }
	];
	function defaultPrefs() {
		return { barPosition: 'top', useRollback: true, autoNotice: false };
	}
	function loadPrefs() {
		try {
			var raw = localStorage.getItem(CONF.storageKey);
			return raw ? Object.assign(defaultPrefs(), JSON.parse(raw)) : defaultPrefs();
		} catch (e) {
			return defaultPrefs();
		}
	}
	function savePrefs(p) {
		try { localStorage.setItem(CONF.storageKey, JSON.stringify(p)); } catch (e) {}
	}
	var prefs = loadPrefs();
	var api = null;
	var hasRollback = false;
	function toast(msg, kind) {
		var el = document.getElementById('antivandal-toast');
		if (!el) {
			el = document.createElement('div');
			el.id = 'antivandal-toast';
			document.body.appendChild(el);
		}
		el.className = 'rk-toast rk-toast-' + (kind || 'info');
		el.textContent = msg;
		el.style.display = 'block';
		clearTimeout(toast._t);
		toast._t = setTimeout(function () { el.style.display = 'none'; }, 4200);
	}
	function pageName() {
		return mw.config.get('wgPageName');
	}
	function relevantUser() {
		return mw.config.get('wgRelevantUserName') || null;
	}
	function talkTitleFor(user) {
		if (!user) { return null; }
		return 'User talk:' + user;
	}
	function actingUser() {
		if (mw.user && typeof mw.user.getName === 'function' && mw.user.getName()) {
			return mw.user.getName();
		}
		return mw.config.get('wgUserName') || '';
	}
	function wikiSig() {
		return '~~' + '~~';
	}
	function injectCss() {
		if (document.getElementById('antivandal-css')) { return; }
		var css = [
			'#antivandal-open.bpneo-title-btn{color:#c62828;}',
			'#antivandal-tools{display:none;align-items:center;gap:inherit;}',
			'.bpneo-title-actions.av-open > :not(#antivandal-tools){display:none !important;}',
			'.bpneo-title-actions.av-open #antivandal-tools{display:inline-flex;}',
			'#antivandal-tools .bpneo-title-btn.rk-on{color:#2a4a33;}',
			'#antivandal-more{position:absolute;right:0;top:36px;background:#fff;border:1px solid #a2a9b1;',
			'box-shadow:0 2px 8px rgba(0,0,0,.18);min-width:220px;z-index:50;display:none;padding:4px 0;}',
			'#antivandal-more button{display:block;width:100%;text-align:left;border:0;background:none;',
			'padding:8px 12px;cursor:pointer;font:13px sans-serif;}',
			'#antivandal-more button:hover{background:#eaecf0;}',
			'#antivandal-ctx{position:fixed;z-index:1300;background:#fff;border:1px solid #a2a9b1;',
			'box-shadow:0 2px 8px rgba(0,0,0,.2);min-width:180px;padding:4px 0;display:none;}',
			'#antivandal-ctx button{display:block;width:100%;text-align:left;border:0;background:none;',
			'padding:8px 12px;cursor:pointer;font:13px sans-serif;}',
			'#antivandal-ctx button:hover{background:#eaecf0;}',
			'.rk-modal-bg{position:fixed;inset:0;background:rgba(17,17,17,.4);z-index:1100;display:flex;',
			'align-items:center;justify-content:center;padding:16px;}',
			'.rk-modal{background:#fff;color:#1b1b1b;max-width:480px;width:100%;border-radius:10px;',
			'border-top:3px solid #c62828;box-shadow:0 8px 28px rgba(0,0,0,.2);overflow:hidden;}',
			'.rk-modal .rk-head{display:flex;align-items:center;justify-content:space-between;',
			'padding:10px 14px;border-bottom:1px solid #eee;}',
			'.rk-modal h2{margin:0;padding:0;background:transparent;color:#111;font-size:15px;font-weight:600;}',
			'.rk-modal .rk-x{border:0;background:none;font-size:18px;line-height:1;cursor:pointer;color:#444;}',
			'.rk-modal .rk-body{padding:12px 14px;font-size:13px;}',
			'.rk-modal label{display:block;font-weight:600;margin:8px 0 4px;}',
			'.rk-modal .rk-checkrow{display:flex;flex-wrap:wrap;gap:10px 18px;margin-top:10px;align-items:center;}',
			'.rk-modal .rk-check{display:inline-flex;align-items:center;gap:6px;font-weight:400;margin:0;}',
			'.rk-modal .rk-check input{margin:0;}',
			'.rk-modal input[type=text],.rk-modal select,.rk-modal textarea{width:100%;box-sizing:border-box;',
			'padding:7px 8px;font:13px sans-serif;}',
			'.rk-modal textarea{min-height:70px;}',
			'.rk-modal .rk-actions{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;}',
			'.rk-modal .rk-actions button{padding:6px 12px;border-radius:4px;border:1px solid #bbb;cursor:pointer;background:#fff;}',
			'.rk-modal .rk-actions .rk-primary{background:#c62828;color:#fff;border-color:#c62828;}',
			'.rk-toast{position:fixed;right:16px;bottom:16px;z-index:1200;background:#1f2a22;color:#fff;',
			'padding:10px 14px;border-radius:6px;max-width:360px;display:none;}',
			'.rk-toast-err{background:#6b2a22;}',
			'.rk-toast-ok{background:#2a4a33;}',
			'#av-patrol{position:fixed;top:0;right:0;width:320px;height:100vh;background:#111;color:#eee;',
			'z-index:1250;overflow:auto;font:13px sans-serif;}',
			'#av-patrol h3{margin:0;padding:12px 14px;background:#1f2a22;}',
			'#av-patrol .av-p-item{display:block;padding:8px 12px;border-bottom:1px solid #333;color:#eee;text-decoration:none;}',
			'.av-contrib-rev{margin-left:6px;font-size:12px;cursor:pointer;color:#c62828;border:0;background:none;}'
		].join('');
		var style = document.createElement('style');
		style.id = 'antivandal-css';
		style.textContent = css;
		document.head.appendChild(style);
	}
	function closeModal() {
		var bg = document.getElementById('antivandal-modal-bg');
		if (bg) { bg.parentNode.removeChild(bg); }
	}
	function openModal(title, bodyNode, actions) {
		closeModal();
		var bg = document.createElement('div');
		bg.id = 'antivandal-modal-bg';
		bg.className = 'rk-modal-bg';
		bg.addEventListener('click', function (e) { if (e.target === bg) { closeModal(); } });
		var box = document.createElement('div');
		box.className = 'rk-modal';
		var head = document.createElement('div');
		head.className = 'rk-head';
		var h = document.createElement('h2');
		h.textContent = title;
		var x = document.createElement('button');
		x.type = 'button';
		x.className = 'rk-x';
		x.setAttribute('aria-label', 'Close');
		x.textContent = 'x';
		x.addEventListener('click', closeModal);
		head.appendChild(h);
		head.appendChild(x);
		box.appendChild(head);
		var body = document.createElement('div');
		body.className = 'rk-body';
		body.appendChild(bodyNode);
		box.appendChild(body);
		var foot = document.createElement('div');
		foot.className = 'rk-actions';
		(actions || []).forEach(function (a) {
			var b = document.createElement('button');
			b.type = 'button';
			b.textContent = a.label;
			if (a.primary) { b.className = 'rk-primary'; }
			b.addEventListener('click', a.onClick);
			foot.appendChild(b);
		});
		box.appendChild(foot);
		bg.appendChild(box);
		document.body.appendChild(bg);
	}
	function field(labelText, node) {
		var wrap = document.createElement('div');
		var lab = document.createElement('label');
		lab.textContent = labelText;
		wrap.appendChild(lab);
		wrap.appendChild(node);
		return wrap;
	}
	function getLastRev() {
		return api.get({
			action: 'query', prop: 'revisions', titles: pageName(),
			rvlimit: 2, rvprop: 'ids|user|comment|timestamp', formatversion: 2
		}).then(function (data) {
			var page = data.query && data.query.pages && data.query.pages[0];
			if (!page || page.missing || !page.revisions || !page.revisions.length) { return null; }
			return { page: page, latest: page.revisions[0], previous: page.revisions[1] || null };
		});
	}
	function doUndo(info, summary) {
		if (!info.previous) {
			throw new Error('Nothing to restore: that editor is the only author, or there is no earlier revision.');
		}
		return api.postWithEditToken({
			action: 'edit', title: pageName(), undo: info.latest.revid, undoafter: info.previous.revid,
			summary: summary + ' ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
		}).then(function (res) {
			if (res.error) { throw new Error(res.error.info || res.error.code); }
			return { method: 'undo', data: res };
		});
	}
	function revertAllByUser(summary, targetUser) {
		return api.get({
			action: 'query', prop: 'revisions', titles: pageName(),
			rvlimit: 50, rvprop: 'ids|user', formatversion: 2
		}).then(function (data) {
			var page = data.query && data.query.pages && data.query.pages[0];
			var revs = (page && page.revisions) || [];
			if (!revs.length) { throw new Error('Could not read this page history.'); }
			if (revs[0].user !== targetUser) {
				throw new Error('Latest edit is not by ' + targetUser + '.');
			}
			var keep = null;
			for (var i = 0; i < revs.length; i++) {
				if (revs[i].user !== targetUser) {
					keep = revs[i];
					break;
				}
			}
			if (!keep) {
				throw new Error('Nothing to restore: that editor is the only author in recent history.');
			}
			return api.postWithEditToken({
				action: 'edit', title: pageName(),
				undo: revs[0].revid, undoafter: keep.revid,
				summary: summary + ' ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
			}).then(function (res) {
				if (res.error) { throw new Error(res.error.info || res.error.code); }
				return { method: 'undo-all', data: res };
			});
		});
	}
	function revertWithSummary(summary, userToRevert, allByUser) {
		return getLastRev().then(function (info) {
			if (!info) { throw new Error('Could not read this page history.'); }
			var targetUser = userToRevert || info.latest.user;
			function errCode(err) {
				return (err && (err.code || err.name)) || (String(err.message || err));
			}
			if (allByUser && prefs.useRollback && hasRollback) {
				return api.postWithToken('rollback', {
					action: 'rollback', title: pageName(), user: targetUser,
					summary: summary + ' ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
				}).then(function (res) {
					if (res.error) { throw res.error; }
					return { method: 'rollback', data: res };
				}).catch(function (err) {
					var code = errCode(err);
					if (String(code).indexOf('onlyauthor') !== -1 ||
						String(code).indexOf('alreadyrolled') !== -1 ||
						String(err.info || '').indexOf('only author') !== -1) {
						return revertAllByUser(summary, targetUser);
					}
					throw new Error(err.info || err.message || code || String(err));
				});
			}
			if (allByUser) {
				return revertAllByUser(summary, targetUser);
			}
			return doUndo(info, summary);
		});
	}
	function restoreRevision(revid) {
		return api.get({
			action: 'query', prop: 'revisions', revids: revid,
			rvprop: 'ids|content|timestamp|user', rvslots: 'main', formatversion: 2
		}).then(function (data) {
			var page = data.query && data.query.pages && data.query.pages[0];
			var rev = page && page.revisions && page.revisions[0];
			var text = rev && rev.slots && rev.slots.main && rev.slots.main.content;
			if (!text && text !== '') { throw new Error('Could not load that revision.'); }
			return api.postWithEditToken({
				action: 'edit', title: page.title || pageName(), text: text,
				summary: 'Restored revision ' + revid + ' ([[Bharatpedia:AntiVandal|AntiVandal]])',
				formatversion: 2
			});
		});
	}
	function appendTalkNotice(user, templateName, article, extra) {
		var talk = talkTitleFor(user);
		var subst = '{' + '{subst:' + templateName;
		if (article) { subst += '|1=' + article.replace(/_/g, ' '); }
		if (extra) { subst += '|2=' + extra; }
		subst += '}}\n' + '~~' + '~~';
		return api.postWithEditToken({
			action: 'edit', title: talk, appendtext: '\n\n' + subst,
			summary: 'Notice ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
		});
	}
	function detectTalkLevel(user) {
		if (!user) { return Promise.resolve(1); }
		return api.get({
			action: 'query', prop: 'revisions', titles: talkTitleFor(user),
			rvlimit: 1, rvprop: 'content', rvslots: 'main', formatversion: 2
		}).then(function (data) {
			var page = data.query && data.query.pages && data.query.pages[0];
			var text = page && page.revisions && page.revisions[0] &&
				page.revisions[0].slots && page.revisions[0].slots.main &&
				page.revisions[0].slots.main.content || '';
			var max = 0;
			var re = /\{\{(?:subst:)?uw-[\w-]+([1-4])\b/gi;
			var m;
			while ((m = re.exec(text))) { max = Math.max(max, parseInt(m[1], 10)); }
			return Math.min(4, max + 1) || 1;
		}).catch(function () { return 1; });
	}
	function openRevertDialog() {
		getLastRev().then(function (info) {
			var user = info && info.latest ? info.latest.user : (relevantUser() || '');
			var wrap = document.createElement('div');
			var userInput = document.createElement('input');
			userInput.type = 'text';
			userInput.value = user || '';
			var sel = document.createElement('select');
			PRESETS.forEach(function (p) {
				var o = document.createElement('option');
				o.value = p.id;
				o.textContent = p.label;
				sel.appendChild(o);
			});
			var sum = document.createElement('input');
			sum.type = 'text';
			sum.value = PRESETS[0].summary;
			sel.addEventListener('change', function () {
				var p = PRESETS.filter(function (x) { return x.id === sel.value; })[0];
				if (p && p.summary) { sum.value = p.summary; }
			});
			var alsoWarn = document.createElement('input');
			alsoWarn.type = 'checkbox';
			var warnLab = document.createElement('label');
			warnLab.className = 'rk-check';
			warnLab.appendChild(alsoWarn);
			warnLab.appendChild(document.createTextNode('Warn user after revert'));
			var allBy = document.createElement('input');
			allBy.type = 'checkbox';
			allBy.checked = true;
			var allLab = document.createElement('label');
			allLab.className = 'rk-check';
			allLab.appendChild(allBy);
			allLab.appendChild(document.createTextNode('Revert all recent edits by this user when possible'));
			var row = document.createElement('div');
			row.className = 'rk-checkrow';
			row.appendChild(warnLab);
			row.appendChild(allLab);
			wrap.appendChild(field('Editor to revert', userInput));
			wrap.appendChild(field('Reason', sel));
			wrap.appendChild(field('Edit summary', sum));
			wrap.appendChild(row);
			openModal('Revert with AntiVandal', wrap, [
				{ label: 'Cancel', onClick: closeModal },
				{
					label: 'Revert', primary: true,
					onClick: function () {
						var summary = (sum.value || 'Reverted edit').trim();
						var who = userInput.value.trim();
						if (!who) { toast('Name the editor.', 'err'); return; }
						toast('Reverting...');
						revertWithSummary(summary, who, allBy.checked).then(function (res) {
							toast('Reverted with ' + res.method + '.', 'ok');
							closeModal();
							if (alsoWarn.checked) { openWarnDialog(who, sel.value); }
							else { setTimeout(function () { location.reload(); }, 700); }
						}).catch(function (err) { toast(err.message || String(err), 'err'); });
					}
				}
			]);
		}).catch(function (err) { toast(err.message || String(err), 'err'); });
	}
	function recentEditors(limit) {
		return api.get({
			action: 'query', prop: 'revisions', titles: pageName(),
			rvlimit: 40, rvprop: 'user', formatversion: 2
		}).then(function (data) {
			var page = data.query && data.query.pages && data.query.pages[0];
			var revs = (page && page.revisions) || [];
			var seen = {};
			var out = [];
			revs.forEach(function (r) {
				if (r.user && !seen[r.user]) {
					seen[r.user] = true;
					out.push(r.user);
				}
			});
			return out.slice(0, limit || 5);
		}).catch(function () { return []; });
	}
	function bindUserSuggest(input, listEl) {
		var t = null;
		input.addEventListener('input', function () {
			clearTimeout(t);
			var q = input.value.trim();
			if (q.length < 2) { return; }
			t = setTimeout(function () {
				api.get({
					action: 'query', list: 'allusers', aufrom: q, auprefix: q,
					aulimit: 8, formatversion: 2
				}).then(function (data) {
					var names = (data.query && data.query.allusers || []).map(function (u) { return u.name; });
					names.forEach(function (name) {
						var exists = false;
						for (var i = 0; i < listEl.options.length; i++) {
							if (listEl.options[i].value === name) { exists = true; break; }
						}
						if (!exists) {
							var o = document.createElement('option');
							o.value = name;
							listEl.appendChild(o);
						}
					});
				}).catch(function () {});
			}, 250);
		});
	}
	function openWarnDialog(prefillUser, presetId) {
		var wrap = document.createElement('div');
		var recentSel = document.createElement('select');
		var opt0 = document.createElement('option');
		opt0.value = '';
		opt0.textContent = 'Recent editors on this page';
		recentSel.appendChild(opt0);
		var userInput = document.createElement('input');
		userInput.type = 'text';
		userInput.setAttribute('list', 'av-user-suggest');
		userInput.autocomplete = 'off';
		userInput.value = prefillUser || relevantUser() || '';
		var suggest = document.createElement('datalist');
		suggest.id = 'av-user-suggest';
		recentSel.addEventListener('change', function () {
			if (recentSel.value) { userInput.value = recentSel.value; }
		});
		recentEditors(5).then(function (names) {
			names.forEach(function (name) {
				var o = document.createElement('option');
				o.value = name;
				o.textContent = name;
				recentSel.appendChild(o);
				var d = document.createElement('option');
				d.value = name;
				suggest.appendChild(d);
			});
			if (!userInput.value && names[0]) { userInput.value = names[0]; }
		});
		bindUserSuggest(userInput, suggest);
		var articleInput = document.createElement('input');
		articleInput.type = 'text';
		articleInput.value = pageName().replace(/_/g, ' ');
		var sel = document.createElement('select');
		NOTICES.forEach(function (n) {
			var o = document.createElement('option');
			o.value = n.id;
			o.textContent = n.label + (n.leveled ? ' (1-4)' : '');
			sel.appendChild(o);
		});
		if (presetId) {
			var mapped = { vandalism: 'uw-vandalism', blanking: 'uw-delete', test: 'uw-test', unsourced: 'uw-unsourced', spam: 'uw-spam', npov: 'uw-biog' }[presetId];
			if (mapped) { sel.value = mapped; }
		}
		var level = document.createElement('select');
		[1, 2, 3, 4].forEach(function (n) {
			var o = document.createElement('option');
			o.value = String(n);
			o.textContent = 'Level ' + n;
			level.appendChild(o);
		});
		var extra = document.createElement('textarea');
		var hint = document.createElement('p');
		hint.style.fontSize = '12px';
		hint.textContent = 'Checking talk page for prior notices...';
		function currentTemplate() {
			var meta = NOTICES.filter(function (n) { return n.id === sel.value; })[0];
			return (meta && meta.leveled) ? sel.value + level.value : sel.value;
		}
		wrap.appendChild(field('Recent editors', recentSel));
		wrap.appendChild(field('User (type to search)', userInput));
		var signLine = document.createElement('p');
		signLine.style.fontSize = '12px';
		signLine.textContent = 'Will sign as ' + actingUser();
		wrap.appendChild(signLine);
		wrap.appendChild(suggest);
		wrap.appendChild(field('Related article', articleInput));
		wrap.appendChild(field('Notice', sel));
		wrap.appendChild(field('Level', level));
		wrap.appendChild(field('Extra note', extra));
		wrap.appendChild(hint);
		detectTalkLevel(userInput.value.trim()).then(function (n) {
			level.value = String(n);
			hint.textContent = 'Suggested level ' + n + ' from the talk page.';
		});
		openModal('Post a talk-page notice', wrap, [
			{ label: 'Cancel', onClick: closeModal },
			{
				label: 'Post notice', primary: true,
				onClick: function () {
					var user = userInput.value.trim();
					if (!user) { toast('Enter a username or IP.', 'err'); return; }
					var tpl = currentTemplate();
					toast('Posting...');
					appendTalkNotice(user, tpl, articleInput.value.trim(), extra.value.trim()).then(function (res) {
						if (res.error) { throw new Error(res.error.info || res.error.code); }
						toast('Notice posted.', 'ok');
						closeModal();
					}).catch(function (err) { toast(err.message || String(err), 'err'); });
				}
			}
		]);
	}
	function openPrefs() {
		var wrap = document.createElement('div');
		var rb = document.createElement('input');
		rb.type = 'checkbox';
		rb.checked = !!prefs.useRollback;
		var rbLab = document.createElement('label');
		rbLab.style.fontWeight = 'normal';
		rbLab.textContent = ' Prefer software rollback when available';
		var row = document.createElement('div');
		row.appendChild(rb);
		row.appendChild(rbLab);
		wrap.appendChild(row);
		openModal('AntiVandal preferences', wrap, [
			{ label: 'Close', onClick: closeModal },
			{
				label: 'Save', primary: true,
				onClick: function () {
					prefs.useRollback = rb.checked;
					savePrefs(prefs);
					closeModal();
					toast('Saved.', 'ok');
				}
			}
		]);
	}
	function goLatest() {
		location.href = mw.util.getUrl(pageName(), { diff: 'cur', oldid: 'prev' });
	}
	function reportAiv() {
		getLastRev().then(function (info) {
			var user = (info && info.latest && info.latest.user) || relevantUser() || '';
			var wrap = document.createElement('div');
			var ta = document.createElement('textarea');
			ta.value = '* {' + '{vandal|' + user + '}} - unconstructive editing on [[' +
				pageName().replace(/_/g, ' ') + ']]. ' + wikiSig();
			wrap.appendChild(ta);
			openModal('Report at AIV', wrap, [
				{ label: 'Cancel', onClick: closeModal },
				{
					label: 'Open AIV', primary: true,
					onClick: function () {
						window.open(mw.util.getUrl(CONF.aivPage, { action: 'edit', section: 'new' }), '_blank');
					}
				}
			]);
		});
	}
	function requestProtection() {
		var wrap = document.createElement('div');
		var art = document.createElement('input');
		art.type = 'text';
		art.value = pageName().replace(/_/g, ' ');
		var level = document.createElement('select');
		['semi-protection', 'extended confirmed', 'full protection', 'move protection', 'unprotection'].forEach(function (v) {
			var o = document.createElement('option');
			o.value = v;
			o.textContent = v;
			level.appendChild(o);
		});
		var reason = document.createElement('textarea');
		reason.placeholder = 'Why this page needs that level';
		var current = document.createElement('p');
		current.textContent = 'Checking current protection...';
		wrap.appendChild(field('Article', art));
		wrap.appendChild(field('Requested level', level));
		wrap.appendChild(field('Reason', reason));
		wrap.appendChild(current);
		api.get({
			action: 'query', titles: pageName(), prop: 'info', inprop: 'protection', formatversion: 2
		}).then(function (data) {
			var page = data.query && data.query.pages && data.query.pages[0];
			var prot = (page && page.protection) || [];
			current.textContent = prot.length
				? ('Current: ' + prot.map(function (p) { return p.type + '=' + p.level; }).join(', '))
				: 'Current: not protected';
		}).catch(function () { current.textContent = ''; });
		function pageTitle() { return art.value.trim(); }
		function requestBody() {
			var title = pageTitle();
			return '* {' + '{pagelinks|' + title + '}}\n' +
				reason.value.trim() + ' ' + wikiSig() + '\n';
		}
		function draft() {
			return '=== [[' + pageTitle() + ']] ===\n' + requestBody();
		}
		openModal('Request page protection', wrap, [
			{ label: 'Cancel', onClick: closeModal },
			{
				label: 'Open board with draft',
				onClick: function () {
					try {
						sessionStorage.setItem('antivandal-rfpp-subject', '[[' + art.value.trim() + ']]');
						sessionStorage.setItem('antivandal-rfpp-draft', requestBody());
					} catch (e) {}
					window.open(mw.util.getUrl(CONF.rfppPage, { action: 'edit', section: 'new' }), '_blank');
				}
			},
			{
				label: 'Post request', primary: true,
				onClick: function () {
					if (!art.value.trim() || !reason.value.trim()) {
						toast('Article and reason are required.', 'err');
						return;
					}
					toast('Posting request...');
					api.postWithEditToken({
						action: 'edit', title: CONF.rfppPage, appendtext: '\n' + draft(),
						summary: 'Protection request for [[' + art.value.trim() + ']] ([[Bharatpedia:AntiVandal|AntiVandal]])',
						formatversion: 2
					}).then(function (res) {
						if (res.error) { throw new Error(res.error.info || res.error.code); }
						toast('Request posted.', 'ok');
						closeModal();
					}).catch(function (err) { toast(err.message || String(err), 'err'); });
				}
			}
		]);
	}
	function openAdminReport(kind) {
		var title = kind === 'privacy' ? CONF.privacyPage : CONF.anPage;
		window.open(mw.util.getUrl(title, { action: 'edit', section: 'new' }), '_blank');
	}
	function openNewMessage(prefillUser) {
		var wrap = document.createElement('div');
		var userInput = document.createElement('input');
		userInput.type = 'text';
		userInput.value = prefillUser || relevantUser() || '';
		var subj = document.createElement('input');
		subj.type = 'text';
		var body = document.createElement('textarea');
		wrap.appendChild(field('User', userInput));
		var signLine2 = document.createElement('p');
		signLine2.style.fontSize = '12px';
		signLine2.textContent = 'Will sign as ' + actingUser();
		wrap.appendChild(signLine2);
		wrap.appendChild(field('Subject', subj));
		wrap.appendChild(field('Message', body));
		openModal('New talk-page message', wrap, [
			{ label: 'Cancel', onClick: closeModal },
			{
				label: 'Send', primary: true,
				onClick: function () {
					var user = userInput.value.trim();
					if (!user || !subj.value.trim()) { toast('Need a user and a subject.', 'err'); return; }
					var text = '\n\n== ' + subj.value.trim() + ' ==\n' + body.value + '\n' + wikiSig();
					toast('Sending...');
					api.postWithEditToken({
						action: 'edit', title: talkTitleFor(user), appendtext: text,
						summary: 'New message ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
					}).then(function (res) {
						if (res.error) { throw new Error(res.error.info || res.error.code); }
						toast('Message posted.', 'ok');
						closeModal();
					}).catch(function (err) { toast(err.message || String(err), 'err'); });
				}
			}
		]);
	}
	function welcomeTemplates() {
		return api.get({
			action: 'query', list: 'categorymembers',
			cmtitle: CONF.welcomeCat, cmnamespace: 10,
			cmtype: 'page', cmlimit: 50, formatversion: 2
		}).then(function (data) {
			var members = (data.query && data.query.categorymembers) || [];
			var out = members.map(function (m) {
				var title = (m.title || '').replace(/^Template:/i, '');
				return { title: title, label: title.replace(/_/g, ' ') };
			}).filter(function (x) { return x.title; });
			if (!out.length) {
				out = [
					{ title: 'Welcome', label: 'Welcome' },
					{ title: 'Welcometest', label: 'Welcome after a test edit' }
				];
			}
			return out;
		}).catch(function () {
			return [
				{ title: 'Welcome', label: 'Welcome' },
				{ title: 'Welcometest', label: 'Welcome after a test edit' }
			];
		});
	}
	function openWelcomeUser(prefillUser) {
		var wrap = document.createElement('div');
		var userInput = document.createElement('input');
		userInput.type = 'text';
		userInput.value = prefillUser || relevantUser() || '';
		var sel = document.createElement('select');
		var loading = document.createElement('option');
		loading.value = '';
		loading.textContent = 'Loading welcome templates...';
		sel.appendChild(loading);
		wrap.appendChild(field('User', userInput));
		var signLine = document.createElement('p');
		signLine.style.fontSize = '12px';
		signLine.textContent = 'Will sign as ' + actingUser();
		wrap.appendChild(signLine);
		wrap.appendChild(field('Welcome template', sel));
		welcomeTemplates().then(function (list) {
			sel.innerHTML = '';
			list.forEach(function (item) {
				var o = document.createElement('option');
				o.value = item.title;
				o.textContent = item.label;
				sel.appendChild(o);
			});
		});
		openModal('Welcome new user', wrap, [
			{ label: 'Cancel', onClick: closeModal },
			{
				label: 'Post welcome', primary: true,
				onClick: function () {
					var user = userInput.value.trim();
					var tpl = sel.value;
					if (!user || !tpl) { toast('Need a user and a template.', 'err'); return; }
					toast('Posting welcome...');
					api.postWithEditToken({
						action: 'edit', title: talkTitleFor(user),
						appendtext: '\n\n{' + '{subst:' + tpl + '}}\n' + wikiSig(),
						summary: 'Welcome ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2
					}).then(function (res) {
						if (res.error) { throw new Error(res.error.info || res.error.code); }
						toast('Welcome posted.', 'ok');
						closeModal();
					}).catch(function (err) { toast(err.message || String(err), 'err'); });
				}
			}
		]);
	}
	function openQuickTemplates(prefillUser) {
		openWelcomeUser(prefillUser);
	}
	function openMat() {
		toast('Loading history...');
		api.get({
			action: 'query', prop: 'revisions', titles: pageName(),
			rvlimit: 30, rvprop: 'ids|user|comment|timestamp', formatversion: 2
		}).then(function (data) {
			var page = data.query && data.query.pages && data.query.pages[0];
			var revs = (page && page.revisions) || [];
			var seen = {};
			var users = [];
			revs.forEach(function (r) {
				if (r.user && !seen[r.user]) { seen[r.user] = true; users.push(r.user); }
			});
			var wrap = document.createElement('div');
			var boxes = [];
			users.forEach(function (u) {
				var lab = document.createElement('label');
				lab.style.display = 'block';
				lab.style.fontWeight = 'normal';
				var c = document.createElement('input');
				c.type = 'checkbox';
				c.value = u;
				c.checked = true;
				lab.appendChild(c);
				lab.appendChild(document.createTextNode(' ' + u));
				wrap.appendChild(lab);
				boxes.push(c);
			});
			var sel = document.createElement('select');
			NOTICES.forEach(function (n) {
				var o = document.createElement('option');
				o.value = n.id + (n.leveled ? '1' : '');
				o.textContent = n.label;
				sel.appendChild(o);
			});
			wrap.appendChild(field('Notice', sel));
			openModal('Multiple notices', wrap, [
				{ label: 'Cancel', onClick: closeModal },
				{
					label: 'Post to checked', primary: true,
					onClick: function () {
						var chosen = boxes.filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
						if (!chosen.length) { toast('None selected.', 'err'); return; }
						if (!window.confirm('Post to ' + chosen.length + ' talk pages?')) { return; }
						var chain = Promise.resolve();
						chosen.forEach(function (u) {
							chain = chain.then(function () {
								return appendTalkNotice(u, sel.value, pageName().replace(/_/g, ' '), '');
							});
						});
						chain.then(function () { toast('Done.', 'ok'); closeModal(); })
							.catch(function (err) { toast(err.message || String(err), 'err'); });
					}
				}
			]);
		}).catch(function (err) { toast(err.message || String(err), 'err'); });
	}
	function openPendingReview() {
		api.get({ action: 'query', list: 'oldreviewedpages', orlimit: 15, formatversion: 2 }).then(function (data) {
			var pages = (data.query && data.query.oldreviewedpages) || [];
			if (!pages.length) { toast('No pending-review queue on this wiki.', 'err'); return; }
			var wrap = document.createElement('div');
			pages.forEach(function (p) {
				var a = document.createElement('a');
				a.href = mw.util.getUrl(p.title);
				a.textContent = p.title;
				a.style.display = 'block';
				wrap.appendChild(a);
			});
			openModal('Pending review', wrap, [{ label: 'Close', onClick: closeModal }]);
		}).catch(function () { toast('Pending changes is not available here.', 'err'); });
	}
	function bindContribsRollback() {
		if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') { return; }
		var list = document.querySelector('.mw-contributions-list');
		if (!list) { return; }
		list.querySelectorAll('li').forEach(function (li) {
			if (li.querySelector('.av-contrib-rev')) { return; }
			var diff = li.querySelector('a[href*="diff="]');
			if (!diff) { return; }
			var b = document.createElement('button');
			b.type = 'button';
			b.className = 'av-contrib-rev';
			b.textContent = 'revert';
			b.addEventListener('click', function (e) {
				e.preventDefault();
				var href = diff.getAttribute('href') || '';
				var titleMatch = href.match(/title=([^&]+)/) || href.match(/\/wiki\/([^?]+)/);
				var title = titleMatch ? decodeURIComponent(titleMatch[1].replace(/\+/g, ' ')) : null;
				var user = mw.config.get('wgRelevantUserName');
				if (!title || !user) { toast('Could not read that row.', 'err'); return; }
				if (!window.confirm('Revert latest edit by ' + user + ' on ' + title + '?')) { return; }
				api.get({
					action: 'query', prop: 'revisions', titles: title.replace(/ /g, '_'),
					rvlimit: 2, rvprop: 'ids|user', formatversion: 2
				}).then(function (data) {
					var page = data.query && data.query.pages && data.query.pages[0];
					var latest = page && page.revisions && page.revisions[0];
					if (!latest) { throw new Error('No revision'); }
					if (prefs.useRollback && hasRollback) {
						return api.postWithToken('rollback', {
							action: 'rollback', title: title, user: user,
							summary: 'Reverted unconstructive edit(s) ([[Bharatpedia:AntiVandal|AntiVandal]])',
							formatversion: 2
						});
					}
					var prev = page.revisions[1];
					if (!prev) { throw new Error('No earlier revision'); }
					return api.postWithEditToken({
						action: 'edit', title: title, undo: latest.revid, undoafter: prev.revid,
						summary: 'Reverted unconstructive edit(s) ([[Bharatpedia:AntiVandal|AntiVandal]])',
						formatversion: 2
					});
				}).then(function () { toast('Reverted.', 'ok'); })
					.catch(function (err) { toast(err.message || String(err), 'err'); });
			});
			li.appendChild(b);
		});
	}
	function bindUserContextMenu() {
		document.addEventListener('contextmenu', function (e) {
			if (!e.shiftKey) { return; }
			var a = e.target.closest ? e.target.closest('a') : null;
			if (!a || !a.href) { return; }
			var m = a.href.match(/[?&]title=User:([^&#]+)/i) || a.href.match(/\/wiki\/User:([^?#]+)/);
			if (!m) { return; }
			e.preventDefault();
			var user = decodeURIComponent(m[1]).replace(/_/g, ' ').split('/')[0];
			var ctx = document.getElementById('antivandal-ctx');
			if (!ctx) {
				ctx = document.createElement('div');
				ctx.id = 'antivandal-ctx';
				document.body.appendChild(ctx);
			}
			ctx.innerHTML = '';
			function item(label, fn) {
				var b = document.createElement('button');
				b.type = 'button';
				b.textContent = label;
				b.addEventListener('click', function () { ctx.style.display = 'none'; fn(); });
				ctx.appendChild(b);
			}
			item('User page', function () { location.href = mw.util.getUrl('User:' + user); });
			item('Talk page', function () { location.href = mw.util.getUrl('User talk:' + user); });
			item('Contributions', function () { location.href = mw.util.getUrl('Special:Contributions/' + user); });
			item('New message', function () { openNewMessage(user); });
			ctx.style.left = e.pageX + 'px';
			ctx.style.top = e.pageY + 'px';
			ctx.style.display = 'block';
		});
		document.addEventListener('click', function () {
			var ctx = document.getElementById('antivandal-ctx');
			if (ctx) { ctx.style.display = 'none'; }
			var more = document.getElementById('antivandal-more');
			if (more && more.style.display === 'block') { more.style.display = 'none'; }
		});
	}
	function neoBtn(iconClass, tip, fn) {
		var b = document.createElement('button');
		b.type = 'button';
		b.className = 'bpneo-title-btn';
		b.title = tip;
		b.setAttribute('aria-label', tip);
		var i = document.createElement('i');
		i.className = iconClass;
		b.appendChild(i);
		if (fn) {
			b.addEventListener('click', function (e) {
				e.preventDefault();
				e.stopPropagation();
				fn(b);
			});
		}
		return b;
	}
	function buildBar() {
		if (document.getElementById('antivandal-open')) { return true; }
		var row = document.querySelector('.bpneo-title-actions');
		if (!row) { return false; }
		var openBtn = neoBtn('ri-error-warning-fill', 'AntiVandal tools');
		openBtn.id = 'antivandal-open';
		openBtn.style.color = '#c62828';
		var tools = document.createElement('span');
		tools.id = 'antivandal-tools';
		tools.appendChild(neoBtn('ri-close-line', 'Close AntiVandal', function () {
			row.classList.remove('av-open');
		}));
		tools.appendChild(neoBtn('ri-arrow-go-back-line', 'Revert last editor', openRevertDialog));
		tools.appendChild(neoBtn('ri-history-line', 'Restore this revision', function () {
			var oldid = mw.util.getParamValue('oldid') || mw.config.get('wgRevisionId');
			if (!oldid) { toast('Open a specific revision first.', 'err'); return; }
			if (!window.confirm('Replace current text with revision ' + oldid + '?')) { return; }
			restoreRevision(oldid).then(function () {
				toast('Restored.', 'ok');
				setTimeout(function () { location.href = mw.util.getUrl(pageName()); }, 600);
			}).catch(function (err) { toast(err.message || String(err), 'err'); });
		}));
		tools.appendChild(neoBtn('ri-refresh-line', 'Latest revision', goLatest));
		tools.appendChild(neoBtn('ri-alarm-warning-line', 'Warn user', function () { openWarnDialog(); }));
		tools.appendChild(neoBtn('ri-user-add-line', 'Welcome new user', function () { openWelcomeUser(); }));
		tools.appendChild(neoBtn('ri-shield-keyhole-line', 'Request protection', requestProtection));
		var moreWrap = document.createElement('span');
		moreWrap.style.position = 'relative';
		var moreBtn = neoBtn('ri-more-2-fill', 'More');
		var menu = document.createElement('div');
		menu.id = 'antivandal-more';
		function menuItem(label, fn) {
			var mb = document.createElement('button');
			mb.type = 'button';
			mb.textContent = label;
			mb.addEventListener('click', function (e) {
				e.stopPropagation();
				menu.style.display = 'none';
				fn();
			});
			menu.appendChild(mb);
		}
		menuItem('New talk-page message', function () { openNewMessage(); });
		menuItem('Report at AIV', reportAiv);
		menuItem('Report to administrators', function () { openAdminReport('admin'); });
		menuItem('Privacy request', function () { openAdminReport('privacy'); });
		menuItem('Multiple notices', openMat);
		menuItem('Pending review', openPendingReview);
		menuItem('Preferences', openPrefs);
		moreBtn.addEventListener('click', function (e) {
			e.preventDefault();
			e.stopPropagation();
			menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
		});
		moreWrap.appendChild(moreBtn);
		moreWrap.appendChild(menu);
		tools.appendChild(moreWrap);
		openBtn.addEventListener('click', function (e) {
			e.preventDefault();
			e.stopPropagation();
			row.classList.add('av-open');
		});
		var slot = document.getElementById('antivandal-slot');
		var editLink = row.querySelector('a.bpneo-title-btn[title="Edit"]') ||
			row.querySelector('a.bpneo-title-btn[href*="action=edit"]');
		if (slot) { slot.replaceWith(openBtn); }
		else if (editLink) { row.insertBefore(openBtn, editLink); }
		else { row.insertBefore(openBtn, row.firstChild); }
		row.appendChild(tools);
		bindUserContextMenu();
		return true;
	}
	function init() {
		if (!mw.config.get('wgUserName')) { return; }
		injectCss();
		api = new mw.Api({ parameters: { formatversion: 2 } });
		var rights = mw.config.get('wgUserGroups') || [];
		hasRollback = rights.indexOf('rollbacker') !== -1 ||
			rights.indexOf('sysop') !== -1 ||
			rights.indexOf('steward') !== -1;
		if (!buildBar()) {
			var tries = 0;
			var t = setInterval(function () {
				tries += 1;
				if (buildBar() || tries > 25) { clearInterval(t); }
			}, 200);
		}
		try { bindContribsRollback(); } catch (e) {}
		fillEditDrafts();
	}
	function fillEditDrafts() {
		if (mw.config.get('wgAction') !== 'edit') { return; }
		var page = mw.config.get('wgPageName') || '';
		var box = document.getElementById('wpTextbox1');
		var subj = document.getElementById('wpSummary');
		if (!box) { return; }
		try {
			if (page === CONF.rfppPage.replace(/ /g, '_')) {
				var d = sessionStorage.getItem('antivandal-rfpp-draft');
				var s = sessionStorage.getItem('antivandal-rfpp-subject');
				if (d && !box.value.trim()) { box.value = d; }
				if (s && subj && !subj.value.trim()) { subj.value = s; }
				sessionStorage.removeItem('antivandal-rfpp-draft');
				sessionStorage.removeItem('antivandal-rfpp-subject');
			}
			if (page === CONF.aivPage.replace(/ /g, '_')) {
				var a = sessionStorage.getItem('antivandal-aiv-draft');
				if (a && !box.value.trim()) { box.value = a; }
				sessionStorage.removeItem('antivandal-aiv-draft');
			}
		} catch (e) {}
	}
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', function () {
			mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(init);
		});
	} else {
		mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(init);
	}
}());