User:DVRTed/multiContribs.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:DVRTed/multiContribs. |
/*
multiContribs.js
- allows viewing contributions of multiple users in one page: [[Special:BlankPage/MultiContribs]];
- adds a link to "multiContribs" tool in SPI pages;
*/
/* global mw, $ */
(() => {
const RUN_PAGE = "Special:BlankPage/MultiContribs";
const RUN_NS = -1;
class MultiContribs {
constructor() {
if (
mw.config.get("wgNamespaceNumber") !== RUN_NS ||
mw.config.get("wgPageName").toLowerCase() !== RUN_PAGE.toLowerCase()
) {
return;
}
this.content_div = document.getElementById("content");
this.namespaces = [
{ id: "", name: "All namespaces" },
{ id: "0", name: "Main (articles)" },
{ id: "1", name: "Talk" },
{ id: "2", name: "User" },
{ id: "3", name: "User talk" },
{ id: "4", name: "Wikipedia" },
{ id: "5", name: "Wikipedia talk" },
{ id: "6", name: "File" },
{ id: "7", name: "File talk" },
{ id: "8", name: "MediaWiki" },
{ id: "9", name: "MediaWiki talk" },
{ id: "10", name: "Template" },
{ id: "11", name: "Template talk" },
{ id: "12", name: "Help" },
{ id: "13", name: "Help talk" },
{ id: "14", name: "Category" },
{ id: "15", name: "Category talk" },
{ id: "100", name: "Portal" },
{ id: "101", name: "Portal talk" },
{ id: "118", name: "Draft" },
{ id: "119", name: "Draft talk" },
{ id: "828", name: "Module" },
{ id: "829", name: "Module talk" },
];
this.limits = [10, 25, 50, 100, 250, 500];
this.init();
}
init() {
document.title = "Contributions of multiple users";
this.load_styles();
this.render_header();
this.bind_events();
this.load_from_url();
}
load_styles() {
mw.loader.load(["mediawiki.interface.helpers.styles"]);
const style = document.createElement("style");
style.textContent = `
.mw-uctop {
font-weight: bold;
}
.mctb-option {
margin: 10px 0;
}
#mctb-form {
border: 1px solid #a2a9b1;
padding: 15px;
margin: 10px 0;
background-color: #f8f9fa;
}
#mctb-form label {
font-weight: bold;
margin-right: 10px;
}
#mctb-form select,
#mctb-form input[type="checkbox"] {
margin-right: 15px;
}
.mw-tag-markers {
font-size: 0.8em;
color: #0645ad;
margin-right: 5px;
}
.mw-tag-markers abbr {
border-bottom: 1px dotted;
cursor: help;
}
`;
document.head.appendChild(style);
}
render_header() {
this.content_div.innerHTML = `
<div class="vector-body">
<h1>Contributions of multiple users</h1>
<div id="mctb-form">
<label for="users-input">Users/IPs (one per line):</label><br>
<textarea id="users-input" rows="5" cols="50" placeholder="Enter usernames or IP addresses, one per line"></textarea>
<div class="mctb-option">
<label for="limit-input">Results per user:</label>
<select id="limit-input">
${this.limits
.map((limit) => `<option value="${limit}">${limit}</option>`)
.join("")}
</select>
</div>
<div class="mctb-option">
<label for="namespace-input">Namespace:</label>
<select id="namespace-input">
${this.namespaces
.map((ns) => `<option value="${ns.id}">${ns.name}</option>`)
.join("")}
</select>
</div>
<div class="mctb-option">
<label>
<input type="checkbox" id="show-new-only"> Show only page creations
</label>
</div>
<button id="load-contribs" class="mw-ui-button mw-ui-progressive">Load Contributions</button>
</div>
<div id="mctb-results" class="mctb-option"></div>
</div>
`;
}
bind_events() {
document.getElementById("load-contribs").addEventListener("click", () => {
this.load_contributions();
});
}
load_from_url() {
const params = new URLSearchParams(window.location.search);
if (params.has("users")) {
document.getElementById("users-input").value = params
.get("users")
.split(",")
.join("\n");
}
document.getElementById("limit-input").value = 50;
if (params.has("limit")) {
const limit = params.get("limit");
const is_valid_limit = this.limits.includes(parseInt(limit));
document.getElementById("limit-input").value = is_valid_limit
? limit
: "50";
}
if (params.has("namespace")) {
const namespace = params.get("namespace");
const valid_namespaces = this.namespaces.map((ns) => ns.id);
document.getElementById("namespace-input").value =
valid_namespaces.includes(namespace) ? namespace : "";
}
if (params.has("new")) {
const new_param = params.get("new");
document.getElementById("show-new-only").checked =
new_param === "1" || new_param === "true";
}
if (params.has("users")) {
this.load_contributions();
}
}
update_url() {
const users = document
.getElementById("users-input")
.value.trim()
.split("\n")
.filter((u) => u.trim());
const limit = document.getElementById("limit-input").value;
const namespace = document.getElementById("namespace-input").value;
const show_new_only = document.getElementById("show-new-only").checked;
const params = new URLSearchParams();
if (users.length > 0) {
params.set("users", users.join(","));
}
if (limit !== "50") {
params.set("limit", limit);
}
if (namespace !== "") {
params.set("namespace", namespace);
}
if (show_new_only) {
params.set("new", "1");
}
const new_url =
window.location.pathname +
(params.toString() ? "?" + params.toString() : "");
window.history.replaceState({}, "", new_url);
}
async load_contributions() {
const users = document
.getElementById("users-input")
.value.trim()
.split("\n")
.filter((u) => u.trim());
const results_div = document.getElementById("mctb-results");
if (users.length === 0) {
results_div.innerHTML =
"<p>Please enter at least one username or IP address.</p>";
return;
}
this.update_url();
const limit = parseInt(document.getElementById("limit-input").value);
const namespace = document.getElementById("namespace-input").value;
const show_new_only = document.getElementById("show-new-only").checked;
results_div.innerHTML = "<p>Loading contributions...</p>";
try {
const all_contribs = [];
for (const user of users) {
const api = new mw.Api();
const params = {
action: "query",
list: "usercontribs",
ucuser: user.trim(),
uclimit: limit,
ucprop: "ids|title|timestamp|comment|size|flags|sizediff",
};
if (namespace !== "") {
params.ucnamespace = namespace;
}
if (show_new_only) {
params.ucshow = "new";
}
const result = await api.get(params);
if (result.query.usercontribs) {
result.query.usercontribs.forEach((contrib) => {
contrib.user = user.trim();
all_contribs.push(contrib);
});
}
}
all_contribs.sort(
(a, b) => new Date(b.timestamp) - new Date(a.timestamp)
);
this.render_results(all_contribs, results_div);
} catch (error) {
results_div.innerHTML =
"<p>Error loading contributions: " + error.message + "</p>";
}
}
render_results(contribs, results_div) {
if (contribs.length === 0) {
results_div.innerHTML =
"<p>No contributions found with the selected filters.</p>";
return;
}
let html = `<p>Found ${contribs.length} contributions</p>
<ul class="mw-contributions-list">`;
contribs.forEach((contrib) => {
const full_date_time = this.format_timestamp(contrib.timestamp);
let flags = []; // might add more in future
if (contrib.new)
flags.push('<abbr title="This edit created a new page">N</abbr>');
const flags_html =
flags.length > 0
? `<span class="mw-tag-markers">${flags.join(" ")}</span> `
: "";
// constructing actual list
//
html += `<li data-mw-revid="${contrib.revid}">`;
// diff and history links
html += `
<span class="mw-changeslist-links">
<span><a href="/w/index.php?title=${contrib.title}&diff=prev&oldid=${contrib.revid}"
class="mw-changeslist-diff" title="${contrib.title}">diff</a></span>
<span><a href="/w/index.php?title=${contrib.title}&action=history"
class="mw-changeslist-history" title="${contrib.title}">hist</a></span>
</span>
`;
// relevant user
html += `[<a href="/wiki/Special:Contributions/${contrib.user}" style="font-weight: bold;">${contrib.user}</a>]`;
// UTC date and time
html += `
<bdi dir="ltr">
<a href="/w/index.php?title=${contrib.title}&oldid=${contrib.revid}"
class="mw-changeslist-date" title="${contrib.title}">${full_date_time}</a>
</bdi>
`;
// separator and flags
html += `<span class="mw-changeslist-separator"></span>${flags_html}`;
// size diff
html += `
<span dir="ltr" class="mw-plusminus-pos mw-diff-bytes" title="${
contrib.size
} bytes after change">${contrib.sizediff || 0}</span>
<span class="mw-changeslist-separator"></span>`;
// title and edit summary
html += `
<bdi dir="ltr">
<a href="/wiki/${contrib.title}"
class="mw-contributions-title" title="${contrib.title}">${
contrib.title
}</a>
</bdi>
<span class="comment comment--without-parentheses">${
contrib.comment || ""
}</span>
`;
// if it's the current revision
if ("top" in contrib) {
html += `
<span class="mw-changeslist-separator"></span>
<span class="mw-uctop">current</span>
`;
}
html += `</li>`;
});
html += "</ul>";
results_div.innerHTML = html;
mw.hook("wikipage.content").fire($("body"));
}
format_timestamp(timestamp) {
const date = new Date(timestamp);
const hours = date.getUTCHours().toString().padStart(2, "0");
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
const day = date.getUTCDate();
const month = date.toLocaleDateString("en-US", {
month: "long",
timeZone: "UTC",
});
const year = date.getUTCFullYear();
return `${hours}:${minutes}, ${day} ${month} ${year}`;
}
}
new MultiContribs();
// multiContribs on suspected sockpuppets' lists
if (
mw.config
.get("wgPageName")
.startsWith("Wikipedia:Sockpuppet_investigations/")
) {
$("ul:has(span.cuEntry)").each(function () {
const users = $(this)
.find("span.cuEntry .plainlinks a")
.map(function () {
return $(this).text();
})
.get();
$(this)
.find("li")
.last()
.find("a")
.first()
.before(
`<a href="/wiki/${RUN_PAGE}?users=${encodeURIComponent(
users.join(",")
)}" style="font-style: italic;">multiContribs</a> <b>·</b> `
);
});
}
})();