Greasy Fork is available in English.
Quickly know if you have ever received Private Messages or profile comments from specific users by opening the user's profile page or by hovering over the user image/name on any page on MAL.
// ==UserScript==
// @name Have we ever talked before? - MAL
// @namespace MALCuriosity
// @version 13
// @description Quickly know if you have ever received Private Messages or profile comments from specific users by opening the user's profile page or by hovering over the user image/name on any page on MAL.
// @author hacker09
// @match https://myanimelist.net/*
// @icon https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://myanimelist.net&size=64
// @run-at document-end
// @grant GM_setValue
// @grant GM_getValue
// @grant window.close
// @grant GM_listValues
// @grant GM.xmlHttpRequest
// ==/UserScript==
(async function() {
'use strict';
var newDocument, ProfileID; //Creates a new global variable
var HoveredUserName = ''; //Creates a new global variable
if (GM_getValue('ScriptUserName') === undefined) //If the variable ScriptUserName doesn't exist yet
{ //Starts the if condition
GM_setValue('ScriptUserName', document.querySelector("a.header-profile-link").innerText); //Save the User Name to the variable ScriptUserName
} //Finishes the if condition
async function GetComments() //Creates a function to get the user id and Starts the function
{ //Starts the function
if (location.href.split('/')[3] === 'profile' && location.href.split('/')[4] !== GM_getValue('ScriptUserName') && HoveredUserName === '') //If the opened page is a profile and if the profile username isn't of the account owner profile, and if the variable HoveredUserName is equal nothing
{ //Starts the if condition
ProfileID = document.querySelector("input[name*=profileMemId]") !== null ? document.querySelector("input[name*=profileMemId]").value : document.querySelector(".mr0").href.match(/\d+/g)[0]; //Save the user id to the variable ProfileID
} //Finishes the if condition
else //If the opened page isn't a profile page
{ //Starts the else condition
const response1 = await (await fetch('https://api.jikan.moe/v4/users/' + HoveredUserName)).json(); //Fetch
ProfileID = response1.data.mal_id; //Saves the user ID to the variable ProfileID
} //Finishes the else condition
const response = await (await fetch('https://myanimelist.net/comtocom.php?id1=' + ProfileID + '&id2=' + GM_getValue("ScriptUserID"))).text(); //Fetch
newDocument = new DOMParser().parseFromString(response, 'text/html'); //Parses the fetch response
} //Finishes the async function
document.querySelectorAll("a[href*='/profile/']").forEach(Element => Element.addEventListener("mouseover", async function() { //Get all the profile link elements and add an event listener to the link element
if (this.title.match('comments') === null) //If the element doesn't have the word comments on its title attribute
{ //Starts the if condition
HoveredUserName = this.href.split('/')[4]; //Save the hovered username to a variable
this.title = "Hover again to see the updated comments and PMs message."; //Add the text "comments" to the username/image, so that even if the element is hovered again too fast the fetch request won't happen again
await GetComments(); //Starts the function
if (newDocument.body.innerText.search("No comments found") > -1) //If the text "No comments found" exists
{ //Starts the if condition
this.title = "❌ There are no comments between you and this user!"; //Add a text to the username/image
} //Finishes the if condition
else //If the text "No comments found" doesn't exist
{ //Starts the else condition
this.title = "✅ There are comments between you and this user!"; //Add a text to the username/image
} //Finishes the else condition
if (GM_listValues().includes(HoveredUserName)) //If the current HoveredUserName is on the user PMed list
{ //Starts the if condition
this.title = this.title + "\n✅ There are PMs between you and this user!"; //Add a text to the username/image
} //Finishes the if condition
else //If the current HoveredUserName isn't on the user PMed list
{ //Starts the else condition
this.title = this.title + "\n❌ There are no PMs between you and this user!"; //Add a text to the username/image
} //Finishes the else condition
} //Finishes the if condition
})); //Finishes the forEach
if (location.href === 'https://myanimelist.net/mymessages.php' || (GM_getValue('ScriptUserID') === undefined && location.href.split('/')[4] === GM_getValue('ScriptUserName'))) //If the opened profile username is the account owner profile and if the variable ScriptUserID doesn't exist yet or if the opened URL is = 'https://myanimelist.net/mymessages.php'
{ //Starts the if condition
var array = []; //Creates a new global array
var nextpagenum = 0; //Creates a new variable
if (GM_getValue('ScriptUserID') === undefined && location.href.split('/')[4] === GM_getValue('ScriptUserName')) //If the opened profile username is the account owner profile and if the variable ScriptUserID doesn't exist yet
{ //Starts the if condition
const response1 = await (await fetch('https://api.jikan.moe/v4/users/' + location.href.split('/')[4])).json(); //Fetch
GM_setValue('ScriptUserID', response1.data.mal_id); //Save the user id to the variable ScriptUserID
} //Finishes the if condition
while (true) { //While the fetched page contains User Names
const url = 'https://myanimelist.net/mymessages.php?go=&show=' + nextpagenum; //Creates a variable to fetch the PM pages
var response = await (await fetch(url)).text(); //Fetches the PM pages and converts the fetched pages to text
const newDocument = new DOMParser().parseFromString(response, 'text/html'); //Parses the fetch response
nextpagenum += 20; //Increase the next page number by 20
if (location.href === 'https://myanimelist.net/mymessages.php') //If the opened URL is = 'https://myanimelist.net/mymessages.php'
{ //Starts the if condition
var DisplayedTotalPMs = 999999999; //Creates a new variable to make the script fetch only the 1-page
} //Finishes the if condition
else //If the opened URL isn't = 'https://myanimelist.net/mymessages.php'
{ //Starts the else condition
DisplayedTotalPMs = parseInt(newDocument.querySelector("div.di-ib").innerText.match(/\d+/g)[2]); //Creates a new variable
} //Finishes the else condition
for (const UserNames of newDocument.querySelectorAll("div.mym.mym_user")) { //For every single User Name that sent a PM to the user
array.push(UserNames.innerText); //Get and save the all mal User Names that sent PMs to the script user
} //Finishes the for condition
if (DisplayedTotalPMs >= parseInt(newDocument.querySelector("div.di-ib").innerText.match(/\d+/g)[0])) { //If the fetched page displayed messages total number is greater or equal the User total inbox messages number
array = [...new Set(array)]; //Remove the duplicated usernames of the array
array = array.filter(d => !GM_listValues().includes(d)); //Remove the duplicated usernames of the array comparing the usernames that the array has and tampermonkey is missing
array.forEach(name => GM_setValue(name, 'PMed MAL User Name')) //Get and save the current PMed MAL User Name
if (location.href !== 'https://myanimelist.net/mymessages.php') //If the opened URL isn't = 'https://myanimelist.net/mymessages.php'
{ //Starts the if condition
close(); //Close the current tab
} //Finishes the if condition
return; //Make the while condition false and stop fetching
} //Finishes the if condition
await new Promise(resolve => setTimeout(resolve, 600)); //Timeout to start the next fetch request
} //Finishes the while condition
} //Finishes the if condition
if (location.href.match(/https:\/\/myanimelist\.net\/profile\/[^\/]+(\/)?$/) !== null && location.href.split('/')[4] !== GM_getValue('ScriptUserName')) //If the opened page is a profile page and the profile username isn't of the script user profile
{ //Starts the if condition
const HasCommented = document.createElement("a"); //Creates an a element
HasCommented.setAttribute("id", "HasCommented"); //Adds the id HasCommented to the a element
HasCommented.setAttribute("style", "cursor: pointer; margin-right: 15%;"); //Set the CSS for the button
await GetComments(); //Starts the function
if (newDocument.body.innerText.search("No comments found") > -1) //If the text "No comments found" is found
{ //Starts the if condition
HasCommented.innerHTML = "❌"; //Add the text ❌ to the button
} //Finishes the if condition
else //If the text "No comments found" isn't found
{ //Starts the else condition
HasCommented.innerHTML = "✅"; //Add a text to the button
HasCommented.onclick = function() { //Detects the mouse click on the '✅' button
open(`https://myanimelist.net/comtocom.php?id1=${ProfileID}&id2=${GM_getValue("ScriptUserID")}`, '_self'); //Open the user comments page on the same tab
}; //Finishes the onclick event listener
} //Finishes the else condition
document.querySelector("#comment").parentElement.appendChild(HasCommented); //Shows the button
setTimeout(function() { //Starts the setTimeout function
document.querySelector("div.mt8 > input").parentElement.appendChild(document.querySelector("#HasCommented").cloneNode(true)); //Clone and append the HasCommented button
document.querySelectorAll("#HasCommented")[1].onclick = function() { //Detects the mouse click on the second HasCommented button
document.querySelector("#HasCommented").click(); //Click on the first HasCommented element
}; //Finishes the onclick event listener
}, 3000); //Finishes the setTimeout function
const HasPMed = document.createElement("a"); //Creates an a element
HasPMed.setAttribute("style", "cursor: pointer; margin-right: 47%;"); //Set the css for the button
HasPMed.innerHTML = GM_listValues().includes(location.href.split('/')[4]) === true ? "✅" : "❌"; //If the current opened profile User Name is on the user PMed list Add a text to the button
document.querySelector("#comment").parentElement.appendChild(HasPMed); //Shows the button
} //Finishes the if condition
})();