Jump to content

User:DVRTed/multiContribs.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by DVRTed (talk | contribs) at 01:36, 14 July 2025 (add number_of_users_limit). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
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.number_of_users_limit = 50;
      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;
      }

      if (users.length > this.number_of_users_limit) {
        results_div.innerHTML = `<p>Exceeded the ${this.number_of_users_limit} users limit.</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> `
        );
    });
  }
})();