/*
* 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");
})();