User:DVRTed/RecordSpam.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/RecordSpam. |
/*
* 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");
})();