Move some options to drop down |
No edit summary |
||
| (12 intermediate revisions by the same user not shown) | |||
| Line 11: | Line 11: | ||
var RK = (window.AntiVandal = window.AntiVandal || {}); | var RK = (window.AntiVandal = window.AntiVandal || {}); | ||
RK.loaded = true; | RK.loaded = true; | ||
RK.version = '1.1. | RK.version = '1.1.2'; | ||
var CONF = { | var CONF = { | ||
storageKey: 'antivandal-prefs-v1', | storageKey: 'antivandal-prefs-v1', | ||
| Line 20: | Line 20: | ||
anPage: 'Bharatpedia:Administrators noticeboard', | anPage: 'Bharatpedia:Administrators noticeboard', | ||
privacyPage: 'Bharatpedia:Requests for oversight', | privacyPage: 'Bharatpedia:Requests for oversight', | ||
docsPage: 'Bharatpedia:AntiVandal' | docsPage: 'Bharatpedia:AntiVandal', | ||
welcomeCat: 'Category:Welcome templates' | |||
}; | }; | ||
var PRESETS = [ | var PRESETS = [ | ||
| Line 86: | Line 87: | ||
if (!user) { return null; } | if (!user) { return null; } | ||
return 'User talk:' + user; | 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() { | function injectCss() { | ||
| Line 105: | Line 115: | ||
'padding:8px 12px;cursor:pointer;font:13px sans-serif;}', | 'padding:8px 12px;cursor:pointer;font:13px sans-serif;}', | ||
'#antivandal-ctx button:hover{background:#eaecf0;}', | '#antivandal-ctx button:hover{background:#eaecf0;}', | ||
'.rk-modal-bg{position:fixed;inset:0;background:rgba( | '.rk-modal-bg{position:fixed;inset:0;background:rgba(17,17,17,.4);z-index:1100;display:flex;', | ||
'align-items: | 'align-items:center;justify-content:center;padding:16px;}', | ||
'.rk-modal{background:#fff;color:#1b1b1b;max-width: | '.rk-modal{background:#fff;color:#1b1b1b;max-width:480px;width:100%;border-radius:10px;', | ||
'box-shadow:0 | 'border-top:3px solid #c62828;box-shadow:0 8px 28px rgba(0,0,0,.2);overflow:hidden;}', | ||
'.rk-modal h2{margin:0;padding: | '.rk-modal .rk-head{display:flex;align-items:center;justify-content:space-between;', | ||
'.rk-modal .rk-body{padding:14px | 'padding:10px 14px;border-bottom:1px solid #eee;}', | ||
'.rk-modal label{display:block;font-weight:600;margin:10px 0 | '.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: | 'padding:7px 8px;font:13px sans-serif;}', | ||
'.rk-modal textarea{min-height: | '.rk-modal textarea{min-height:70px;}', | ||
'.rk-modal .rk-actions{display:flex;justify-content:flex-end;gap:8px;padding:10px | '.rk-modal .rk-actions{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;}', | ||
'.rk-modal .rk-actions button{padding: | '.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:# | '.rk-modal .rk-actions .rk-primary{background:#c62828;color:#fff;border-color:#c62828;}', | ||
'.rk-toast{position:fixed;right:16px;bottom:16px;z-index:1200;background:#1f2a22;color:#fff;', | '.rk-toast{position:fixed;right:16px;bottom:16px;z-index:1200;background:#1f2a22;color:#fff;', | ||
'padding:10px 14px;border-radius:6px;max-width:360px;display:none;}', | 'padding:10px 14px;border-radius:6px;max-width:360px;display:none;}', | ||
| Line 145: | Line 161: | ||
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( | 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 184: | Line 210: | ||
}); | }); | ||
} | } | ||
function revertWithSummary(summary, userToRevert) { | 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) { | return getLastRev().then(function (info) { | ||
if (!info) { throw new Error('Could not read this page history.'); } | if (!info) { throw new Error('Could not read this page history.'); } | ||
var | var targetUser = userToRevert || info.latest.user; | ||
function errCode(err) { | |||
if (prefs.useRollback && hasRollback) { | return (err && (err.code || err.name)) || (String(err.message || err)); | ||
} | |||
if (allByUser && prefs.useRollback && hasRollback) { | |||
return api.postWithToken('rollback', { | return api.postWithToken('rollback', { | ||
action: 'rollback', title: pageName(), user: targetUser, | action: 'rollback', title: pageName(), user: targetUser, | ||
summary: summary + ' ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2 | summary: summary + ' ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2 | ||
}).then(function (res) { | }).then(function (res) { | ||
if (res.error) { throw | if (res.error) { throw res.error; } | ||
return { method: 'rollback', data: res }; | 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 ( | if (allByUser) { | ||
return revertAllByUser(summary, targetUser); | |||
} | |||
return doUndo(info, summary); | |||
} | |||
}); | }); | ||
} | } | ||
| Line 229: | Line 304: | ||
if (article) { subst += '|1=' + article.replace(/_/g, ' '); } | if (article) { subst += '|1=' + article.replace(/_/g, ' '); } | ||
if (extra) { subst += '|2=' + extra; } | if (extra) { subst += '|2=' + extra; } | ||
subst += '}} | subst += '}}\n' + '~~' + '~~'; | ||
return api.postWithEditToken({ | return api.postWithEditToken({ | ||
action: 'edit', title: talk, appendtext: '\n\n' + subst, | action: 'edit', title: talk, appendtext: '\n\n' + subst, | ||
| Line 276: | Line 351: | ||
alsoWarn.type = 'checkbox'; | alsoWarn.type = 'checkbox'; | ||
var warnLab = document.createElement('label'); | var warnLab = document.createElement('label'); | ||
warnLab. | warnLab.className = 'rk-check'; | ||
warnLab. | 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'); | var row = document.createElement('div'); | ||
row. | row.className = 'rk-checkrow'; | ||
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)); | ||
| Line 295: | Line 378: | ||
if (!who) { toast('Name the editor.', 'err'); return; } | if (!who) { toast('Name the editor.', 'err'); return; } | ||
toast('Reverting...'); | toast('Reverting...'); | ||
revertWithSummary(summary, who).then(function (res) { | revertWithSummary(summary, who, allBy.checked).then(function (res) { | ||
toast('Reverted with ' + res.method + '.', 'ok'); | toast('Reverted with ' + res.method + '.', 'ok'); | ||
closeModal(); | closeModal(); | ||
| Line 305: | Line 388: | ||
]); | ]); | ||
}).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) { | 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 340: | Line 493: | ||
return (meta && meta.leveled) ? sel.value + level.value : sel.value; | return (meta && meta.leveled) ? sel.value + level.value : sel.value; | ||
} | } | ||
wrap.appendChild(field('User', userInput)); | 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('Related article', articleInput)); | ||
wrap.appendChild(field('Notice', sel)); | wrap.appendChild(field('Notice', sel)); | ||
| Line 402: | Line 561: | ||
var ta = document.createElement('textarea'); | var ta = document.createElement('textarea'); | ||
ta.value = '* {' + '{vandal|' + user + '}} - unconstructive editing on [[' + | ta.value = '* {' + '{vandal|' + user + '}} - unconstructive editing on [[' + | ||
pageName().replace(/_/g, ' ') + ']]. | pageName().replace(/_/g, ' ') + ']]. ' + wikiSig(); | ||
wrap.appendChild(ta); | wrap.appendChild(ta); | ||
openModal('Report at AIV', wrap, [ | openModal('Report at AIV', wrap, [ | ||
| Line 416: | Line 575: | ||
} | } | ||
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) { | function openAdminReport(kind) { | ||
| Line 431: | Line 658: | ||
var body = document.createElement('textarea'); | var body = document.createElement('textarea'); | ||
wrap.appendChild(field('User', userInput)); | wrap.appendChild(field('User', userInput)); | ||
var signLine2 = document.createElement('p'); | |||
signLine2.style.fontSize = '12px'; | |||
signLine2.textContent = 'Will sign as ' + actingUser(); | |||
wrap.appendChild(signLine2); | |||
wrap.appendChild(field('Subject', subj)); | wrap.appendChild(field('Subject', subj)); | ||
wrap.appendChild(field('Message', body)); | wrap.appendChild(field('Message', body)); | ||
| Line 440: | Line 671: | ||
var user = userInput.value.trim(); | var user = userInput.value.trim(); | ||
if (!user || !subj.value.trim()) { toast('Need a user and a subject.', 'err'); return; } | 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 | var text = '\n\n== ' + subj.value.trim() + ' ==\n' + body.value + '\n' + wikiSig(); | ||
toast('Sending...'); | toast('Sending...'); | ||
api.postWithEditToken({ | api.postWithEditToken({ | ||
| Line 454: | Line 685: | ||
]); | ]); | ||
} | } | ||
function welcomeTemplates() { | |||
return api.get({ | |||
function | 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) { | |||
return; | 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 | function openWelcomeUser(prefillUser) { | ||
var wrap = document.createElement('div'); | var wrap = document.createElement('div'); | ||
var userInput = document.createElement('input'); | var userInput = document.createElement('input'); | ||
userInput.type = 'text'; | userInput.type = 'text'; | ||
userInput.value = prefillUser || relevantUser() || ''; | userInput.value = prefillUser || relevantUser() || ''; | ||
wrap.appendChild(field('User | var sel = document.createElement('select'); | ||
var loading = document.createElement('option'); | |||
loading.value = ''; | |||
var | 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 user = userInput.value.trim(); | ||
if (!user) { toast(' | var tpl = sel.value; | ||
if (!user || !tpl) { toast('Need a user and a template.', 'err'); return; } | |||
toast('Posting welcome...'); | |||
api.postWithEditToken({ | api.postWithEditToken({ | ||
action: 'edit', title: talkTitleFor(user), | action: 'edit', title: talkTitleFor(user), | ||
appendtext: '\n\n' + | appendtext: '\n\n{' + '{subst:' + tpl + '}}\n' + wikiSig(), | ||
summary: ' | summary: 'Welcome ([[Bharatpedia:AntiVandal|AntiVandal]])', formatversion: 2 | ||
}).then(function (res) { | }).then(function (res) { | ||
if (res.error) { throw new Error(res.error.info || res.error.code); } | if (res.error) { throw new Error(res.error.info || res.error.code); } | ||
toast(' | toast('Welcome posted.', 'ok'); | ||
closeModal(); | closeModal(); | ||
}).catch(function (err) { toast(err.message || String(err), 'err'); }); | }).catch(function (err) { toast(err.message || String(err), 'err'); }); | ||
} | } | ||
} | |||
} | ]); | ||
} | } | ||
function | function openQuickTemplates(prefillUser) { | ||
openWelcomeUser(prefillUser); | |||
} | } | ||
function openMat() { | function openMat() { | ||
| Line 703: | Line 906: | ||
item('Contributions', function () { location.href = mw.util.getUrl('Special:Contributions/' + user); }); | item('Contributions', function () { location.href = mw.util.getUrl('Special:Contributions/' + user); }); | ||
item('New message', function () { openNewMessage(user); }); | item('New message', function () { openNewMessage(user); }); | ||
ctx.style.left = e.pageX + 'px'; | ctx.style.left = e.pageX + 'px'; | ||
ctx.style.top = e.pageY + 'px'; | ctx.style.top = e.pageY + 'px'; | ||
| Line 757: | Line 958: | ||
})); | })); | ||
tools.appendChild(neoBtn('ri-refresh-line', 'Latest revision', goLatest)); | 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- | |||
tools.appendChild(neoBtn('ri-shield-keyhole-line', 'Request protection', requestProtection)); | tools.appendChild(neoBtn('ri-shield-keyhole-line', 'Request protection', requestProtection)); | ||
var moreWrap = document.createElement('span'); | var moreWrap = document.createElement('span'); | ||
| Line 777: | Line 977: | ||
menu.appendChild(mb); | menu.appendChild(mb); | ||
} | } | ||
menuItem('New talk-page message', function () { openNewMessage(); }); | menuItem('New talk-page message', function () { openNewMessage(); }); | ||
menuItem('Report at AIV', reportAiv); | menuItem('Report at AIV', reportAiv); | ||
menuItem('Report to administrators', function () { openAdminReport('admin'); }); | menuItem('Report to administrators', function () { openAdminReport('admin'); }); | ||
| Line 825: | Line 1,023: | ||
} | } | ||
try { bindContribsRollback(); } catch (e) {} | 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') { | ||
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);
}
}());