User:WikiDwarf/AntiVandal.js: Difference between revisions

Add
 
No edit summary
 
(19 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.0.0';
RK.version = '1.1.2';
 
var CONF = {
var CONF = {
storageKey: 'antivandal-prefs-v1',
storageKey: 'antivandal-prefs-v1',
packKey: 'antivandal-packs-v1',
tag: 'AntiVandal',
tag: 'AntiVandal',
aivPage: 'Bharatpedia:Administrator intervention against vandalism',
aivPage: 'Bharatpedia:Administrator intervention against vandalism',
rfppPage: 'Bharatpedia:Requests for page protection',
rfppPage: 'Bharatpedia:Requests for page protection',
docsPage: 'Bharatpedia:AntiVandal'
anPage: 'Bharatpedia:Administrators noticeboard',
privacyPage: 'Bharatpedia:Requests for oversight',
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 40: 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 57: 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 74: 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 90: 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 97: 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-bar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:6px 10px;',
'#antivandal-open.bpneo-title-btn{color:#c62828;}',
'background:#1f2a22;color:#f4efe4;font:13px/1.3 sans-serif;z-index:200;',
'#antivandal-tools{display:none;align-items:center;gap:inherit;}',
'border-bottom:3px solid #c9a227;}',
'.bpneo-title-actions.av-open > :not(#antivandal-tools){display:none !important;}',
'#antivandal-bar.rk-bottom{position:sticky;bottom:0;border-bottom:0;border-top:3px solid #c9a227;}',
'.bpneo-title-actions.av-open #antivandal-tools{display:inline-flex;}',
'#antivandal-bar .rk-brand{font-weight:700;letter-spacing:.04em;margin-right:8px;color:#e8c547;}',
'#antivandal-tools .bpneo-title-btn.rk-on{color:#2a4a33;}',
'#antivandal-bar button{background:#2e3d32;color:#f4efe4;border:1px solid #4a5d4e;border-radius:4px;',
'#antivandal-more{position:absolute;right:0;top:36px;background:#fff;border:1px solid #a2a9b1;',
'padding:6px 10px;cursor:pointer;font:inherit;}',
'box-shadow:0 2px 8px rgba(0,0,0,.18);min-width:220px;z-index:50;display:none;padding:4px 0;}',
'#antivandal-bar button:hover{background:#3c5142;}',
'#antivandal-more button{display:block;width:100%;text-align:left;border:0;background:none;',
'#antivandal-bar button.rk-warn{background:#6b2a22;border-color:#8a3a30;}',
'padding:8px 12px;cursor:pointer;font:13px sans-serif;}',
'#antivandal-bar button.rk-go{background:#2a4a33;border-color:#3e6b48;}',
'#antivandal-more button:hover{background:#eaecf0;}',
'.rk-modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1100;display:flex;',
'#antivandal-ctx{position:fixed;z-index:1300;background:#fff;border:1px solid #a2a9b1;',
'align-items:flex-start;justify-content:center;padding:8vh 16px 16px;}',
'box-shadow:0 2px 8px rgba(0,0,0,.2);min-width:180px;padding:4px 0;display:none;}',
'.rk-modal{background:#fff;color:#1b1b1b;max-width:560px;width:100%;border-radius:8px;',
'#antivandal-ctx button{display:block;width:100%;text-align:left;border:0;background:none;',
'box-shadow:0 12px 40px rgba(0,0,0,.28);overflow:hidden;}',
'padding:8px 12px;cursor:pointer;font:13px sans-serif;}',
'.rk-modal h2{margin:0;padding:12px 16px;background:#1f2a22;color:#f4efe4;font-size:16px;}',
'#antivandal-ctx button:hover{background:#eaecf0;}',
'.rk-modal .rk-body{padding:14px 16px;}',
'.rk-modal-bg{position:fixed;inset:0;background:rgba(17,17,17,.4);z-index:1100;display:flex;',
'.rk-modal label{display:block;font-weight:600;margin:10px 0 4px;}',
'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;',
'.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;}',
'.rk-toast-err{background:#6b2a22;}',
'.rk-toast-err{background:#6b2a22;}',
'.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;',
'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('');
].join('');
var style = document.createElement('style');
var style = document.createElement('style');
Line 159: 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 172: 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 192: 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 201: 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 212: 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(),
rvlimit: 2,
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];
if (!page || page.missing || !page.revisions || !page.revisions.length) {
if (!page || page.missing || !page.revisions || !page.revisions.length) { return null; }
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 + '.');
}
}
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]]) 12:11, 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 341: 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 406: 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 430: 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));
 
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, [
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 488: 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]]) 12:11, 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 540: 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 addDiffRestore() {
var wrap = document.createElement('div');
if (mw.config.get('wgDiffNewId') || mw.config.get('wgDiffOldId') ||
var userInput = document.createElement('input');
mw.config.get('wgAction') === 'history') {
userInput.type = 'text';
var oldid = mw.util.getParamValue('oldid') || mw.config.get('wgRevisionId');
userInput.value = prefillUser || relevantUser() || '';
if (!oldid) {
var subj = document.createElement('input');
return;
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'); });
}
}
}
var bar = document.getElementById('antivandal-bar');
]);
if (!bar) {
}
return;
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' }
];
}
}
var b = document.createElement('button');
return out;
b.type = 'button';
}).catch(function () {
b.textContent = 'Restore this revision';
return [
b.addEventListener('click', function () {
{ title: 'Welcome', label: 'Welcome' },
if (!window.confirm('Replace the current page text with revision ' + oldid + '?')) {
{ title: 'Welcometest', label: 'Welcome after a test edit' }
return;
];
});
}
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'); });
}
}
toast('Restoring…');
}
restoreRevision(oldid).then(function (res) {
]);
if (res.error) {
}
throw new Error(res.error.info || res.error.code);
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'); });
}
}
toast('Revision restored.', 'ok');
}
setTimeout(function () { location.href = mw.util.getUrl(pageName()); }, 600);
]);
}).catch(function (err) {
}).catch(function (err) { toast(err.message || String(err), '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);
});
});
bar.appendChild(b);
openModal('Pending review', wrap, [{ label: 'Close', onClick: closeModal }]);
}
}).catch(function () { toast('Pending changes is not available here.', 'err'); });
}
}
 
function bindContribsRollback() {
function buildBar() {
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') { return; }
if (document.getElementById('antivandal-bar')) {
var list = document.querySelector('.mw-contributions-list');
return;
if (!list) { return; }
}
list.querySelectorAll('li').forEach(function (li) {
if (!mw.config.get('wgIsArticle') &&
if (li.querySelector('.av-contrib-rev')) { return; }
mw.config.get('wgAction') !== 'view' &&
var diff = li.querySelector('a[href*="diff="]');
mw.config.get('wgAction') !== 'history') {
if (!diff) { return; }
return;
}
if (mw.config.get('wgCanonicalNamespace') === 'Special') {
return;
}
 
var bar = document.createElement('div');
bar.id = 'antivandal-bar';
if (prefs.barPosition === 'bottom') {
bar.className = 'rk-bottom';
}
 
var brand = document.createElement('span');
brand.className = 'rk-brand';
brand.textContent = 'AntiVandal';
bar.appendChild(brand);
 
function addBtn(label, cls, fn) {
var b = document.createElement('button');
var b = document.createElement('button');
b.type = 'button';
b.type = 'button';
b.textContent = label;
b.className = 'av-contrib-rev';
if (cls) {
b.textContent = 'revert';
b.className = cls;
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);
}
}
b.addEventListener('click', fn);
ctx.innerHTML = '';
bar.appendChild(b);
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;
addBtn('Revert', 'rk-go', openRevertDialog);
}
addBtn('Notice', 'rk-warn', function () { openWarnDialog(); });
function buildBar() {
addBtn('Latest diff', null, goLatest);
if (document.getElementById('antivandal-open')) { return true; }
addBtn('AIV', null, reportAiv);
var row = document.querySelector('.bpneo-title-actions');
addBtn('Protect request', null, requestProtection);
if (!row) { return false; }
addBtn('Prefs', null, openPrefs);
var openBtn = neoBtn('ri-error-warning-fill', 'AntiVandal tools');
 
openBtn.id = 'antivandal-open';
var heading = document.getElementById('firstHeading') || document.getElementById('content');
openBtn.style.color = '#c62828';
if (prefs.barPosition === 'bottom') {
var tools = document.createElement('span');
document.body.appendChild(bar);
tools.id = 'antivandal-tools';
} else if (heading && heading.parentNode) {
tools.appendChild(neoBtn('ri-close-line', 'Close AntiVandal', function () {
heading.parentNode.insertBefore(bar, heading.nextSibling);
row.classList.remove('av-open');
} else {
}));
document.body.insertBefore(bar, document.body.firstChild);
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(); });
addDiffRestore();
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() {
function init() {
if (!mw.config.get('wgUserName')) {
if (!mw.config.get('wgUserName')) { return; }
return;
}
injectCss();
injectCss();
api = new mw.Api({ parameters: { formatversion: 2 } });
api = new mw.Api({ parameters: { formatversion: 2 } });
Line 642: Line 1,015:
rights.indexOf('sysop') !== -1 ||
rights.indexOf('sysop') !== -1 ||
rights.indexOf('steward') !== -1;
rights.indexOf('steward') !== -1;
buildBar();
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') {
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);
	}
}());