User:Ingenuity/BetterContribs.js

const canRollback = mw.config.values.wgUserGroups.includes("sysop") || mw.config.values.wgUserGroups.includes("rollbacker"); const bcApi = new mw.Api; const namespaceList = [ { name: "Main", id: 0, active: true }, { name: "User", id: 2, active: true }, { name: "Project", id: 4, active: true }, { name: "File", id: 6, active: true }, { name: "MediaWiki", id: 8, active: true }, { name: "Template", id: 10, active: true }, { name: "Help", id: 12, active: true }, { name: "Category", id: 14, active: true }, { name: "Portal", id: 100, active: true }, { name: "Draft", id: 118, active: true }, { name: "Talk", id: 1, active: true }, { name: "User talk", id: 3, active: true }, { name: "Project talk", id: 5, active: true }, { name: "File talk", id: 7, active: true }, { name: "MediaWiki talk", id: 9, active: true }, { name: "Template talk", id: 11, active: true }, { name: "Help talk", id: 13, active: true }, { name: "Category talk", id: 15, active: true }, { name: "Portal talk", id: 101, active: true }, { name: "Draft talk", id: 119, active: true } ]; const shownActions = { "edit": true, "block": true, "delete": true, "move": true, "protect": true, "patrol": true, "thank": true, "user-creation": true };

const tagValues = { "mobile edit": "mobile edit", "mobile web edit": "mobile web edit", "AWB": "AWB", "visualeditor": "visual edit", "wikieditor": "", "mw-reverted": "reverted", "mw-rollback": "rollback", "mw-undo": "undo", "visualeditor-wikitext": "", "advanced mobile edit": "", "twinkle": "Twinkle", "mobile app edit": "mobile app edit", "mw-new-redirect": "new redirect", "discussiontools-added-comment": "", "huggle": "Huggle", "pagetriage": "PageTriage", "mw-manual-revert": "manual revert", "mw-changed-redirect-target": "redirect target changed", "references removed": "references removed", "mw-replace": "replaced", "visualeditor-switched": "", "possible libel or vandalism": "possible libel or vandalism", "section blanking": "section blanking", "ios app edit": "", "mw-removed-redirect": "removed redirect", "discussiontools": "", "RedWarn": "RW", "discussiontools-source": "source", "discussiontools-reply": "reply", "mw-blank": "blanking", "discussiontools-source-enhanced": "", "OAuth CID: 542": "WikiEduDashboard", "OAuth CID: 1232": "WikiEduDashboard", "Possible self promotion in userspace": "possible self promotion in userspace", "OAuth CID: 1805": "SWViewer", "STiki": "<a href='/wiki/Wikipedia:STiki'>STiki</a>" }; let viewWithoutAdded = false;

if (window.location.href.includes("Special:Contributions/") && !window.location.href.includes("bettercontribs=0")) { loadContribs; }

function loadContribs { const offset = window.location.href.match(/[\&\?]offset=(\d+)/); const limit = window.location.href.match(/[\&\?]limit=(\d+)/); const dirMatch = window.location.href.match(/[\&\?]dir=(\w+)/); const dir = dirMatch && dirMatch.length > 1 && dirMatch[1] === "prev" ? "newer" : "older"; createHistoryTable({		user: window.location.href.split("Special:Contributions/")[1].split(/[\?\&]/)[0].replaceAll("#", ""),		offset: offset ? offset[1] : null,		limit: limit ? limit[1] : 50,		dir: dir	}); }

async function createHistoryTable(data) { const bodyDiv = document.querySelector(".mw-pager-body"); bodyDiv.innerHTML = "";

const navBar = document.querySelector(".mw-pager-navigation-bar"); const symbol = /.+?(\?.+?=.+)/i.test(window.location.href) ? "&" : "?";	if (navBar && !viewWithoutAdded) { navBar.innerHTML += " <a href='" + window.location.href + symbol + "bettercontribs=0'>View without BetterContribs</a>"; } else if (!viewWithoutAdded) { bodyDiv.innerHTML = "<a href='" + window.location.href + symbol + "bettercontribs=0'>View without BetterContribs</a>"; }	viewWithoutAdded = true;

let namespaceHTML = " ";

bodyDiv.innerHTML += ` Filter edits by namespace (will hide log entries if any are unchecked): ${namespaceHTML} <a href="#" onclick="showExtendedSettings(this)">Show extended settings</a> <button onclick="loadSettings">Load <button onclick="saveSettings">Save as default `;

[...document.querySelectorAll(".namespace-checkbox")].forEach(checkbox => {		checkbox.addEventListener("change", => { if (!checkbox.checked) { for (let lb of [...document.getElementsByClassName("log-checkbox")]) { if (lb.name !== "edit-checkbox") { lb.disabled = true; }				}			}

let anyUnchecked = false; for (let box of [...document.getElementsByClassName("namespace-checkbox")]) { if (!box.checked) { anyUnchecked = true; }			}			if (!anyUnchecked) { for (let lb of [...document.getElementsByClassName("log-checkbox")]) { lb.disabled = false; }			}		});	});

try { const contribResults = (await bcApi.get({ action: "query", list: "usercontribs|logevents", ucuser: data.user, ucprop: "ids|title|timestamp|parsedcomment|size|sizediff|tags|flags", uclimit: data.limit + 1, ucstart: data.offset === null ? null : (Number(data.offset) - 1).toString, leprop: "ids|title|timestamp|parsedcomment|tags|type|details", leuser: data.user, lestart: data.offset === null ? null : (Number(data.offset) - 1).toString, lelimit: 500, ledir: data.dir, ucdir: data.dir, ucnamespace: getNamespaceString })).query;

const table = document.createElement("ul"); table.className = "mw-contributions-list"; bodyDiv.appendChild(table);

if (contribResults.usercontribs.length === 0) { table.innerHTML = "No results found with that criteria."; return; }

const mostRecent = findMostRecent([contribResults.usercontribs, contribResults.logevents], data.limit, data.dir); let addedContent = "";

for (const elem of mostRecent) { if (elem.action) { if (namespaceList.filter(e => e.active).length < namespaceList.length) { continue; }				try { let toAdd = ""; switch (elem.type + "|" + elem.action) { case "patrol|patrol": case "patrol|autopatrol": toAdd = ` <li> <a href="/w/index.php?title=Special:Log&logid=${elem.logid}">${formatTimestamp(elem.timestamp)}</a> <a href="/wiki/Special:Log/patrol">(Patrol log)</a> marked revision <a href="/w/index.php?title=${elem.title}&oldid=${elem.params.curid}">${elem.params.curid}</a> of <a href="/wiki/${elem.title}">${elem.title}</a> patrolled </li>`; break; case "block|block": toAdd = ` <li> <a href="/w/index.php?title=Special:Log&logid=${elem.logid}">${formatTimestamp(elem.timestamp)}</a> <a href="/wiki/Special:Log/block">(Block log)</a> blocked ${typeof elem.actionhidden === "string" ? "<s style='color: grey; font-style: italic;'>(block details hidden) " : blockedUser(elem.title)}${typeof elem.actionhidden !== "string" ? ` with a duration of ${elem.params.duration}` : ""} (${elem.parsedcomment}) </li>`; break; case "protect|protect": case "protect|modify": toAdd = ` <li> <a href="/w/index.php?title=Special:Log&logid=${elem.logid}">${formatTimestamp(elem.timestamp)}</a> <a href="/wiki/Special:Log/protect">(Protection log)</a> ${elem.action === "modify" ? "changed protection settings for" : "protected"} ${elem.title} ${elem.params.description} (${elem.parsedcomment}) </li>`; break; case "delete|delete": case "delete|restore": toAdd = ` <li> <a href="/w/index.php?title=Special:Log&logid=${elem.logid}">${formatTimestamp(elem.timestamp)}</a> <a href="/wiki/Special:Log/delete">(Deletion log)</a> ${elem.action === "restore" ? "restored" : "deleted"} page <a href="/wiki/${elem.title}">${elem.title}</a>${elem.action === "restore" ? " (" + elem.params.count.revisions + " revisions)" : ""} (${elem.parsedcomment}) </li>`; break; case "delete|revision": toAdd = ` <li> <a href="/w/index.php?title=Special:Log&logid=${elem.logid}">${formatTimestamp(elem.timestamp)}</a> <a href="/wiki/Special:Log/delete">(Deletion log)</a> changed visibility of ${elem.params.ids.length} revision${elem.params.ids.length === 1 ? "" : "s"} on <a href="/wiki/${elem.title}">${elem.title}</a> (${elem.parsedcomment}) </li>`; break; case "move|move": case "move|move_redir": toAdd = ` <li> <a href="/w/index.php?title=Special:Log&logid=${elem.logid}">${formatTimestamp(elem.timestamp)}</a> <a href="/wiki/Special:Log/move">(Move log)</a> moved page <a href="/wiki/${elem.title}">${elem.title}</a> to <a href="/wiki/${elem.params.target_title}">${elem.params.target_title}</a> (${elem.parsedcomment}) </li>`; break; case "thanks|thank": toAdd = ` <li> <a href="/w/index.php?title=Special:Log&logid=${elem.logid}">${formatTimestamp(elem.timestamp)}</a> <a href="/wiki/Special:Log/thanks">(Thanks log)</a> thanked ${blockedUser(elem.title)}</a> </li>`; break; case "newusers|create": case "newusers|create2": case "newusers|autocreate": case "newusers|newusers": case "newusers|byemail": case "newusers|forcecreatelocal": let userText = blockedUser(elem.title); let text = userText + " was created"; if (elem.action === "autocreate") { text = userText + " was created automatically"; } else if (elem.action === "byemail") { text = userText + " was created and password was sent by email"; } else if (elem.action === "forcecreatelocal") { text = "forcibly created a local account for " + userText; }							toAdd = ` <li> <a href="/w/index.php?title=Special:Log&logid=${elem.logid}">${formatTimestamp(elem.timestamp)}</a> <a href="/wiki/Special:Log/newusers">(User creation log)</a> ${text} ${elem.parsedcomment ? `(${elem.parsedcomment})` : ""} </li>`; break; default: break; }					addedContent += toAdd; } catch (err) { console.log("Could not display log entry: " + err, elem); }			} else { try { const flagStyle = "style='font-weight: bold; text-decoration: underline; text-decoration-style: dotted; cursor: help;'"; let flags = ""; let tags = []; if (typeof elem.minor === "string") { flags += `<span ${flagStyle} title="This is a minor edit">m `; }					if (typeof elem.new === "string") { flags += `<span ${flagStyle} title="This edit created a new page">N `; }					let comment = typeof elem.commenthidden === "string" ? "<s style='color: grey;" + (typeof elem.suppressed === "string" ? "text-decoration-style: double;" : "") + "'>edit summary removed " : elem.parsedcomment; comment = comment.length === 0 ? "no edit summary" : comment;

for (let tag of elem.tags) { if (typeof tagValues[tag] !== "undefined") { if (tagValues[tag].length > 0) { tags.push(tagValues[tag]); }						} else { tags.push(tag); }					}					addedContent += ` <li> ${typeof elem.texthidden === "string" ? `<s style="color: grey;${typeof elem.suppressed === "string" ? "text-decoration-style: double;" : ""}">${formatTimestamp(elem.timestamp)} ` : `<a href="/w/index.php?title=${elem.title}&oldid=${elem.revid}">${formatTimestamp(elem.timestamp)}</a>`} (${typeof elem.new === "string" || typeof elem.texthidden === "string" ? "diff" : `<a href='/w/index.php?title=` + elem.title + `&diff=prev&oldid=` + elem.revid + `'>diff</a>`} | <a href="/w/index.php?title=${elem.title}&action=history">hist</a>). .							 ${diffChange(elem.sizediff)}. . ${flags} <a href="/wiki/${elem.title}">${elem.title}</a> (${comment}) ${typeof elem.top === "string" ? "(current)" : ""} ${canRollback && typeof elem.top === "string" ? `[ <a onclick='betterContribsRollback(this.parentElement, "${elem.title}", "${data.user}")'>rollback</a> ]` : ""} ${tags.length > 0 ? ` (<a href="/wiki/Special:Tags">Tag${tags.length === 1 ? "" : "s"}</a>: ${tags.join(", ")}) ` : ""} </li> `;				} catch (err) { console.log("Could not display edit: " + err, elem); }			}		}

table.innerHTML = addedContent; } catch (err) { bodyDiv.innerHTML = "Could not fetch results, please reload."; console.log(err); } }

async function betterContribsRollback(elem, page, user) { elem.innerHTML = "reverting..."; await bcApi.post({		action: "rollback",		title: page,		user: user,		token: await bcApi.getToken("rollback")	}); elem.innerHTML = "done"; }

function showExtendedSettings(elem) { const extendedSettings = document.getElementById("extendedSettings"); if (extendedSettings.style.display === "none") { extendedSettings.style.display = "block"; elem.innerHTML = "Hide extended settings"; } else { extendedSettings.style.display = "none"; elem.innerHTML = "Show extended settings"; } }

function findMostRecent(data, limit) { const combinedList = []; const sortedEdits = data[0].sort((a, b) => {		return new Date(b.timestamp) - new Date(a.timestamp);	});

for (let i = 0; i < limit; i++) { if (i >= sortedEdits.length) { break; }		combinedList.push(sortedEdits[i]); }

for (let action of data[1]) { if (new Date(action.timestamp) - new Date(sortedEdits[0].timestamp) <= 0 && new Date(action.timestamp) - new Date(sortedEdits[sortedEdits.length - 1].timestamp) >= 0) { combinedList.push(action); }	}

combinedList.sort((a, b) => {		return new Date(b.timestamp) - new Date(a.timestamp);	});

return combinedList; }

function formatTimestamp(timestamp) { const date = new Date(timestamp); const hour = date.getHours.toString.length === 1 ? "0" + date.getHours : date.getHours; const minute = date.getMinutes.toString.length === 1 ? "0" + date.getMinutes : date.getMinutes; const second = date.getSeconds.toString.length === 1 ? "0" + date.getSeconds : date.getSeconds; const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; return `${hour}:${minute}:${second}, ${date.getDate} ${monthNames[date.getMonth]} ${date.getFullYear}`; }

function diffChange(change) { const bold = Math.abs(change) >= 500 ? "font-weight: bold;" : ""; if (change === 0) { return " (0) "; } else if (change > 0) { return "<span style='color: green;" + bold + "'>(+" + change + ") "; } else { return "<span style='color: #8b0000;" + bold + "'>(" + change.toString.replace("-", "–") + ") "; } }

function blockedUser(user) { user = user.split("User:")[1]; if (isIPv6(user) || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(user)) { if (user.includes("/")) { return "<a href='/wiki/Special:Contributions/" + user + "'>" + user + "</a>"; }		return "<a href='/wiki/Special:Contributions/" + user + "'>" + user + "</a> (<a href='/wiki/User_talk:" + user + "'>talk</a>)"; }

return "<a href='/wiki/User:" + user + "'>" + user + "</a> (<a href='/wiki/User_talk:" + user + "'>talk</a> | <a href='/wiki/Special:Contributions/" + user + "'>contribs</a>)"; }

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); }

function saveSettings {

}

function loadSettings { const namespaceBoxes = [...document.getElementsByClassName("namespace-checkbox")]; const logBoxes = [...document.getElementsByClassName("log-checkbox")];

for (let box of namespaceBoxes) { const namespace = box.id.replace("namespace-", "").replaceAll("_", " "); for (let ns of namespaceList) { if (ns.name === namespace) { ns.active = box.checked; }		}	}

loadContribs; }

function getNamespaceString { return namespaceList.filter(ns => ns.active).map(ns => ns.id).join("|"); }