Foydalanuvchi:Ingenuity/AntiVandal.js
Eʼtibor bering: Oʻzgartirishlaringizni koʻrish uchun, yangi moslamalaringizning saqlashdan keyin, brauzer keshini tozalash kerak:
Mozilla / Firefox: Ctrl+Shift+R, IE: Ctrl+F5, Safari: Cmd+Shift+R, Konqueror: F5, Opera: Tools → Preferences orqali keshni tozalang.
/* <nowiki> */
// list of common warnings
const warnings = {
"Vandalizm": {
templates: [
"subst:uw-vandalism1",
"subst:uw-vandalism2",
"subst:uw-vandalism3",
"subst:uw-vandalism4",
"subst:uw-vandalism4im"
],
label: "vandalism",
desc: "Vandalizm uchun birlamchi ogohlantirish."
},
"Buzish": {
templates: [
"subst:uw-disruptive1",
"subst:uw-disruptive2",
"subst:uw-disruptive3",
"subst:uw-generic4"
],
label: "disruptive editing",
desc: "Buzg‘unchi tahrir uchun birlamchi ogohlantirish (har doim ham vandalizm emas)"
},
"O‘chirib tashlash": {
templates: [
"subst:uw-delete1",
"subst:uw-delete2",
"subst:uw-delete3",
"subst:uw-delete4",
"subst:uw-delete4im"
],
label: "unexplained deletion",
desc: "Agar foydalanuvchi maqolaning bir qismini o'chirishni tushuntirmasa ishlatiladi."
},
"Reklama": {
templates: [
"subst:uw-advert1",
"subst:uw-advert2",
"subst:uw-advert3",
"subst:uw-advert4",
"subst:uw-advert4im"
],
label: "advertising or promotion",
desc: "Maqolaga reklama mazmunini qo‘shish."
},
"Spam havolalari": {
templates: [
"subst:uw-spam1",
"subst:uw-spam2",
"subst:uw-spam3",
"subst:uw-spam4",
"subst:uw-spam4im"
],
label: "adding inappropriate links",
desc: "Spam deb hisoblanishi mumkin bo‘lgan havolalarni qo‘shish."
},
"Manbasiz": {
templates: [
"subst:uw-unsourced1",
"subst:uw-unsourced2",
"subst:uw-unsourced3",
"subst:uw-unsourced4"
],
label: "adding unsourced content",
desc: "Maqolaga manbasiz, ehtimol tuhmat qiluvchi kontent qo‘shish."
},
"Test sahifa sifatida tahrirlash": {
templates: [
"subst:uw-test1",
"subst:uw-test2",
"subst:uw-test3",
"subst:uw-vandalism4"
],
label: "making editing tests",
desc: "Maqolalarni tahrirlash testlarini o‘tkazish."
},
"Izoh": {
templates: [
"subst:uw-talkinarticle1",
"subst:uw-talkinarticle2",
"subst:uw-talkinarticle3",
"subst:uw-generic4"
],
label: "adding commentary",
desc: "Maqolalarga fikr yoki sharh qo‘shish."
},
"POV": {
templates: [
"subst:uw-npov1",
"subst:uw-npov2",
"subst:uw-npov3",
"subst:uw-npov4"
],
label: "adding non-neutral content",
desc: "Neytral nuqtai nazar siyosatini buzadigan kontent qo‘shish."
},
"Xatolar": {
templates: [
"subst:uw-error1",
"subst:uw-error2",
"subst:uw-error3",
"subst:uw-error4"
],
label: "adding deliberate errors to articles",
desc: "Maqolalarga ataylab xatolar qo‘shish."
},
"Egalik": {
templates: [
"subst:uw-own1",
"subst:uw-own2",
"subst:uw-own3",
"subst:uw-own4"
],
label: "assuming ownership of articles",
desc: "Maqolalarga egalik qilish."
},
"Manbasiz (BLP)": {
templates: [
"subst:uw-biog1",
"subst:uw-biog2",
"subst:uw-biog3",
"subst:uw-biog4",
"subst:uw-biog4im"
],
label: "adding unsourced content to biographies of living persons",
desc: "Tirik odamlarning tarjimai holiga manbasiz tarkib qo‘shish."
},
"Suhbat": {
templates: [
"subst:uw-chat1",
"subst:uw-chat2",
"subst:uw-chat3",
"subst:uw-chat4"
],
label: "conversation in article talk space",
desc: "Noto‘g‘ri muhokama uchun maqola muhokama sahifalaridan foydalanish."
},
"Tasvir vandalizmi": {
templates: [
"subst:uw-image1",
"subst:uw-image2",
"subst:uw-image3",
"subst:uw-image4"
],
label: "image vandalism",
desc: "Tasvir vandalizmi."
},
"AfDni olib tashlash": {
templates: [
"subst:uw-afd1",
"subst:uw-afd2",
"subst:uw-afd3",
"subst:uw-afd4"
],
label: "removing AfD templates or other users' comments from AfD discussions",
desc: "AfD shablonlarini yoki boshqa foydalanuvchilarning sharhlarini AfD muhokamalaridan olib tashlash."
},
"Hazillar": {
templates: [
"subst:uw-joke1",
"subst:uw-joke2",
"subst:uw-joke3",
"subst:uw-joke4",
"subst:uw-joke4im"
],
label: "adding inappropriate humor",
desc: "Maqolalarga nomaqbul hazil qo‘shish."
},
"Shaxsiy hujumlar": {
templates: [
"subst:uw-npa1",
"subst:uw-npa2",
"subst:uw-npa3",
"subst:uw-npa4",
"subst:uw-npa4im"
],
label: "personal attacks",
desc: "Boshqa foydalanuvchiga shaxsiy hujumlar."
}
};
const namespaceList = [
{ name: "Main", id: 0, category: "main" },
{ name: "User", id: 2, category: "user" },
{ name: "Project", id: 4, category: "wikipedia" },
{ name: "File", id: 6, category: "other" },
{ name: "MediaWiki", id: 8, category: "other" },
{ name: "Template", id: 10, category: "other" },
{ name: "Help", id: 12, category: "other" },
{ name: "Category", id: 14, category: "other" },
{ name: "Portal", id: 100, category: "other" },
{ name: "Draft", id: 118, category: "draft" },
{ name: "Talk", id: 1, category: "main" },
{ name: "User talk", id: 3, category: "user" },
{ name: "Project talk", id: 5, category: "wikipedia" },
{ name: "File talk", id: 7, category: "other" },
{ name: "MediaWiki talk", id: 9, category: "other" },
{ name: "Template talk", id: 11, category: "other" },
{ name: "Help talk", id: 13, category: "other" },
{ name: "Category talk", id: 15, category: "other" },
{ name: "Portal talk", id: 101, category: "other" },
{ name: "Draft talk", id: 119, category: "draft" }
];
const rollbackAllowed = mw.config.values.wgUserGroups.includes("sysop") || mw.config.values.wgUserGroups.includes("rollbacker");
let currentId = null, currentEdit = null, rcstart = null, messageTimeout = null;
let lastRevId = 0;
let queueItems = [], pastQueueItems = [];
const antiVandalApi = new mw.Api();
let currentAIVReports = [];
let reportNum = 0;
// shorthand for querySelector
const qs = document.querySelector.bind(document);
async function runAntiVandal() {
// if the url isn't the run location, add a link to the page instead
if (!location.href.includes("Vikipediya:AntiVandal/run")) {
addAntiVandalLink();
return;
}
const username = mw.config.values.wgUserName;
const userGroups = mw.config.values.wgUserGroups;
let allowed = false;
document.head.innerHTML = `
<style>
a {
color: black;
}
body, html {
display: flex;
align-items: center;
justify-content: center;
height: 80%;
font-family: Arial, Helvetica, sans-serif;
}
.start {
text-align: center;
background: blue;
cursor: pointer;
padding: 15px;
color: white;
border: none;
}
.start[disabled] {
background: grey;
cursor: not-allowed;
}
</style>
<title>AntiVandal</title>
`;
document.body.innerHTML = `
<div class="container" style="text-align: center">
<h1 style="margin-bottom: 5px">AntiVandal</h1>
<p style="margin-top: 0"><a target="_blank" href="https://uz.wikipedia.org/wiki/User:Ingenuity">Ingenuity</a> tomonidan yaratilgan</p>
<div style="text-align: left">
<p>AntiVandalni ishga tushirish quyidagilardan birini talab qiladi:</p>
<ul>
<li class="rights"><a target="_blank" href="/wiki/Vikipediya:Eski holiga qaytaruvchilar">Eski holiga qaytaruvchilar</a> yoki <a target="_blank" href="/wiki/Vikipediya:Administratorlar">administratorlar </a> foydalanuvchi huquqlari</li>
<li class="whitelist">AntiVandal ro‘yxatiga kiritilgan foydalanuvchilar</li>
</ul>
</div>
<button class="start" disabled onclick="startInterface()">AntiVandal dasturini ishga tushirish</button>
</div>
`;
if (userGroups.includes("rollbacker") || userGroups.includes("sysop")) {
qs(".rights").style.color = "green";
allowed = true;
} else {
qs(".rights").style.color = "red";
}
let users = [];
try {
const options = {
action: "query",
format: "json",
prop: "revisions",
titles: "User:Ingenuity/AntiVandalWhitelist.json",
formatversion: 2,
rvprop: "content",
rvslots: "*"
};
const content = (await antiVandalApi.get(options)).query.pages[0].revisions[0].slots.main.content;
users = JSON.parse(content).users;
} catch (err) {}
if (users.includes(username)) {
qs(".whitelist").style.color = "green";
allowed = true;
} else {
qs(".whitelist").style.color = "red";
}
qs(".start").disabled = !allowed;
}
function startInterface() {
createInterface();
// add listener for hotkey presses
window.addEventListener("keyup", (event) => {
if (event.ctrlKey) {
return;
}
if (document.activeElement.nodeName.toLowerCase() === "input") {
return;
}
if (event.key.toLowerCase() === antiVandalOptions.controls.queueBack) {
if (pastQueueItems.length === 0) {
return;
}
queueItems.unshift(pastQueueItems.pop());
renderQueue();
return;
}
if (event.key.toLowerCase() === antiVandalOptions.controls.queueForward) {
shiftQueue();
return;
}
if (event.key.toLowerCase() === antiVandalOptions.controls.continueToNext) {
shiftQueue();
return;
}
if (event.key.toLowerCase() === antiVandalOptions.controls.markAsVandalism) {
revert({ id: currentId, template: "auto" });
shiftQueue();
return;
}
if (event.key.toLowerCase() === antiVandalOptions.controls.rollback) {
revert({ id: currentId }, true);
shiftQueue();
return;
}
});
// prevent spacebar from scrolling page
window.addEventListener("keydown", (event) => {
if (event.key === " " && event.target === document.body) {
event.preventDefault();
}
});
qs("#queueDelete").onclick = () => {
queueItems = [];
renderQueue();
}
qs("#queueBack").onclick = () => {
if (pastQueueItems.length === 0) {
return;
}
queueItems.unshift(pastQueueItems.pop());
renderQueue();
}
qs("#queueForward").onclick = () => {
shiftQueue();
return;
}
qs(".settingsClose").onclick = hideSettings;
qs(".settingsCancel").onclick = hideSettings;
qs(".settingsSave").onclick = saveSettings;
qs("#settings").onclick = showSettings;
qs("input[name=minORES]").oninput = function() {
qs("label[for=minORES]").innerText = this.value;
}
const diffToolbarItems = Array.prototype.slice.call(document.querySelectorAll(".diffActionItem"));
const warningsContainer = qs(".diffWarningsContainer");
[...document.querySelectorAll(".diffActionBox")].forEach(e => {
e.onclick = (event) => event.stopPropagation()
});
qs("#report-reason").onfocus = () => qs("#other-reason").checked = true;
qs(".report-button").onclick = reportCurrentUser;
qs("#revert-button").onclick = () => {
const summary = qs("#revert-summary").value;
revert({ id: currentId, summary: summary }, true);
shiftQueue();
for (let e of [...document.querySelectorAll(".diffActionBox")]) {
e.style.display = "none";
}
for (let e of [...document.querySelectorAll(".diffActionItem")]) {
e.style.background = "";
}
}
for (let item in warnings) {
const templates = document.createElement("tr");
let html = `<td><span class="diffWarningLabel">${item}</span></td>`;
for (let i = 0; i < warnings[item].templates.length; i++) {
html += `<td><span
class="diffWarning warningLevel${i + 1}"
title="${warnings[item].templates[i]}"
onclick="revertButton('${item}', ${i})">${i === 4 ? "4im" : i + 1}</span></td>`;
}
if (warnings[item].templates.length === 4) {
html += "<td></td>";
}
templates.innerHTML = html + "<td><span class='fas fa-circle-question reason-explanation' title='" + warnings[item].desc + "'></span></td>";
warningsContainer.appendChild(templates);
}
for (let item of diffToolbarItems) {
item.onclick = function() {
if (this.id === "report-menu") {
qs("#report-reason").value = "";
qs("#past-final-warning").checked = true;
}
const elem = this.querySelector(".diffActionBox");
if (elem.style.display !== "initial") {
for (let e of [...document.querySelectorAll(".diffActionBox")]) {
e.style.display = "none";
}
for (let e of [...document.querySelectorAll(".diffActionItem")]) {
e.style.background = "";
}
elem.style.display = "initial";
this.style.background = "#ddd";
} else {
elem.style.display = "none";
this.style.background = "";
}
}
}
fetchRecentChanges();
updateAIVReports();
window.setInterval(updateAIVReports, 15000);
}
// fetch recent changes from API
async function fetchRecentChanges() {
let greatestRevId = 0;
try {
if (queueItems.length >= antiVandalOptions.maxQueueSize) {
renderQueue();
window.setTimeout(() => fetchRecentChanges(), antiVandalOptions.refreshTime);
return;
}
const options = {
action: "query",
format: "json",
list: "recentchanges",
rcprop: "title|ids|sizes|flags|user|tags|comment",
rclimit: 50,
rctype: "edit|new",
rcnamespace: getNamespaceString()
};
if (rcstart) {
options.rcstart = rcstart;
options.rcdir = "newer";
}
const date = new Date();
rcstart = date.getUTCFullYear() + "-" +
padString(date.getUTCMonth() + 1, 2) + "-" +
padString(date.getUTCDate(), 2) + "T" +
padString(date.getUTCHours(), 2) + ":" +
padString(date.getUTCMinutes(), 2) + ":" +
padString(date.getUTCSeconds(), 2);
const data = await antiVandalApi.get(options);
const changes = data.query.recentchanges;
let usersToFetch = [];
for (let change of changes) {
if (lastRevId > change.revid) {
continue;
}
if (!usersToFetch.includes(change.user)) {
usersToFetch.push(isIPv6(change.user) ? change.user.toUpperCase() : change.user);
}
greatestRevId = Math.max(greatestRevId, change.revid);
}
const userData = (await antiVandalApi.get({
action: "query",
format: "json",
list: "users",
usprop: "editcount",
ususers: usersToFetch.join("|")
})).query.users;
let talkPageData = (await antiVandalApi.get({
action: "query",
format: "json",
prop: "revisions",
titles: usersToFetch.map(u => "User_talk:" + u).join("|"),
formatversion: 2,
rvprop: "content",
rvslots: "*"
}));
if (typeof talkPageData.query === "undefined") {
window.setTimeout(() => fetchRecentChanges(), antiVandalOptions.refreshTime);
return;
}
talkPageData = talkPageData.query.pages;
const warnLevels = {};
for (let item in talkPageData) {
const username = talkPageData[item].title.split("User talk:")[1];
if (typeof talkPageData[item].missing !== "undefined") {
warnLevels[username] = "0";
continue;
}
warnLevels[username.toLowerCase()] = getWarningLevel(talkPageData[item].revisions[0].slots.main.content);
}
let editCounts = {};
for (let user of userData) {
if (typeof user.invalid === "string") {
editCounts[user.name] = -1;
continue;
}
editCounts[user.name] = user.editcount;
}
for (let change of changes) {
if (editCounts[change.user] > antiVandalOptions.maxEditCount || !editCounts[change.user]) {
continue;
}
addQueueItem(change, editCounts[change.user], warnLevels[change.user.toLowerCase()] || "0");
}
} catch (e) {
console.log("Failed to fetch recent changes: " + e);
}
lastRevId = Math.max(lastRevId, greatestRevId);
window.setTimeout(() => fetchRecentChanges(), antiVandalOptions.refreshTime);
}
function escapeHTML(unsafe) {
return unsafe.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
}
// get diff, user contributions, and page history for each edit
async function addQueueItem(change, editcount, warnLevel) {
for (let item of queueItems) {
if (item.id === change.revid) {
return;
}
}
for (let item of pastQueueItems) {
if (item && item.id === change.revid) {
return;
}
}
try {
let diff = "<!--NEWPAGE-->";
if (change.type === "new") {
const data = await antiVandalApi.get({
action: "query",
format: "json",
prop: "revisions",
titles: change.title,
formatversion: 2,
rvprop: "content",
rvslots: "*"
});
const content = data.query.pages[0].revisions[0].slots.main.content;
for (let line of content.split("\n")) {
diff += `<tr style="width:100%"><td style="width:50%"></td><td style="width:50%" class="diff-addedline diff-side-added">${escapeHTML(line)}</td></tr>`;
}
} else {
diff = (await antiVandalApi.get({
action: "compare",
format: "json",
fromrev: change.old_revid,
torev: change.revid,
prop: "diff"
})).compare["*"];
}
const usercontribs = await (antiVandalApi.get({
action: "query",
format: "json",
list: "usercontribs|abuselog",
afllimit: 20,
afluser: change.user,
uclimit: 10,
ucuser: change.user,
ucprop: "title|timestamp|comment|sizediff|tags|ids"
}));
const contribsList = usercontribs.query.usercontribs;
const pageHistory = (await (antiVandalApi.get({
action: "query",
format: "json",
prop: "revisions",
titles: change.title,
formatversion: 2,
rvprop: "comment|user|timestamp|tags|ids",
rvslots: "*",
rvlimit: 10
}))).query.pages[0].revisions;
outer: for (let item of usercontribs.query.abuselog) {
for (let log of contribsList) {
if (item.timestamp === log.timestamp && typeof log.result === "string") {
log.itemsMatched++;
if (item.result === "disallow") {
log.result = "disallow";
}
continue outer;
}
}
contribsList.push({
itemsMatched: 1,
timestamp: item.timestamp,
result: item.result,
title: item.title
});
}
contribsList.sort((a, b) => {
return new Date(b.timestamp) - new Date(a.timestamp);
});
let score = 0;
try {
const ORES = await (await (fetch(`https://ores.wikimedia.org/v3/scores/enwiki/${change.revid}?models=damaging|goodfaith`))).json();
const results = ORES["enwiki"]["scores"][change.revid];
score = (results["goodfaith"]["score"]["probability"]["false"]);
} catch (e) {
console.log(e);
}
if (score < antiVandalOptions.minimumORESScore || 0) {
return;
}
const item = {
user: change.user,
editcount: editcount,
change: change.newlen - change.oldlen,
title: change.title,
pageLink: `https://uz.wikipedia.org/wiki/${change.title}`,
userLink: `https://uz.wikipedia.org/wiki/Special:Contributions/${change.user}`,
userTalkLink: `https://uz.wikipedia.org/wiki/User_talk:${change.user}`,
userPageLink: `https://uz.wikipedia.org/wiki/User:${change.user}`,
tags: change.tags,
diff: diff,
id: change.revid,
comment: change.comment,
usercontribs: contribsList,
wiki: "en",
warnLevel: warnLevel,
pageHistory: pageHistory,
score: score
};
if (queueItems.length > 0 && antiVandalOptions.sortQueueItems) {
const firstItem = queueItems.shift();
queueItems.push(item);
queueItems.sort((a, b) => b.score - a.score);
queueItems.unshift(firstItem);
} else {
queueItems.push(item);
}
renderQueue();
} catch (err) {
console.log(err);
}
}
async function revert(data, toWarn) {
qs(".diffProgressContainer").innerHTML += `
<div class="diffProgressBar" id="revert-${data.id}">
<div class="diffProgressBarOverlay"></div>
<div class="diffProgressBarText">Getting history...</div>
</div>
`;
const progressBarId = "#revert-" + data.id;
const overlayId = "#revert-" + data.id + " > .diffProgressBarOverlay";
const textId = "#revert-" + data.id + " > .diffProgressBarText";
try {
let revdata = (await antiVandalApi.get({
action: "query",
format: "json",
prop: "revisions",
revids: data.id,
rvprop: "user|timestamp"
})).query.pages;
let pageId;
for (let item in revdata) {
pageId = item;
}
const revision = revdata[pageId].revisions[0];
const title = revdata[pageId].title;
if (rollbackAllowed) {
try {
qs(overlayId).style.width = "25%";
qs(textId).innerText = "Eski holiga qaytarilmoqda...";
await antiVandalApi.postWithToken(
"rollback",
{
action: "rollback",
title: title,
user: revision.user,
summary: `Reverted edits by [[Special:Contributions/${revision.user}|${revision.user}]] ([[User talk:${revision.user}|talk]])${data.summary ? ": " + data.summary : ""} ([[WP:AntiVandal|AV]])`
}
);
} catch (e) {
console.log(e);
qs(overlayId).style.background = "rgb(60, 220, 60)";
qs(overlayId).style.width = "100%";
qs(textId).innerText = "Qarama-qarshilikni tahrirlash";
hideProgressBar(progressBarId);
return;
}
} else {
const pageHistory = (await antiVandalApi.get({
action: "query",
format: "json",
prop: "revisions",
pageids: pageId,
rvprop: "user|timestamp|ids",
rvlimit: 10
})).query.pages[pageId].revisions;
let content;
for (let rev of pageHistory) {
if (rev.user !== revision.user) {
content = (await antiVandalApi.get({
action: "query",
format: "json",
formatversion: 2,
prop: "revisions",
revids: rev.revid,
rvprop: "content"
})).query.pages[0].revisions[0].content;
break;
}
}
if (!content) {
qs(overlayId).style.background = "rgb(255, 60, 60)";
qs(overlayId).style.width = "100%";
qs(textId).innerText = "Could not get page";
hideProgressBar(progressBarId);
return;
}
qs(overlayId).style.width = "25%";
qs(textId).innerText = "Reverting...";
const summary = `Reverted edits by [[Special:Contributions/${revision.user}|${revision.user}]] ([[User talk:${revision.user}|talk]])${data.summary ? ": " + data.summary : ""} ([[WP:AntiVandal|AV]])`;
const response = await antiVandalApi.post({
action: "edit",
format: "json",
pageid: pageId,
baserevid: data.id,
summary: summary,
text: content,
token: await getCSRFToken(),
nocreate: 1
});
const editors = await antiVandalApi.get({
action: "query",
prop: "revisions",
titles: title,
rvlimit: 1,
rvprop: "user"
});
const lastEditor = editors.query.pages[pageId].revisions[0].user;
if (response.error || response.edit.result !== "Success" || lastEditor !== mw.config.values.wgUserName) {
qs(overlayId).style.background = "rgb(60, 220, 60)";
qs(overlayId).style.width = "100%";
qs(textId).innerText = "Edit conflict";
hideProgressBar(progressBarId);
return;
}
}
if (!toWarn) {
qs(overlayId).style.width = "50%";
qs(textId).innerText = "Getting talk...";
const talkPage = (await antiVandalApi.get({
action: "query",
format: "json",
formatversion: 2,
prop: "revisions",
rvprop: "content",
titles: "User_talk:" + revision.user
})).query.pages;
qs(overlayId).style.width = "75%";
qs(textId).innerText = "Warning...";
let createNewSection = false, talkContent = "", newContent = "";
if (typeof Object.values(talkPage)[0].missing !== "undefined") {
createNewSection = true;
} else {
talkContent = Object.values(talkPage)[0].revisions[0].content;
if (talkContent.match(new RegExp("== ?" + getMonthSectionName() + " ?==")) === null) {
createNewSection = true;
}
}
const warnLevel = getWarningLevel(talkContent);
if (warnLevel === "4" || warnLevel === "4im") {
newMessage("User is already at level 4 warning");
qs(overlayId).style.width = "100%";
qs(textId).innerText = "Done";
hideProgressBar(progressBarId);
return;
}
let warnTemplate;
if (data.template === "auto") {
warnTemplate = "{{subst:uw-vandalism" + (Number(warnLevel) + 1) + "|" + title + "}} ~~" + "~~";
} else {
warnTemplate = "{{" + data.template.wikitext + "|" + title + "}}" + " ~~" + "~~";
}
if (createNewSection) {
newContent = talkContent + "\n== " + getMonthSectionName() + " ==\n\n" + warnTemplate;
} else {
const sections = talkContent.split(/(?=== ?[\w\d ]+ ?==)/g);
for (let section in sections) {
if (sections[section].match(new RegExp("== ?" + getMonthSectionName() + " ?==")) !== null) {
sections[section] += "\n\n" + warnTemplate + "\n";
}
}
newContent = sections.join("");
}
newContent = newContent.replaceAll("\n\n\n", "\n\n");
await antiVandalApi.post({
action: "edit",
format: "json",
title: "User_talk:" + revision.user,
summary: `Message about your edit on [[${title}]] (level ${(data.template.level || Number(warnLevel)) + 1}) ([[WP:AntiVandal|AV]])`,
text: newContent,
token: await getCSRFToken()
});
}
qs(overlayId).style.width = "100%";
qs(textId).innerText = "Done";
hideProgressBar(progressBarId);
} catch (err) {
qs(overlayId).style.background = "rgb(255, 60, 60)";
qs(overlayId).style.width = "100%";
qs(textId).innerText = "Edit conflict";
hideProgressBar(progressBarId);
console.log(err);
}
}
// hide a progress bar after 3 secs
function hideProgressBar(id) {
window.setTimeout(() => {
qs(id).style.opacity = "0";
window.setTimeout(() => {
qs(id).remove();
}, 400);
}, 3000);
}
function revertButton(template, level) {
revert({
id: currentId,
template: {
label: warnings[template].label,
wikitext: warnings[template].templates[level],
level: level
}
});
shiftQueue();
for (let e of [...document.querySelectorAll(".diffActionBox")]) {
e.style.display = "none";
}
for (let e of [...document.querySelectorAll(".diffActionItem")]) {
e.style.background = "";
}
}
// get CSRF token used for making edits
async function getCSRFToken() {
return (await antiVandalApi.get({
action: "query",
meta: "tokens",
format: "json"
})).query.tokens.csrftoken;
}
// display message
function newMessage(message) {
qs(".message").innerHTML = message;
clearTimeout(messageTimeout);
messageTimeout = setTimeout(() => qs(".message").innerHTML = "", 5000);
}
// find maximum warning level for user
function getWarningLevel(page) {
const monthSections = page.split(/(?=== ?[\w\d ]+ ?==)/g);
for (let section of monthSections) {
if (new RegExp("== ?" + getMonthSectionName() + " ?==").test(section)) {
const templates = section.match(/<\!-- Template:[\w-]+?(\di?m?) -->/g);
if (templates === null) {
return "0";
}
const filteredTemplates = templates.map(t => {
const match = t.match(/<\!-- Template:[\w-]+?(\di?m?) -->/);
if (!match) {
return "0";
}
return match[1];
});
return filteredTemplates.sort()[filteredTemplates.length - 1].toString();
}
}
return "0";
}
// returns current month and year (etc "April 2022")
function getMonthSectionName() {
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const currentMonth = months[new Date().getUTCMonth()];
const currentYear = new Date().getUTCFullYear();
return currentMonth + " " + currentYear;
}
// pad a string with 0's
function padString(str, len) {
str = str.toString();
while (str.length < len) {
str = "0" + str;
}
return str;
}
// delete the first item from the queue
function shiftQueue() {
if (queueItems.length > 0) {
pastQueueItems.push(queueItems.shift());
}
if (pastQueueItems.length > 20) {
pastQueueItems.shift();
}
currentEdit = null;
currentId = null;
renderQueue();
}
// check if username is ipv6
function isIPv6(ip) {
const regex = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/gi;
return regex.test(ip);
}
// renders items to queue
function renderQueue() {
const queueContainer = qs(".queueItemsContainer");
queueContainer.innerHTML = "";
if (queueItems.length === 0) {
displayDiff(null);
}
for (let i = 0; i < queueItems.length; i++) {
if (i === 0 && queueItems[i].id !== currentId) {
currentId = queueItems[i].id;
currentEdit = queueItems[i];
displayDiff(queueItems[i]);
}
renderQueueItem(queueItems[i], i !== 0);
}
let status;
if (queueItems.length >= antiVandalOptions.maxQueueSize) {
status = "Queue is paused";
} else if (queueItems.length === 0) {
status = "Boshqa natijalar yuklanmoqda...";
}
if (status) {
qs(".queueStatusContainer").style.display = "initial";
qs(".queueStatus").innerText = status;
} else {
qs(".queueStatusContainer").style.display = "none";
}
}
// render an individual item to the queue section
function renderQueueItem(item, isSelected) {
const queueContainer = qs(".queueItemsContainer");
let tagHTML = "";
if (item.tags.length > 2) {
let extra = item.tags.length - 2;
item.tags = item.tags.splice(0, 2);
item.tags.push("+" + extra + " more");
}
for (let tag of item.tags) {
tagHTML += `<span class="queueItemTag">${tag}</span>`;
}
let OREScolor = "grey";
let title = "likely not vandalism";
if (item.score > 0.5) {
OREScolor = "yellow";
title = "possible vandalism";
}
if (item.score > 0.6) {
OREScolor = "orange";
title = "likely vandalism";
}
if (item.score > 0.65) {
OREScolor = "red";
title = "very likely vandalism";
}
queueContainer.innerHTML += `
<div class="queueItem${isSelected ? "" : " currentQueueItem"}">
<a class="queueItemTitle" href="${item.pageLink}" target="_blank" title="${item.title}">
<span class="fas fa-file-lines"></span>${item.title}
</a>
<a class="queueItemUser" href="${item.userLink}" target="_blank" title="User:${item.user}">
<span class="fas fa-user"></span>${maxStringLength(item.user, 25)}
</a>
<div class="queueItemChange" style="color: ${getChangeColor(item.change)};">
<span class="queueItemChangeText">${getChangeString(item.change)}</span>
</div>
<div class="queueItemTags">
${tagHTML}
</div>
<div class="ores ores-${OREScolor}" title="ORES score of ${Math.floor(item.score * 100) / 100}; ${title}"></div>
</div>
`;
}
// update the diff menu, user contributions, and page history sections
function displayDiff(item) {
const diffContainer = qs(".diffChangeContainer");
diffContainer.style.height = "auto";
diffContainer.style.display = "block";
const toolbar = qs(".diffToolbar");
const userContribsContainer = qs(".userContribs");
userContribsContainer.innerHTML = "";
const pageHistoryContainer = qs(".pageHistory");
pageHistoryContainer.innerHTML = "";
const editCountContainer = qs(".infoEditCount");
const warnLevelContainer = qs(".infoWarnLevel");
if (item === null) {
diffContainer.style.height = "calc(100% - 100px)";
diffContainer.style.display = "flex";
diffContainer.style.alignItems = "center";
diffContainer.style.justifyContent = "center";
diffContainer.innerHTML = `Boshqa natijalar yuklanmoqda...`;
toolbar.innerHTML = `<i>No edit selected</i>`;
warnLevelContainer.innerText = "Warn level: N/A";
editCountContainer.style.display = "none";
qs("#user-being-reported").innerHTML = "User being reported: none";
qs(".report-button").disabled = true;
return;
}
if (item.diff.includes("<!--NEWPAGE-->")) {
diffContainer.innerHTML = `
<table style="width:100%">${item.diff}</table>
<span class="newPageWarning">Ushbu tahrir yangi sahifa yaratdi va uni qaytarib bo‘lmaydi.</span>
`;
} else {
diffContainer.innerHTML = `<table>${item.diff}</table>`;
}
const summary = item.comment.length > 0 ? `Summary: ${maxStringLength(item.comment, 50)}` : "";
toolbar.innerHTML = `
<a class="diffToolbarItem" href="${item.pageLink}" target="_blank">
<span class="fas fa-file-lines"></span>
${item.title}
</a>
<span class="diffToolbarItem">
<span class="fas fa-user"></span>
<a href="${item.userPageLink}" target="_blank">${item.user}</a>
<span class="unbold">
(<a href="${item.userTalkLink}" target="_blank">talk</a> • <a href="${item.userLink}" target="_blank">contribs</a>)
</span>
</span>
<span class="diffToolbarItem">
<span class="fas fa-pencil"></span>
<span style="color: ${getChangeColor(item.change)};">${getChangeString(item.change)}</span>
</span>
<div class="diffToolbarOverlay">
<span title="${item.comment}">${summary}</span>
</div>
`;
for (let con of item.usercontribs) {
if (typeof con.result === "string") {
if (con.result === "disallow") {
userContribsContainer.innerHTML += `
<div class="abuseFilterDisallow">
<a class="infoItemTitle" href="${getPageLink(con.title, item.wiki)}" target="_blank" title="${con.title}">
<span class="fas fa-file-lines"></span>${con.title}
</a>
<a class="infoItemTitle infoItemTime" title="${con.timestamp}">
<span class="fas fa-clock"></span>${timeAgo(con.timestamp)}
</a>
<a class="infoItemTitle">
<span class="fas fa-circle-exclamation"></span>Disallowed by ${con.itemsMatched} abuse filter${con.itemsMatched === 1 ? "" : "s"}
</a>
</div>
`;
}
continue;
}
con.comment = escapeHTML(con.comment);
let tagHTML = "";
if (con.tags.includes("mw-reverted")) {
tagHTML = `<span class="queueItemTag">Reverted</span>`;
}
userContribsContainer.innerHTML += `
<div class="queueItem${con.revid === item.id ? ' currentQueueItem':''}">
<a class="infoItemTitle" href="${getPageLink(con.title, item.wiki)}" target="_blank" title="${con.title}">
<span class="fas fa-file-lines"></span>${con.title}
</a>
<a class="infoItemTitle" title="${con.comment || "No edit summary"}">
<span class="fas fa-comment-dots"></span>${con.comment || "<em>No edit summary</em>"}
</a>
<a class="infoItemTitle infoItemTime" title="${con.timestamp}">
<span class="fas fa-clock"></span>${timeAgo(con.timestamp)}
</a>
<div class="queueItemChange" style="color: ${getChangeColor(con.sizediff)};">
<span class="queueItemChangeText">${getChangeString(con.sizediff)}</span>
</div>
<div class="queueItemTags">
${tagHTML}
</div>
</div>
`;
}
for (let con of item.pageHistory) {
let tagHTML = "";
if (con.tags.includes("mw-reverted")) {
tagHTML = `<span class="queueItemTag">Reverted</span>`;
}
pageHistoryContainer.innerHTML += `
<div class="queueItem${con.revid === item.id ? ' currentQueueItem':''}">
<a class="infoItemTitle" href="${getPageLink("Special:Contributions/" + con.user, item.wiki)}" target="_blank" title="${con.user}">
<span class="fas fa-user"></span>${con.user}
</a>
<a class="infoItemTitle" title="${con.comment || "No edit summary"}">
<span class="fas fa-comment-dots"></span>${con.comment || "<em>No edit summary</em>"}
</a>
<a class="infoItemTitle infoItemTime" title="${con.timestamp}">
<span class="fas fa-clock"></span>${timeAgo(con.timestamp)}
</a>
<div class="queueItemTags">
${tagHTML}
</div>
</div>
`;
}
if (item.editcount !== -1) {
editCountContainer.style.display = "initial";
editCountContainer.innerText = "Count: " + item.editcount;
} else {
editCountContainer.style.display = "none";
}
warnLevelContainer.innerText = "Warn level: " + item.warnLevel;
const warningsContainer = qs(".diffWarningsContainer");
if (qs("#diffWarn")) {
qs("#diffWarn").remove();
}
let html = "<tbody id='diffWarn'><tr><td></td>";
const warnLevels = ["0", "1", "2", "3", "4", "4im"];
for (let i = 1; i < 6; i++) {
if (currentEdit.warnLevel === warnLevels[i - 1]) {
html += `<td class='centered' title="User's current warning level"><span class='fas fa-caret-down'></span></td>`;
} else {
html += "<td></td>";
}
}
warningsContainer.innerHTML = html + "</tr></tbody>" + warningsContainer.innerHTML;
qs("#user-being-reported").innerHTML = `User being reported: <a target="_blank" href="${item.userPageLink}">${item.user}</a> (<a target="_blank" href="${item.userTalkLink}">talk</a> • <a target="_blank" href="${item.userLink}">contribs</a>)`;
if (item.warnLevel === "4" || item.warnLevel === "4im") {
qs("#report-notice").innerText = "";
} else {
qs("#report-notice").innerText = "This user does not appear to have a final warning on their talk page. Are you sure you want to report?";
}
if (checkIfReported(item.user)) {
qs("#report-notice").innerText = "This user has already been reported.";
qs(".report-button").disabled = true;
} else {
qs(".report-button").disabled = false;
}
updateReportToolbar(item.user, item.warnLevel);
}
// update the icon on the toolbar
function updateReportToolbar(username, warnLevel) {
const icon = qs("#reportIcon");
icon.style.color = "black";
icon.style.display = "initial";
if (checkIfReported(username)) {
icon.className = "fas fa-circle-info";
} else if (warnLevel === "4" || warnLevel === "4im") {
icon.className = "fas fa-circle-exclamation";
icon.style.color = "red";
} else {
icon.style.display = "none";
}
}
// add link to the top bar
function addAntiVandalLink() {
mw.util.addPortletLink(
'p-personal',
mw.util.getUrl('Vikipediya:AntiVandal/run'),
'AntiVandal',
'pt-AntiVandal',
'AntiVandal',
null,
'#pt-preferences'
);
}
// load the css and html for the interface
function createInterface() {
document.body.innerHTML = `
<div class="mainContainer mainFullHeight">
<div class="queueContainer mainFullHeight">
<div class="queueControls">
<h2 class="sectionHeading">Queue</h2>
<span class="fas fa-gear queueControl" id="settings" title="Settings"></span>
<span class="fas fa-trash-can queueControl" id="queueDelete" title="Remove all items from queue"></span>
<span class="fas fa-arrow-right queueControl" id="queueForward" title="Go to next edit"></span>
<span class="fas fa-arrow-left queueControl" id="queueBack" title="Go to previous edit"></span>
</div>
<div class="queueItemsContainer"></div>
<div class="queueStatusContainer">
<div class="queueStatus">Navbat yuklanmoqda...</div>
</div>
</div>
<div class="diffContainer mainFullHeight">
<div class="diffToolbar"></div>
<div class="diffChangeContainer"></div>
<div class="diffActionContainer">
<div class="diffActionItem">
Ogohlantirish
<div class="diffActionBox">
<span>Ogohlantirish va qaytarish</span>
<table class="diffWarningsContainer"></table>
</div>
</div>
<div class="diffActionItem" id="report-menu">
Xabar
<span id="reportIcon"></span>
<div class="diffActionBox">
<span>Foydalanuvchiga xabar berish
<a target="_blank" title="Vandalizmga qarshi administrator aralashuvi" href="https://uz.wikipedia.org/wiki/Vikipediya:Foydalanuvchilarni_chetlashtirishga_so%CA%BBrovlar">AIV</a>
</span><br>
<input type="radio" id="past-final-warning" name="report-reason" checked>
<label for="past-final-warning">Vandalizm oxirgi ogohlantirishdan o‘tgan</label><br>
<input type="radio" id="vandalism-only-acc" name="report-reason">
<label for="vandalism-only-acc">Faqat vandalizm hisobi</label><br>
<input type="radio" id="other-reason" name="report-reason">
<label for="other-reason">Boshqa (aniqlash)</label><br>
<input for="other-reason" id="report-reason" type="text"><br>
<button class="report-button" disabled>Xabar</button><br><br>
<i id="user-being-reported">Xabar qilingan foydalanuvchi: Yo‘q</i><br><br>
<i id="report-notice"></i>
</div>
</div>
<div class="diffActionItem">
Xulosa bilan qaytarish
<div class="diffActionBox">
<input type="text" id="revert-summary" placeholder="Xulosa qaytarish"><br>
<button id="revert-button">Oldingi holatga qaytarish</button>
</div>
</div>
<!-- <div class="diffActionItem">
Bloklash
</div> -->
<div class="message"></div>
</div>
<div class="diffProgressContainer"></div>
</div>
<div class="infoContainer mainFullHeight">
<div class="infoContainerItem">
<div class="infoContainerItemHeading">
<h2 class="sectionHeading">Foydalanuvchi hissalari</h2><br>
<span class="infoEditCount">Hisob: ___</span>
<span class="infoWarnLevel">Ogohlantirish darajasi: _</span>
</div>
<div class="userContribs"></div>
</div>
<div class="infoContainerItem">
<div class="infoContainerItemHeading">
<h2 class="sectionHeading">Sahifa tarixi</h2>
</div>
<div class="pageHistory"></div>
</div>
</div>
</div>
<div class="settings">
<div class="settingsContainer">
<div class="settingsSectionContainer">
<div class="settingsSection settingsSectionSelected">Navbat</div>
<!--<div class="settingsSection">Nazorat</div>
<div class="settingsSection">Interfeys</div>-->
</div>
<div class="settingsButtonContainer">
<button class="settingsButton settingsCancel">Bekor qilish</button>
<button class="settingsButton settingsSave">Saqlash</button>
</div>
<div class="settingsCloseContainer">
<span class="fas fa-xmark settingsClose" title="Close settings"></span>
</div>
<div class="selectedSettings">
<div class="queueSettings">
<span>Kamroq foydalanuvchi tahrirlarini koʻrsatish</span>
<input type="number" name="queueUsersCount">
<label for="queueUsersCount">tahrirlar</label><br><br>
<label for="queueMaxSize">Maksimal navbat hajmi:</label>
<input type="number" name="queueMaxSize"><br><br>
<span>Ushbu nom maydonlaridan tahrirlarni ko'rsatish:</span><br>
<input type="checkbox" name="namespaceMain">
<label for="namespaceMain">Asosiy safiha va munozara:</label><br>
<input type="checkbox" name="namespaceUser">
<label for="namespaceUser">Foydalanuvchi: va Foydalanuvchi munozarasi:</label><br>
<input type="checkbox" name="namespaceDraft">
<label for="namespaceDraft">Draft: va Draft munozarasi:</label><br>
<input type="checkbox" name="namespaceWikipedia">
<label for="namespaceWikipedia">Vikipediya: va Vikipediya munozarasi:</label><br>
<input type="checkbox" name="namespaceOther">
<label for="namespaceOther">Boshqa barcha nom maydonlari</label><br><br>
<span>ORES balli quyidagidan past boʻlgan tahrirlarga eʼtibor bermang:</span><br>
<label for="minORES">0</label>
<input type="range" name="minORES" min=0 max=1 step=0.05>
<p></p>
</div>
</div>
</div>
</div>
`;
document.head.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
<title>AntiVandal</title>
<style>
/* general */
body, html {
width: 100%;
height: 100%;
font-family: Arial, Helvetica, sans-serif;
overflow-x: hidden;
margin: 0;
}
* {
box-sizing: border-box;
}
.unbold {
font-weight: initial;
}
.sectionHeading {
margin: 0;
display: inline-block;
font-size: 1em;
}
.centered {
text-align: center;
}
/* login form */
.loginFormContainer {
display: flex;
flex-direction: column;
justify-content: center;
padding: 30px;
width: 50%;
min-width: 400px;
height: 100%;
margin: auto;
}
.loginFormInput:not([type=checkbox]) {
display: block;
}
.loginFormInput:not([type=checkbox]) {
margin-bottom: 10px;
padding: 5px;
border: 1px solid #ccc;
}
.loginFormButton {
display: block;
width: 100%;
height: 30px;
text-align: center;
margin-top: 10px;
}
.loginFormCheckboxContainer {
margin-bottom: 10px;
}
.loginFormLabel {
font-size: 0.9em;
}
.loginError {
color: red;
font-size: 0.9em;
}
/* main */
.queueContainer, .infoContainer {
width: 25%;
max-width: 300px;
height: 100%;
}
.queueContainer {
overflow-y: scroll;
overflow-x: hidden;
}
.diffContainer {
width: 50%;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
flex-grow: 2;
}
.mainContainer {
display: flex;
}
.mainFullHeight {
height: 100%;
}
/* queue */
.newPageWarning {
background: yellow;
position: absolute;
top: 10px;
left: 10px;
padding: 5px;
border-radius: 5px;
}
.queueItem, .abuseFilterDisallow {
padding: 10px;
border-bottom: 1px solid #ccc;
position: relative;
}
.abuseFilterDisallow {
padding: 10px 10px 10px 5px;
border-left: 5px solid red;
}
.queueItemTitle, .queueItemUser, .infoItemTitle {
text-decoration: none;
color: black;
font-size: 0.9em;
text-overflow: clip;
white-space: nowrap;
overflow: hidden;
width: fit-content;
display: block;
}
.queueItemTitle span, .queueItemUser span {
margin-right: 10px;
}
.queueItemUser {
margin-top: 5px;
font-size: 0.8em;
}
.queueItemChange {
width: 75px;
height: 100%;
position: absolute;
top: 0;
left: calc(100% - 75px);
font-size: 0.9em;
display: flex;
align-items: center;
justify-content: right;
padding-right: 10px;
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
}
.queueItemChangeText {
position: relative;
z-index: 3;
}
.queueItemTag {
border-radius: 3px;
background: #ddd;
padding: 2px 4px;
margin: 3px;
font-size: 0.7em;
position: relative;
z-index: 2;
}
.queueControls, .diffToolbar {
padding: 10px;
font-size: 1em;
font-weight: bold;
height: 50px;
position: sticky;
background: white;
z-index: 4;
border-bottom: 1px solid #ccc;
top: 0;
}
.queueControls .sectionHeading {
margin-top: 5px;
}
.queueControl {
float: right;
padding: 5px;
font-size: 1.2em;
cursor: pointer;
}
.currentQueueItem {
background: #eee;
}
.queueStatusContainer {
top: calc(100% - 60px);
z-index: 5;
position: fixed;
font-size: 0.8em;
width: 25%;
max-width: 300px;
text-align: center;
}
.queueStatus {
background: #888;
color: white;
border-radius: 7px;
padding: 8px;
width: fit-content;
margin: auto;
}
/* diff viewer */
.diffContainer {
overflow-y: auto;
}
.diffContainer td, .diffContainer tr {
overflow-wrap: anywhere;
}
.diff-addedline {
background: rgba(0, 255, 0, 0.3);
}
ins {
background: rgba(0, 255, 0, 0.5);
text-decoration: none;
}
.diff-deletedline {
background: rgba(255, 0, 0, 0.3);
}
del {
background: rgba(255, 0, 0, 0.5);
text-decoration: none;
}
.diff-lineno {
border-bottom: 1px dashed grey;
background: rgba(0, 0, 0, 0.2);
}
.diffChangeContainer table, .diffChangeContainer tbody {
font-family: monospace;
vertical-align: baseline;
}
.diffToolbar {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
position: fixed;
width: calc(100% - 300px);
}
.diffToolbarItem {
color: black;
text-decoration: none;
margin: 0 10px;
}
.diffToolbarItem a {
color: black;
text-decoration: none;
}
.diffChangeContainer td:not(.diff-marker) {
width: 50%;
}
.diffToolbarOverlay {
flex-basis: 100%;
display: flex;
justify-content: center;
font-weight: normal;
padding: 0 20px;
}
.diffChangeContainer {
margin-top: 50px;
position: relative;
}
.diffActionContainer {
position: fixed;
height: 40px;
top: calc(100% - 40px);
background: white;
display: flex;
align-items: center;
border-top: 1px solid #ccc;
width: calc(100% - 602px);
}
.diffActionItem {
height: 100%;
width: fit-content;
cursor: pointer;
user-select: none;
padding: 0 15px;
display: flex;
align-items: center;
text-align: center;
position: relative;
z-index: 5;
}
.diffActionBox {
position: absolute;
left: 0;
top: -410px;
height: 410px;
width: 380px;
border: 1px solid #ccc;
cursor: initial;
user-select: initial;
text-align: left;
display: none;
padding: 15px;
background: white;
overflow-y: scroll;
}
.diffActionItem:hover {
background: #eee;
}
.diffWarning {
padding: 5px;
border-radius: 3px;
width: 35px;
display: inline-block;
font-size: 0.8em;
user-select: none;
cursor: pointer;
text-align: center;
}
.diffWarningLabel {
font-size: 0.9em;
}
.diffProgressContainer {
position: fixed;
top: calc(100% - 80px);
height: 40px;
display: flex;
justify-content: flex-end;
align-items: center;
width: calc(100% - 600px);
padding: 0px 20px;
}
.diffProgressBar {
border-radius: 5px;
width: 150px;
height: 25px;
background: #ddd;
font-size: 0.8em;
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-left: 10px;
opacity: 1;
transition: 0.3s;
}
.diffProgressBarOverlay {
position: absolute;
top: 0;
left: 0;
border-radius: 5px;
width: 0px;
transition: 0.3s;
height: 100%;
background: rgb(0, 170, 255);
}
.diffActionBox a {
color: black;
}
.diffProgressBarText {
position: relative;
}
.diffWarningsContainer td {
padding: 2px;
}
.warningLevel1 {
background: rgb(138, 203, 223);
}
.warningLevel2 {
background: rgb(215, 223, 138);
}
.warningLevel3 {
background: rgb(226, 170, 97);
}
.warningLevel4 {
background: rgb(224, 82, 64);
}
.warningLevel5 {
color: white;
background: rgb(0, 0, 0);
}
/* info container */
.infoContainer {
margin-top: 50px;
height: calc(100% - 50px);
}
.infoContainerItem {
height: 50%;
overflow-y: scroll;
overflow-x: hidden;
border-bottom: 1px solid #ccc;
}
.infoItemTitle {
margin-bottom: 3px;
}
.infoItemTitle .fas {
width: 20px;
}
.infoItemTime {
font-size: 0.8em;
}
.infoContainerItemHeading {
padding: 10px;
border-bottom: 1px solid #ccc;
}
.infoEditCount, .infoWarnLevel {
font-size: 0.8em;
}
.infoEditCount {
margin-right: 10px;
}
/* settings */
.settings {
display: none;
align-items: center;
justify-content: center;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 10;
}
.settingsContainer {
width: 60%;
min-width: 800px;
height: 60%;
min-height: 600px;
background: white;
border: 1px solid #bbb;
position: relative;
display: flex;
flex-wrap: wrap;
}
.settingsSectionContainer {
width: 150px;
border-right: 1px solid #ccc;
height: 100%;
}
.settingsSection {
border-bottom: 1px solid #ccc;
padding: 10px;
user-select: none;
cursor: pointer;
}
.settingsSectionSelected {
background: #ddd;
}
.settingsButton {
width: 100px;
height: 30px;
}
.settingsButtonContainer, .settingsCloseContainer {
text-align: right;
position: absolute;
top: calc(100% - 40px);
width: calc(100% - 10px);
left: 0;
user-select: none;
flex-basis: 100%;
}
.settingsCloseContainer {
top: 10px;
}
.settingsClose {
cursor: pointer;
font-size: 1.5em;
}
.selectedSettings {
padding: 15px;
}
.message {
position: absolute;
text-align: right;
left: 0;
width: calc(100% - 20px);
}
#reportIcon {
margin-left: 10px;
}
#user-being-reported, #report-notice {
font-size: 0.8em;
}
.ores {
height: 5px;
background: #ddd;
position: absolute;
top: calc(100% - 5px);
left: 0;
width: 100%;
}
.ores-red {
background: red;
}
.ores-orange {
background: orange;
}
.ores-yellow {
background: yellow;
}
label[for=minORES] {
display: block;
}
@media screen and (max-width: 1200px) {
.diffActionContainer {
width: calc(50% - 2px);
}
.diffToolbar {
width: calc(75%);
}
}
</style>
`;
}
// green for changes > 0, gray for = 0, red for < 0
function getChangeColor(change) {
if (change < 0) {
return "red";
} else if (change > 0) {
return "green";
}
return "black";
}
// + for > 0, - for < 0
function getChangeString(change) {
if (change > 0) {
change = "+" + change;
} else {
change = change.toString().replace("-", "–");
}
return change;
}
// changes timestamp into x seconds/minutes/hours ago
function timeAgo(timestamp) {
const difference = new Date().getTime() - new Date(timestamp);
const seconds = Math.floor(difference / 1000);
if (seconds > 60) {
if (seconds > 60 * 60) {
if (seconds > 60 * 60 * 24) {
const val = Math.floor(seconds / 60 / 60 / 24);
return val + " day" + (val !== 1 ? "s" : "") + " ago";
}
const val = Math.floor(seconds / 60 / 60);
return val + " hour" + (val !== 1 ? "s" : "") + " ago";
}
const val = Math.floor(seconds / 60);
return val + " minute" + (val !== 1 ? "s" : "") + " ago";
}
return seconds + " second" + (seconds !== 1 ? "s" : "") + " ago";
}
// get url to page
function getPageLink(title) {
return "https://" + antiVandalOptions.wiki + ".wikipedia.org/wiki/" + title;
}
// chop off strings over maximum length
function maxStringLength(str, len) {
if (str.length > len) {
str = str.substring(0, len) + "...";
}
return str;
}
// show the settings container
function showSettings() {
qs(".settings").style.display = "flex";
qs("input[name=queueUsersCount]").value = antiVandalOptions.maxEditCount;
qs("input[name=queueMaxSize]").value = antiVandalOptions.maxQueueSize;
qs("input[name=namespaceMain]").checked = antiVandalOptions.namespaces.main;
qs("input[name=namespaceUser]").checked = antiVandalOptions.namespaces.user;
qs("input[name=namespaceDraft]").checked = antiVandalOptions.namespaces.draft;
qs("input[name=namespaceWikipedia]").checked = antiVandalOptions.namespaces.wikipedia;
qs("input[name=namespaceOther]").checked = antiVandalOptions.namespaces.other;
qs("input[name=minORES]").value = antiVandalOptions.minimumORESScore;
qs("label[for=minORES]").innerText = antiVandalOptions.minimumORESScore;
}
// hide the settings container
function hideSettings() {
qs(".settings").style.display = "none";
}
// save settings
function saveSettings() {
antiVandalOptions.maxEditCount = parseInt(qs("input[name=queueUsersCount]").value);
antiVandalOptions.maxQueueSize = parseInt(qs("input[name=queueMaxSize]").value);
antiVandalOptions.namespaces.main = qs("input[name=namespaceMain]").checked;
antiVandalOptions.namespaces.user = qs("input[name=namespaceUser]").checked;
antiVandalOptions.namespaces.draft = qs("input[name=namespaceDraft]").checked;
antiVandalOptions.namespaces.wikipedia = qs("input[name=namespaceWikipedia]").checked;
antiVandalOptions.namespaces.other = qs("input[name=namespaceOther]").checked;
antiVandalOptions.minimumORESScore = parseFloat(qs("input[name=minORES]").value);
setSettings(antiVandalOptions);
hideSettings();
}
// get list of users reported to AIV
async function updateAIVReports() {
const AIVregex = /{{(?:ip)?vandal\|(?:1=)?(.+?)}}/gmi;
try {
const pages = (await antiVandalApi.get({
action: "query",
format: "json",
prop: "revisions",
titles: "Wikipedia:Administrator_intervention_against_vandalism|Wikipedia:Administrator_intervention_against_vandalism/TB2",
formatversion: 2,
rvprop: "content",
rvslots: "*"
})).query.pages;
currentAIVReports = [...pages[0].revisions[0].slots.main.content.matchAll(AIVregex)]
.concat([...pages[1].revisions[0].slots.main.content.matchAll(AIVregex)])
.map(e => e[1]);
} catch (err) {
console.log(err);
}
}
// check if a user is reported to AIV
function checkIfReported(user) {
for (let username of currentAIVReports) {
if (username.toLowerCase() === user.toLowerCase()) {
return true;
}
}
return false;
}
// add report for user
async function reportUserToAIV(user, reason) {
await updateAIVReports();
if (checkIfReported(user)) {
return;
}
qs(".diffProgressContainer").innerHTML += `
<div class="diffProgressBar" id="report-${reportNum}">
<div class="diffProgressBarOverlay"></div>
<div class="diffProgressBarText">Getting AIV page...</div>
</div>
`;
const progressBarId = "#report-" + reportNum;
const overlayId = "#report-" + reportNum + " > .diffProgressBarOverlay";
const textId = "#report-" + reportNum + " > .diffProgressBarText";
reportNum++;
qs(overlayId).style.background = "orange";
let template = "Vandal";
if (user.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) || isIPv6(user)) {
template = "IPvandal";
}
const text = "\n* {" + "{" + template + "|" + user + "}} – " + reason + " ~~" + "~~";
qs(overlayId).style.width = "50%";
qs(textId).innerText = "Reporting...";
try {
const AIVcontent = (await antiVandalApi.get({
action: "query",
format: "json",
prop: "revisions",
titles: "Wikipedia:Administrator_intervention_against_vandalism",
formatversion: 2,
rvprop: "content|ids",
rvslots: "*"
})).query.pages[0].revisions[0];
await antiVandalApi.post({
action: "edit",
format: "json",
title: "Wikipedia:Administrator_intervention_against_vandalism",
summary: `Reporting [[Special:Contributions/${user}|${user}]] ([[WP:AntiVandal|AV]])`,
text: AIVcontent.slots.main.content + text,
token: await getCSRFToken(),
baserevid: AIVcontent.revid
});
} catch (err) {
console.log(err);
}
qs(overlayId).style.width = "100%";
qs(textId).innerText = "Done";
hideProgressBar(progressBarId)
}
function reportCurrentUser() {
const user = currentEdit.user;
if (qs("#past-final-warning").checked) {
reportUserToAIV(user, "Vandalism past final warning.");
} else if (qs("#vandalism-only-acc").checked) {
reportUserToAIV(user, "Evidently a vandalism-only account.");
} else if (qs("#other-reason").checked) {
reportUserToAIV(user, qs("#report-reason").value);
}
qs("#report-menu").click();
}
function loadSettings() {
if (!mw.storage.get("AntiVandalSettings")) {
setSettings({
maxQueueSize: 50,
wiki: mw.config.values.wgServerName.split(".")[0],
controls: {
markAsVandalism: "q",
continueToNext: " ",
queueBack: "[",
queueForward: "]",
rollback: "r"
},
refreshTime: 5000,
maxEditCount: 50,
sortQueueItems: true,
namespaces: {
main: true,
user: true,
draft: true,
wikipedia: true,
other: true
},
minimumORESScore: 0
});
}
return JSON.parse(mw.storage.get("AntiVandalSettings"));
}
function setSettings(settings) {
mw.storage.set("AntiVandalSettings", JSON.stringify(settings));
}
function getNamespaceString() {
return namespaceList.filter(n => antiVandalOptions.namespaces[n.category]).map(n => n.id).join("|");
}
const antiVandalOptions = loadSettings();
runAntiVandal();
/* </nowiki> */