Greasy Fork 还支持 简体中文。

Discord/Shapes - Main Logic

Handling the logic of Rules and Lorebook

  1. // ==UserScript==
  2. // @name Discord/Shapes - Main Logic
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2
  5. // @description Handling the logic of Rules and Lorebook
  6. // @author Vishanka
  7. // @match https://discord.com/channels/*
  8. // @grant unsafeWindow
  9. // @run-at document-idle
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Function to check localStorage and reload if not ready
  16. function checkLocalStorageAndReload() {
  17. try {
  18. if (localStorage.length > 0) {
  19. console.log("LocalStorage has items. Proceeding with script...");
  20. initializeScript();
  21. } else {
  22. console.warn("LocalStorage is empty. Reloading page...");
  23. setTimeout(() => {
  24. location.reload();
  25. }, 5000); // Wait 5 seconds before reloading
  26. }
  27. } catch (error) {
  28. console.error("Error accessing localStorage:", error);
  29. setTimeout(() => {
  30. location.reload();
  31. }, 5000); // Wait 5 seconds before reloading
  32. }
  33. }
  34.  
  35. // Initial check for localStorage existence
  36. checkLocalStorageAndReload();
  37.  
  38. function initializeScript() {
  39. // Retrieve settings from localStorage or set default values
  40. let enterKeyDisabled = JSON.parse(localStorage.getItem('enterKeyDisabled')) || false;
  41. let customRuleEnabled = JSON.parse(localStorage.getItem('customRuleEnabled')) || true;
  42. let scanForKeywordsEnabled = JSON.parse(localStorage.getItem('scanForKeywordsEnabled')) || true;
  43.  
  44. // Create the settings window
  45. unsafeWindow.settingsWindow = document.createElement('div');
  46. // settingsWindow.style.position = 'fixed';
  47. settingsWindow.style.bottom = '60px';
  48. settingsWindow.style.right = '20px';
  49. settingsWindow.style.width = '250px';
  50. settingsWindow.style.padding = '15px';
  51. // settingsWindow.style.backgroundColor = '#2f3136';
  52. settingsWindow.style.color = 'white';
  53. // settingsWindow.style.border = '1px solid #5865F2';
  54. settingsWindow.style.borderRadius = '5px';
  55. // settingsWindow.style.display = 'none';
  56. settingsWindow.style.zIndex = '1001';
  57. DCstoragePanel.appendChild(settingsWindow);
  58. // Custom Rule Checkbox
  59. const enableCustomRuleCheckbox = document.createElement('input');
  60. enableCustomRuleCheckbox.type = 'checkbox';
  61. enableCustomRuleCheckbox.checked = customRuleEnabled;
  62. enableCustomRuleCheckbox.id = 'enableCustomRuleCheckbox';
  63.  
  64. const enableCustomRuleLabel = document.createElement('label');
  65. enableCustomRuleLabel.htmlFor = 'enableCustomRuleCheckbox';
  66. enableCustomRuleLabel.innerText = ' Enable Custom Rules';
  67.  
  68. // Scan for Keywords Checkbox
  69. const enableScanForKeywordsCheckbox = document.createElement('input');
  70. enableScanForKeywordsCheckbox.type = 'checkbox';
  71. enableScanForKeywordsCheckbox.checked = scanForKeywordsEnabled;
  72. enableScanForKeywordsCheckbox.id = 'enableScanForKeywordsCheckbox';
  73.  
  74. const enableScanForKeywordsLabel = document.createElement('label');
  75. enableScanForKeywordsLabel.htmlFor = 'enableScanForKeywordsCheckbox';
  76. enableScanForKeywordsLabel.innerText = ' Enable Lorebook';
  77.  
  78. // Append elements to settings window
  79. settingsWindow.appendChild(enableCustomRuleCheckbox);
  80. settingsWindow.appendChild(enableCustomRuleLabel);
  81. settingsWindow.appendChild(document.createElement('br'));
  82. settingsWindow.appendChild(enableScanForKeywordsCheckbox);
  83. settingsWindow.appendChild(enableScanForKeywordsLabel);
  84. // document.body.appendChild(settingsWindow);
  85.  
  86.  
  87. // Update customRuleEnabled when checkbox is toggled, and save it in localStorage
  88. enableCustomRuleCheckbox.addEventListener('change', function() {
  89. customRuleEnabled = enableCustomRuleCheckbox.checked;
  90. localStorage.setItem('customRuleEnabled', JSON.stringify(customRuleEnabled));
  91. });
  92.  
  93. // Update scanForKeywordsEnabled when checkbox is toggled, and save it in localStorage
  94. enableScanForKeywordsCheckbox.addEventListener('change', function() {
  95. scanForKeywordsEnabled = enableScanForKeywordsCheckbox.checked;
  96. localStorage.setItem('scanForKeywordsEnabled', JSON.stringify(scanForKeywordsEnabled));
  97. });
  98.  
  99.  
  100. // Function to get the correct input element based on the mode
  101. // Function to get the correct input element based on the mode
  102. // Function to get the correct input element based on the mode
  103. function getInputElement() {
  104. return document.querySelector('[data-slate-editor="true"]') || document.querySelector('textarea[class*="textArea_"]');
  105. }
  106.  
  107. // Add event listener to handle Enter key behavior
  108. window.addEventListener('keydown', function(event) {
  109. const inputElement = getInputElement();
  110.  
  111. if (event.key === 'Enter' && !event.shiftKey && !enterKeyDisabled) {
  112. if (inputElement && inputElement.nodeName === 'TEXTAREA') {
  113. // Mobile version: Only allow line break
  114. return;
  115. }
  116.  
  117. event.preventDefault();
  118. event.stopPropagation();
  119. event.stopImmediatePropagation();
  120. console.log('Enter key disabled');
  121. enterKeyDisabled = true;
  122.  
  123. // Execute main handler for Enter key
  124. handleEnterKey();
  125.  
  126. enterKeyDisabled = false;
  127. }
  128. }, true); // Use capture mode to intercept the event before Discord's handlers
  129.  
  130. // Add event listener to the send button to execute handleEnterKey when clicked
  131. window.addEventListener('click', function(event) {
  132. const sendButton = document.querySelector('button[aria-label="Nachricht senden"]');
  133. if (sendButton && sendButton.contains(event.target)) {
  134. // Execute main handler for Enter key
  135. handleEnterKey();
  136. }
  137. }, true);
  138.  
  139. // Main function that handles Enter key behavior
  140. function handleEnterKey() {
  141. let inputElement = getInputElement();
  142. if (inputElement) {
  143. inputElement.focus();
  144. setCursorToEnd(inputElement);
  145. if (customRuleEnabled) {
  146. applyCustomRule(inputElement);
  147. }
  148. setCursorToEnd(inputElement);
  149. if (scanForKeywordsEnabled) {
  150. scanForKeywords(inputElement);
  151. }
  152. getRandomEntry(inputElement);
  153. sendMessage(inputElement);
  154. anotherCustomFunction();
  155. }
  156. }
  157.  
  158. // Function to apply custom rules for the input field
  159. function applyCustomRule(inputElement) {
  160. const customRule = unsafeWindow.customRuleLogic ? unsafeWindow.customRuleLogic.getCurrentText() : '';
  161.  
  162. if (inputElement.nodeName === 'TEXTAREA') {
  163. // For mobile version where input is <textarea>
  164. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  165. nativeInputValueSetter.call(inputElement, inputElement.value + customRule);
  166.  
  167. const inputEvent = new Event('input', {
  168. bubbles: true,
  169. cancelable: true,
  170. });
  171. inputElement.dispatchEvent(inputEvent);
  172. } else {
  173. // For desktop version where input is a Slate editor
  174. const inputEvent = new InputEvent('beforeinput', {
  175. bubbles: true,
  176. cancelable: true,
  177. inputType: 'insertText',
  178. data: customRule,
  179. });
  180. inputElement.dispatchEvent(inputEvent);
  181. }
  182. }
  183.  
  184. // Function to set the cursor position to the end after inserting the text
  185. function setCursorToEnd(inputElement) {
  186. if (inputElement.nodeName === 'TEXTAREA') {
  187. // For mobile version where input is <textarea>
  188. inputElement.selectionStart = inputElement.selectionEnd = inputElement.value.length;
  189. } else {
  190. // For desktop version where input is a Slate editor
  191. inputElement.focus();
  192.  
  193. // Simulate repeated Ctrl + ArrowRight key press events to move cursor to the end
  194. const repeatPresses = 150; // Number of times to simulate Ctrl + ArrowRight
  195. for (let i = 0; i < repeatPresses; i++) {
  196. const ctrlArrowRightEvent = new KeyboardEvent('keydown', {
  197. key: 'ArrowRight',
  198. code: 'ArrowRight',
  199. keyCode: 39, // ArrowRight key code
  200. charCode: 0,
  201. which: 39,
  202. bubbles: true,
  203. cancelable: true,
  204. ctrlKey: true // Set Ctrl key to true
  205. });
  206. inputElement.dispatchEvent(ctrlArrowRightEvent);
  207. }
  208. }
  209. }
  210.  
  211. // Function to send the message (either click send button or simulate Enter key)
  212. function sendMessage(inputElement) {
  213. if (inputElement.nodeName === 'TEXTAREA') {
  214. // Mobile version: Do not send message, just return to allow linebreak
  215. return;
  216. }
  217.  
  218. let sendButton = document.querySelector('button[aria-label="Nachricht senden"]');
  219. if (sendButton) {
  220. sendButton.click();
  221. console.log('Send button clicked to send message');
  222. } else {
  223. // For desktop version, simulate pressing Enter to send the message
  224. let enterEvent = new KeyboardEvent('keydown', {
  225. key: 'Enter',
  226. code: 'Enter',
  227. keyCode: 13,
  228. which: 13,
  229. bubbles: true,
  230. cancelable: true
  231. });
  232. inputElement.dispatchEvent(enterEvent);
  233. console.log('Enter key simulated to send message');
  234. }
  235. }
  236.  
  237.  
  238.  
  239. // Example of adding another function
  240. function anotherCustomFunction() {
  241. console.log('Another custom function executed');
  242. }
  243.  
  244. // Function to scan for keywords and access local storage
  245. function scanForKeywords(inputElement) {
  246. const currentProfile = getCurrentProfile();
  247. if (currentProfile) {
  248. // Retrieve all messages before iterating through storage keys
  249. const messageItems = document.querySelectorAll('div[class*="messageContent_"]');
  250. let relevantMessages = Array.from(messageItems).slice(-15); // Last 15 messages
  251. const lastMessage = Array.from(messageItems).slice(-1); // Last message only
  252.  
  253.  
  254. // Iterate over the last 15 messages to extract hidden bracket content
  255. relevantMessages = relevantMessages.map(msg => {
  256. // Retrieve all span elements within the message
  257. const spans = msg.querySelectorAll('span');
  258.  
  259. // Filter out the spans based on both style conditions: opacity and position
  260. const hiddenSpans = Array.from(spans).filter(span => {
  261. const style = window.getComputedStyle(span);
  262. return style.opacity === '0' && style.position === 'absolute';
  263. });
  264.  
  265. // Join the text content of all matching spans
  266. const bracketContent = hiddenSpans.map(span => span.textContent).join('');
  267.  
  268. // Extract content within square brackets, if any
  269. const match = bracketContent.match(/\[(.*?)\]/);
  270. return match ? match[1] : null;
  271. }).filter(content => content !== null);
  272.  
  273. // Log the filtered messages for debugging purposes
  274. console.log("Filtered Relevant Messages (content in brackets, last 15):", relevantMessages);
  275. console.log("Last Message:", lastMessage.map(msg => msg.textContent));
  276.  
  277. // Track how many entries have been appended
  278. let appendedCount = 0;
  279. const maxAppends = 3;
  280.  
  281. // Check if the last message contains a specific link pattern
  282. const mp3LinkPattern = /https:\/\/files\.shapes\.inc\/.*\.mp3/;
  283. let mp3LinkValue = null;
  284.  
  285. if (lastMessage.length > 0) {
  286. const lastMessageText = lastMessage[0].textContent;
  287. const mp3LinkMatch = lastMessageText.match(mp3LinkPattern);
  288. if (mp3LinkMatch) {
  289. const mp3LinkKey = mp3LinkMatch[0];
  290. mp3LinkValue = localStorage.getItem(mp3LinkKey);
  291. console.log(`MP3 Link detected: ${mp3LinkKey}. Retrieved value: ${mp3LinkValue}`);
  292. }
  293. }
  294.  
  295. // Create an array to hold all entry keys that need to be checked
  296. let allEntryKeys = [];
  297.  
  298. // Iterate through all localStorage keys that match the profile-lorebook prefix
  299. Object.keys(localStorage).forEach(storageKey => {
  300. if (storageKey.startsWith(`${currentProfile}-lorebook:`)) {
  301. const entryKeys = storageKey.replace(`${currentProfile}-lorebook:`, '').split(',');
  302. const entryValue = localStorage.getItem(storageKey);
  303.  
  304. // Log the entry keys for debugging purposes
  305. console.log(`Entry Keys: `, entryKeys);
  306.  
  307. entryKeys.forEach(entryKey => {
  308. allEntryKeys.push({ entryKey, entryValue });
  309. });
  310. }
  311. });
  312.  
  313. // If mp3LinkValue is present, parse it for keywords as well
  314. if (mp3LinkValue) {
  315. console.log(`Scanning MP3 link value for keywords: ${mp3LinkValue}`);
  316. const mp3Keywords = mp3LinkValue.split(',');
  317. mp3Keywords.forEach(keyword => {
  318. const trimmedKeyword = keyword.trim();
  319. console.log(`Adding keyword from MP3 value: ${trimmedKeyword}`);
  320. // Add mp3 keywords but set entryValue to an empty string instead of null
  321. allEntryKeys.push({ entryKey: trimmedKeyword, entryValue: '' });
  322. });
  323. }
  324.  
  325. // Iterate over all collected entry keys and perform the checks
  326. allEntryKeys.forEach(({ entryKey, entryValue }) => {
  327. if (appendedCount >= maxAppends) return; // Stop if max appends reached
  328.  
  329. // Log each keyword being checked
  330. console.log(`Checking keyword: ${entryKey}`);
  331.  
  332. // Check input element text for complete word match of keyword (case-insensitive)
  333. const inputText = inputElement.value || inputElement.textContent;
  334.  
  335. // Combine check for keyword in input, in the last message, or in the mp3 link value
  336. const isKeywordInInput = inputText && new RegExp(`\\b${entryKey}\\b`, 'i').test(inputText);
  337. const isKeywordInLastMessage = lastMessage.some(message => {
  338. const lastMessageText = message.textContent;
  339. return new RegExp(`\\b${entryKey}\\b`, 'i').test(lastMessageText);
  340. });
  341. const isKeywordInMp3LinkValue = mp3LinkValue && mp3LinkValue.includes(entryKey);
  342.  
  343. console.log(`Keyword '${entryKey}' in input: ${isKeywordInInput}, in last message: ${isKeywordInLastMessage}, in MP3 value: ${isKeywordInMp3LinkValue}`);
  344.  
  345. if ((isKeywordInInput || isKeywordInLastMessage || isKeywordInMp3LinkValue) && entryValue) {
  346. const keywordAlreadyUsed = relevantMessages.some(bracketContent => {
  347. return new RegExp(`\\b${entryKey}\\b`, 'i').test(bracketContent);
  348. });
  349.  
  350. if (!keywordAlreadyUsed) {
  351. // Append the entryValue to the input element only if entryValue is not null or empty
  352. if (inputElement.nodeName === 'TEXTAREA') {
  353. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  354. nativeInputValueSetter.call(inputElement, inputElement.value + '\n' + entryValue);
  355.  
  356. const inputEvent = new Event('input', {
  357. bubbles: true,
  358. cancelable: true,
  359. });
  360. inputElement.dispatchEvent(inputEvent);
  361. } else {
  362. const inputEvent = new InputEvent('beforeinput', {
  363. bubbles: true,
  364. cancelable: true,
  365. inputType: 'insertText',
  366. data: '\n' + entryValue,
  367. });
  368. inputElement.dispatchEvent(inputEvent);
  369. }
  370. appendedCount++; // Increment the count
  371. console.log(`Keyword '${entryKey}' detected. Appended lorebook entry to the input.`);
  372. } else {
  373. console.log(`Keyword '${entryKey}' already found in recent bracketed messages or entryValue is null/empty. Skipping append.`);
  374. }
  375. }
  376. });
  377.  
  378. // Log the total number of entries appended
  379. console.log(`Total lorebook entries appended: ${appendedCount}`);
  380. }
  381. }
  382.  
  383.  
  384. // Function to get the current profile from local storage
  385. function getCurrentProfile() {
  386. return localStorage.getItem('currentProfile');
  387. }
  388.  
  389.  
  390. function getRandomEntry(inputElement) {
  391. const selectedProfile = localStorage.getItem('events.selectedProfile');
  392. if (selectedProfile) {
  393. let profileEntries = [];
  394. const currentHour = new Date().getHours();
  395. for (let key in localStorage) {
  396. if (key.startsWith(`events.${selectedProfile}:`)) {
  397. const entryData = JSON.parse(localStorage.getItem(key));
  398. const [startHour, endHour] = entryData.timeRange.split('-').map(Number);
  399. // Check if current hour is within the time range
  400. if (
  401. (startHour <= endHour && currentHour >= startHour && currentHour < endHour) || // Normal range
  402. (startHour > endHour && (currentHour >= startHour || currentHour < endHour)) // Crosses midnight
  403. ) {
  404. profileEntries.push({ key, ...entryData });
  405. }
  406. }
  407. }
  408.  
  409. if (profileEntries.length > 0) {
  410. const probability = parseInt(localStorage.getItem('events.probability') || '100', 10);
  411. let selectedEntry = null;
  412. while (profileEntries.length > 0) {
  413. // Randomly select an entry from the available entries
  414. const randomIndex = Math.floor(Math.random() * profileEntries.length);
  415. const randomEntry = profileEntries[randomIndex];
  416. // Check if the entry passes the individual probability check
  417. if (Math.random() * 100 < randomEntry.probability) {
  418. selectedEntry = randomEntry;
  419. break;
  420. } else {
  421. // Remove the entry from the list if it fails the probability check
  422. profileEntries.splice(randomIndex, 1);
  423. }
  424. }
  425. if (selectedEntry && Math.random() * 100 < probability) {
  426. console.log(`Random Entry Selected: ${selectedEntry.value}`);
  427. appendToInput(inputElement, selectedEntry.value); // Append the random entry to the input element
  428. }
  429. } else {
  430. console.log('No entries available for the selected profile in the current time range.');
  431. }
  432. } else {
  433. console.log('No profile selected. Please select a profile to retrieve a random entry.');
  434. }
  435. }
  436.  
  437. // Helper function to append text to the input element
  438. function appendToInput(inputElement, text) {
  439. const lineBreak = '\n';
  440. if (!inputElement) {
  441. console.error('Input element not found.');
  442. return;
  443. }
  444.  
  445. if (inputElement.nodeName === 'TEXTAREA') {
  446. // Mobile: Append text to <textarea>
  447. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  448. nativeInputValueSetter.call(inputElement, inputElement.value + `${lineBreak}${text}`);
  449.  
  450. const inputEvent = new Event('input', {
  451. bubbles: true,
  452. cancelable: true,
  453. });
  454. inputElement.dispatchEvent(inputEvent);
  455. } else if (inputElement.hasAttribute('data-slate-editor')) {
  456. // Desktop: Append text for Slate editor
  457. const inputEvent = new InputEvent('beforeinput', {
  458. bubbles: true,
  459. cancelable: true,
  460. inputType: 'insertText',
  461. data: `${lineBreak}${text}`,
  462. });
  463. inputElement.dispatchEvent(inputEvent);
  464. } else {
  465. console.error('Unsupported input element type.');
  466. }
  467. }
  468.  
  469.  
  470.  
  471. // Expose the function to be accessible from other scripts
  472. unsafeWindow.getRandomEntry = getRandomEntry;
  473.  
  474.  
  475. }
  476. })();