User:DVRTed/RecordSpam.js

From Wikipedia, the free encyclopedia
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.
/*
 * notes:
 ** prettier with default config is used to format this code;
 */

(async () => {
  // Page where the logs will be stored
  const LOG_PAGE = "User:DVRTed/SpamLog";
  /* global mw, $ */
  const API = new mw.Api();

  const sign = "(via [[User:DVRTed/RecordSpam.js|RecordSpam]])";

  /**
   * "Send" a message, i.e. write on the popup's info section
   * @param {string} message
   * @param {string} type Default is `error` that results in a red-background, otherwise blue-ish color is used
   * @returns
   */
  function send_message(message, type = "error") {
    if (!message) {
      $(".script_message").addClass("hidden");
      $(".script_message").html("");
      return;
    }

    $(".script_message").removeClass("hidden");
    $(".script_message").html(message);
    $(".script_message").removeClass("error");
    if (type == "error") {
      $(".script_message").addClass("error");
    }
  }

  /**
   * Get a list of sections
   * @returns array of sections w/ sub-sections
   */
  async function get_existing_sections() {
    // parse headings (sections) from `LOG_PAGE`
    const { parse } = await API.get({
      action: "parse",
      prop: ["sections"],
      page: LOG_PAGE,
      format: "json",
    });

    const main_sections = parse.sections
      // select level 2 sections, i.e. == heading ==
      .filter((sec) => parseInt(sec.level) === 2)
      .map((sec) => {
        return {
          ...sec,
          sub_sections: parse.sections.filter((s) =>
            s.number.startsWith(`${sec.number}.`)
          ),
        };
      });

    return main_sections;
  }

  /**
   * Checks if user is already exists under the relevant section
   * @param {int} section Section index
   * @param {string} user User/IP
   * @returns boolean
   */
  async function check_user_exists(section, user) {
    const { parse } = await API.get({
      action: "parse",
      prop: ["wikitext"],
      page: LOG_PAGE,
      section,
      format: "json",
    });
    const entries = parse.wikitext["*"].split("\n");

    const check_match = entries.find((entry) => {
      // dont bother running regex on empty string
      if (!entry) return false;

      const regex_match = entry.match(/^\*\s\{\{User\|(.*)\}\}/i);
      return regex_match && user === regex_match[1];
    });

    if (check_match) {
      return true;
    }
    return false;
  }

  /**
   * Adds new section for the spam hostname
   * If user is provided, adds user underneath the Users section
   * If user already exists under the relevant section, displays an error message
   * @param {string} spamlink
   * @param {string} user
   * @returns
   */
  async function add_entry(spamlink, user) {
    user = user?.trim();
    user = user.replace(/^User\s*:\s*/i, "");
    spamlink = spamlink?.trim();

    if (!spamlink) {
      send_message("URL input is empty!");
      return;
    }

    const spam_URL = new URL(spamlink);
    const host_name = spam_URL.hostname;

    const sections = await get_existing_sections();
    const relevant_section = sections.find((sec) => sec.line === host_name);

    if (relevant_section) {
      // hostname section exists
      send_message(
        "A section for the hostname <code>" +
          host_name +
          "</code> already exists." +
          (user ? "Appending user entry inside the section..." : ""),
        "info"
      );
      if (!user) {
        toggle_loading();
        toggle_buttons(false);
        return;
      }
      const users_section = relevant_section.sub_sections[1];

      if (await check_user_exists(users_section.index, user)) {
        send_message(
          "An entry for user <b>" +
            user +
            "</b> already exists on the section for <code>" +
            spam_URL.hostname +
            "</code>."
        );
        toggle_loading();
        toggle_buttons(false);
        return;
      }

      const { edit } = await API.postWithToken("csrf", {
        action: "edit",
        title: LOG_PAGE,

        section: users_section.index,
        appendtext: `\n* {{User|${user}}}`,
        summary: `Adding entry for [[Special:Contributions/${user}|${user}]] ${sign}`,

        format: "json",
      }).always(function () {
        toggle_loading();
        toggle_buttons(false);
      });

      if (edit.result === "Success") {
        send_message(
          "Successfully added entry for the user <b>" + user + "</b>",
          "info"
        );
      } else {
        console.error("Something went wrong while performing the edit:");
        console.error(edit);
        send_message("Something went wrong while performing the edit.");
      }

      return;
    }

    // hostname section doesn't exist; create a new one:
    const report_text =
      "=== Linkback ===\n" +
      `* {{Link summary|${host_name}}}\n` +
      `* HTTP: [http://${host_name}]\n` +
      `* HTTPS: [https://${host_name}]\n\n` +
      `=== Users ===\n` +
      // leave empty if no user is specified
      (user ? `* {{User|${user}}}\n` : "");

    let summary = `Creating new section with no user entry ${sign}`;

    if (user)
      summary = `Creating new section with added entry for [[Special:Contributions/${user}|${user}]] ${sign}`;

    const { edit } = await API.postWithToken("csrf", {
      action: "edit",
      title: LOG_PAGE,

      section: "new",
      sectiontitle: host_name,
      text: report_text,
      summary: summary,

      format: "json",
    }).always(function () {
      toggle_loading();
      toggle_buttons(false);
    });

    if (edit.result === "Success") {
      send_message(
        "Successfully created a new section" +
          (user ? " and added the user entry" : "") +
          "!",
        "info"
      );
    } else {
      console.error("Something went wrong while performing the edit:");
      console.error(edit);
      send_message("Something went wrong while performing the edit.");
    }
  }

  /**
   * Toggle loading animation on the submit button
   * @param {boolean} disable If set to true, disable loading animation else enable.
   */
  const toggle_loading = (disable = true) => {
    if (disable) $(".SpamLogDialog").find("#SubmitBtn").removeClass("loading");
    else $(".SpamLogDialog").find("#SubmitBtn").addClass("loading");
  };

  /**
   * Toggle b/w disabled and enabled state of all buttons
   * @param {boolean} disable If set to true, disable buttons else enable.
   */
  const toggle_buttons = (disable = true) => {
    if (disable) $(".SpamLogDialog").find("button").addClass("disabled");
    else $(".SpamLogDialog").find("button").removeClass("disabled");
  };

  /**
   * Toggle b/w disabled and enabled state of the submit button only
   * @param {boolean} disable If set to true, disable buttons else enable.
   */
  const toggle_submit_button = (disable = true) => {
    if (disable) $(".SpamLogDialog").find("#SubmitBtn").addClass("disabled");
    else $(".SpamLogDialog").find("#SubmitBtn").removeClass("disabled");
  };

  function handle_events() {
    // Cancel button
    $(".SpamLogDialog")
      .find("#CancelBtn")
      .on("click", function () {
        toggle_buttons();
        $(".SpamLogDialog").addClass("hidden");
        setTimeout(() => {
          $("body").find(".SpamLogDialog").remove();
        }, 400);
      });

    // Submit button
    $(".SpamLogDialog")
      .find("#SubmitBtn")
      .on("click", async function () {
        send_message();
        toggle_buttons();
        toggle_loading(false);
        const cur_URL = $("#SpamURL").val();
        const cur_user = $("#SpamUser").val();

        await add_entry(cur_URL, cur_user);
      });

    // parse and write to hostname input
    $("#SpamURL").on("input", function () {
      try {
        const parse_url = new URL($("#SpamURL").val());
        if (!parse_url.hostname) throw "No hostname!";
        $("#SpamHostname").val(parse_url.hostname);
        toggle_submit_button(false);
      } catch {
        $("#SpamHostname").val("Invalid URL");
        toggle_submit_button();
      }
    });
  }

  const POPUP_HTML = `
<div class="SpamLogDialog hidden">
<div class="heading">
  <h1>Record Spam</h1>
  <button class="danger" id="CancelBtn">Cancel</button>
</div>
<div class="divider"></div>
<div class="mainContent">
  <label for="SpamUser">User or IP address (prefix "User:" is not necessary)</label>
  <input type="text" id="SpamUser" />
  <div class="spacer"></div>
  <div class="URL_detail">
    <div class="URL">
      <label for="SpamURL">Spam URL (e.g. https://somewebsite.com)</label>
      <input type="text" id="SpamURL" />
    </div>
    <div class="host">
      <label for="SpamHostname">Parsed hostname</label>
      <input type="text" id="SpamHostname" disabled />
    </div>
  </div>
</div>
<div class="script_message hidden">No message.</div>
<div class="divider"></div>

<div class="footer reported">
  <div class="txt">
    Recorded at: <a href="/wiki/${LOG_PAGE}">${LOG_PAGE}</a>
  </div>
  <button class="primary disabled" id="SubmitBtn">Submit</button>
</div>
</div>
`;

  // Add link "Record spam" under personal content-actions (? lol)
  const node = mw.util.addPortletLink(
    "p-cactions",
    "#",
    "Record spam",
    "spamrecord-usc"
  );

  // when the aforementioned link is clicked,
  $(node).on("click", function (e) {
    e.preventDefault();
    // remove if there's an existing dialog box
    $("body").find(".SpamLogDialog").remove();

    // inject html for the popup display
    $("body").append(POPUP_HTML);
    handle_events();

    const relevant_username = mw.config.get("wgRelevantUserName");
    if (relevant_username && relevant_username !== mw.config.get("wgUserName"))
      $(".SpamLogDialog").find("#SpamUser").val(relevant_username);

    // just for the kewl (cool) animation
    setTimeout(() => {
      $(".SpamLogDialog").removeClass("hidden");
    }, 0);
  });

  // CSS for the dialog box
  const CSS = `
.SpamLogDialog {
  font-family: Arial, Helvetica, sans-serif;
  background-color: white;
  position: fixed;
  border-radius: 10px;
  box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
  width: 800px;
  height: 400px;
  z-index: 99;
  left: 50%;
  transform: translate(-50%, 0);
  top: 10%;
  padding: 30px;
  opacity: 1;
  transition: all 0.2s ease-in-out;
}

.SpamLogDialog.hidden {
  left: 40%;
  opacity: 0;
}

.SpamLogDialog .mainContent {
  margin: 20 0px;
}

.SpamLogDialog .heading {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.SpamLogDialog .footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 10px;

}

.SpamLogDialog .txt {
  color: #808080;
}

.SpamLogDialog h1 {
  font-size: 19pt;
  border: none;
  font-weight: normal;
}

.SpamLogDialog .divider {
  border-bottom: 1px solid #e7e6e6;
}

.SpamLogDialog .spacer {
  margin: 40px 0;
}

.SpamLogDialog button {
  padding: 10px 20px;
  border-radius: 5px;
  position: relative;
  border: none;
  color: white;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.2s ease-in-out;
}

.SpamLogDialog button:hover {
  transform: translateY(-2px);
  box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.2);
}

button.danger {
  background-color: #580234;
}

button.danger:hover {
  background-color: #a0055f;
}

button.primary {
  background-color: #027740;
}

button.primary:hover {
  background-color: #08a15a;
}

.SpamLogDialog button.loading,
button.disabled {
  position: relative;
  pointer-events: none;
  background-color: rgb(143, 167, 167);
}

.SpamLogDialog button.loading:before {
  content: "";
  position: absolute;
  left: -30px;
  border: 2px solid rgb(0, 0, 0);
  border-top: 2px solid rgb(255, 0, 0);
  border-radius: 50%;
  width: 16px;
  height: 16px;
  animation: spinner 1s linear infinite;
}

@keyframes spinner {
  0% {
      transform: rotate(0deg);
  }

  100% {
      transform: rotate(360deg);
  }
}

.SpamLogDialog label {
  font-size: 12pt;
  display: block;
  margin: 10px 0;
}

.SpamLogDialog .URL_detail {
  display: flex;
  justify-content: space-between;
}

.SpamLogDialog input[type="text"] {
  padding: 10px;
  border-radius: 5px;
  border: none;
  border: 2px solid #ccc;
  font-size: 16px;
  width: 300px;
  transition: all 0.2s ease-in-out;
}

.SpamLogDialog input[type="text"]:focus {
  border-color: #4caf50;
  outline: none;
  box-shadow: 0px 0px 10px #4caf50;
}

.SpamLogDialog input#SpamURL {
  width: 400px;
}

.SpamLogDialog input#SpamHostname {
  background: rgb(216, 214, 214);
}

.SpamLogDialog .script_message {
  background: #1a6cb9;
  color: white;
  padding: 9px;
  width: 100%;
  margin: 20px 0;
  text-align: center;
  font-size: 14pt;
}

.SpamLogDialog code {
  font-size: 10pt;
}

.SpamLogDialog .script_message.hidden {
  visibility: hidden;
}

.SpamLogDialog .script_message.error {
  background: #d10034;
}`;

  // Load CSS
  mw.loader.addStyleTag(CSS, "text/css");
})();