MAL Conversation Button Adder (Async, DOMParser)

Adds a conversation button to each comment on a MAL profile using async/await and DOMParser to extract numerical user IDs.

  1. // ==UserScript==
  2. // @name MAL Conversation Button Adder (Async, DOMParser)
  3. // @namespace ConversationMAL
  4. // @version 2
  5. // @description Adds a conversation button to each comment on a MAL profile using async/await and DOMParser to extract numerical user IDs.
  6. // @author Indochina
  7. // @match https://myanimelist.net/profile/*
  8. // @match https://myanimelist.net/comments.php?id=*
  9. // @grant none
  10. // @run-at document-end
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (async function() {
  15. 'use strict';
  16.  
  17. // --- STEP 1: Extract the Profile Owner’s Numerical ID ---
  18. // Look for the Report link and extract the "id" parameter.
  19. const reportLink = document.querySelector('a.header-right[href*="/modules.php?go=report"]');
  20. let ownerId = null;
  21. if (reportLink) {
  22. const match = reportLink.href.match(/id=(\d+)/);
  23. if (match) {
  24. ownerId = match[1];
  25. console.log(`Profile owner ID extracted: ${ownerId}`);
  26. }
  27. }
  28. if (!ownerId) {
  29. console.error("Could not determine the profile owner's numerical ID from the Report link.");
  30. return;
  31. }
  32.  
  33. // --- STEP 2: Define a helper to fetch and extract a commenter's numerical ID ---
  34. // We use a cache to avoid refetching the same profile.
  35. const userIdCache = {};
  36.  
  37. async function getUserId(username) {
  38. if (userIdCache[username]) return userIdCache[username];
  39. try {
  40. // Fetch the commenter's profile page
  41. const response = await fetch(`https://myanimelist.net/profile/${username}`);
  42. const text = await response.text();
  43.  
  44. // Parse the HTML response to a document
  45. const parser = new DOMParser();
  46. const doc = parser.parseFromString(text, 'text/html');
  47.  
  48. // Look for the Report link on the fetched profile page.
  49. // (This should work for profiles that include it.)
  50. const reportLink = doc.querySelector('a.header-right[href*="/modules.php?go=report"]');
  51. if (reportLink) {
  52. const match = reportLink.href.match(/id=(\d+)/);
  53. if (match) {
  54. const userId = match[1];
  55. userIdCache[username] = userId;
  56. console.log(`Extracted user id for ${username} via report link: ${userId}`);
  57. return userId;
  58. }
  59. }
  60.  
  61. // If the Report link wasn't found, log an error.
  62. console.error(`Could not extract user_id for ${username}`);
  63. return null;
  64. } catch (error) {
  65. console.error(`Error fetching profile for ${username}:`, error);
  66. return null;
  67. }
  68. }
  69.  
  70. // --- STEP 3: Process each comment ---
  71. // We target each comment container. In many MAL pages, the comment content is within a <div class="text"> element.
  72. const commentDivs = document.querySelectorAll('div.text');
  73. for (const commentDiv of commentDivs) {
  74. // Find the comment author’s profile link; usually an <a class="fw-b"> element.
  75. const profileLink = commentDiv.querySelector('a.fw-b[href*="/profile/"]');
  76. if (!profileLink) continue;
  77.  
  78. // Extract the username from the URL.
  79. // For example: "https://myanimelist.net/profile/Indochina"
  80. const urlParts = profileLink.href.split('/profile/');
  81. if (urlParts.length < 2) continue;
  82. const username = urlParts[1].split('?')[0];
  83. console.log(`Processing comment by: ${username}`);
  84.  
  85. // Find or create the container for action links.
  86. let actionsDiv = commentDiv.querySelector('div.postActions');
  87. if (!actionsDiv) {
  88. actionsDiv = document.createElement("div");
  89. actionsDiv.className = "postActions ar mt4";
  90. actionsDiv.style.clear = "both";
  91. actionsDiv.style.paddingTop = "10px";
  92. commentDiv.appendChild(actionsDiv);
  93. }
  94.  
  95. // If a Conversation link already exists, skip this comment.
  96. if (actionsDiv.querySelector('a.ml8[href*="comtocom.php"]')) {
  97. console.log(`Conversation link already exists for ${username}`);
  98. continue;
  99. }
  100.  
  101. // --- STEP 4: Fetch the commenter's numerical ID and create the button ---
  102. const commenterId = await getUserId(username);
  103. if (!commenterId) continue;
  104.  
  105. // Create the Conversation button.
  106. // id1 is the profile owner's id and id2 is the commenter's id.
  107. const convoLink = document.createElement("a");
  108. convoLink.className = "ml8";
  109. convoLink.href = `https://myanimelist.net/comtocom.php?id1=${ownerId}&id2=${commenterId}`;
  110. convoLink.textContent = "Conversation";
  111. convoLink.style.marginLeft = "10px";
  112.  
  113. // Append the button to the actions container.
  114. actionsDiv.appendChild(convoLink);
  115. }
  116. })();