Discord/Shapes - Main Logic

Handling the logic of Rules and Lorebook

当前为 2024-11-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Discord/Shapes - Main Logic
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.1
  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 getInputElement() {
  103. return document.querySelector('[data-slate-editor="true"]') || document.querySelector('textarea[class*="textArea_"]');
  104. }
  105.  
  106. // Add event listener to handle Enter key behavior
  107. window.addEventListener('keydown', function(event) {
  108. const inputElement = getInputElement();
  109.  
  110. if (event.key === 'Enter' && !event.shiftKey && !enterKeyDisabled) {
  111. if (inputElement && inputElement.nodeName === 'TEXTAREA') {
  112. // Mobile version: Only allow line break
  113. return;
  114. }
  115.  
  116. event.preventDefault();
  117. event.stopPropagation();
  118. event.stopImmediatePropagation();
  119. console.log('Enter key disabled');
  120. enterKeyDisabled = true;
  121.  
  122. // Execute main handler for Enter key
  123. handleEnterKey();
  124.  
  125. enterKeyDisabled = false;
  126. }
  127. }, true); // Use capture mode to intercept the event before Discord's handlers
  128.  
  129. // Main function that handles Enter key behavior
  130. function handleEnterKey() {
  131. let inputElement = getInputElement();
  132. if (inputElement) {
  133. inputElement.focus();
  134. setCursorToEnd(inputElement);
  135. if (customRuleEnabled) {
  136. applyCustomRule(inputElement);
  137. }
  138. setCursorToEnd(inputElement);
  139. if (scanForKeywordsEnabled) {
  140. scanForKeywords(inputElement);
  141. }
  142. getRandomEntry(inputElement);
  143. sendMessage(inputElement);
  144. anotherCustomFunction();
  145. }
  146. }
  147.  
  148. // Function to apply custom rules for the input field
  149. function applyCustomRule(inputElement) {
  150. const customRule = unsafeWindow.customRuleLogic ? unsafeWindow.customRuleLogic.getCurrentText() : '';
  151.  
  152. if (inputElement.nodeName === 'TEXTAREA') {
  153. // For mobile version where input is <textarea>
  154. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  155. nativeInputValueSetter.call(inputElement, inputElement.value + customRule);
  156.  
  157. const inputEvent = new Event('input', {
  158. bubbles: true,
  159. cancelable: true,
  160. });
  161. inputElement.dispatchEvent(inputEvent);
  162. } else {
  163. // For desktop version where input is a Slate editor
  164. const inputEvent = new InputEvent('beforeinput', {
  165. bubbles: true,
  166. cancelable: true,
  167. inputType: 'insertText',
  168. data: customRule,
  169. });
  170. inputElement.dispatchEvent(inputEvent);
  171. }
  172. }
  173.  
  174. // Function to set the cursor position to the end after inserting the text
  175. function setCursorToEnd(inputElement) {
  176. if (inputElement.nodeName === 'TEXTAREA') {
  177. // For mobile version where input is <textarea>
  178. inputElement.selectionStart = inputElement.selectionEnd = inputElement.value.length;
  179. } else {
  180. // For desktop version where input is a Slate editor
  181. inputElement.focus();
  182.  
  183. // Simulate repeated Ctrl + ArrowRight key press events to move cursor to the end
  184. const repeatPresses = 150; // Number of times to simulate Ctrl + ArrowRight
  185. for (let i = 0; i < repeatPresses; i++) {
  186. const ctrlArrowRightEvent = new KeyboardEvent('keydown', {
  187. key: 'ArrowRight',
  188. code: 'ArrowRight',
  189. keyCode: 39, // ArrowRight key code
  190. charCode: 0,
  191. which: 39,
  192. bubbles: true,
  193. cancelable: true,
  194. ctrlKey: true // Set Ctrl key to true
  195. });
  196. inputElement.dispatchEvent(ctrlArrowRightEvent);
  197. }
  198. }
  199. }
  200.  
  201. // Function to send the message (either click send button or simulate Enter key)
  202. function sendMessage(inputElement) {
  203. if (inputElement.nodeName === 'TEXTAREA') {
  204. // Mobile version: Do not send message, just return to allow linebreak
  205. return;
  206. }
  207.  
  208. let sendButton = document.querySelector('button[aria-label="Nachricht senden"]');
  209. if (sendButton) {
  210. sendButton.click();
  211. console.log('Send button clicked to send message');
  212. } else {
  213. // For desktop version, simulate pressing Enter to send the message
  214. let enterEvent = new KeyboardEvent('keydown', {
  215. key: 'Enter',
  216. code: 'Enter',
  217. keyCode: 13,
  218. which: 13,
  219. bubbles: true,
  220. cancelable: true
  221. });
  222. inputElement.dispatchEvent(enterEvent);
  223. console.log('Enter key simulated to send message');
  224. }
  225. }
  226.  
  227. // Function to check if the device is a phone
  228. function isPhone() {
  229. return (
  230. /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
  231. window.innerWidth <= 768 // Adjust width as needed to target mobile devices
  232. );
  233. }
  234.  
  235. // Function to add a simple button with an SVG icon that also triggers Enter keypress
  236. function addButton() {
  237. const button = document.createElement('button');
  238. button.style.position = 'fixed';
  239. button.style.bottom = '34px';
  240. button.style.right = '42px';
  241. button.style.zIndex = '1000';
  242. button.style.border = 'none';
  243. button.style.background = 'transparent'; // Make background transparent
  244. button.style.padding = '0'; // Remove padding
  245. button.style.cursor = 'pointer';
  246. button.style.display = 'flex';
  247. button.style.alignItems = 'center';
  248.  
  249. const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  250. svgIcon.setAttribute('aria-hidden', 'true');
  251. svgIcon.setAttribute('role', 'img');
  252. svgIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  253. svgIcon.setAttribute('width', '24');
  254. svgIcon.setAttribute('height', '24');
  255. svgIcon.setAttribute('fill', '#5865F2'); // Set icon color to blue
  256. svgIcon.setAttribute('viewBox', '0 0 24 24');
  257.  
  258. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  259. path.setAttribute('fill', '#5865F2'); // Set path fill color to blue
  260. path.setAttribute('d', 'M6.6 10.02 14 11.4a.6.6 0 0 1 0 1.18L6.6 14l-2.94 5.87a1.48 1.48 0 0 0 1.99 1.98l17.03-8.52a1.48 1.48 0 0 0 0-2.64L5.65 2.16a1.48 1.48 0 0 0-1.99 1.98l2.94 5.88Z');
  261.  
  262. svgIcon.appendChild(path);
  263. button.appendChild(svgIcon);
  264.  
  265. button.addEventListener('click', function() {
  266. handleEnterKey();
  267. });
  268.  
  269. document.body.appendChild(button);
  270. }
  271.  
  272. // Add the button to the page only if the device is a phone
  273. if (isPhone()) {
  274. addButton();
  275. }
  276.  
  277. // Function to simulate the Enter keypress
  278. function handleEnterKey() {
  279. const event = new KeyboardEvent('keydown', {
  280. key: 'Enter',
  281. keyCode: 13,
  282. code: 'Enter',
  283. which: 13,
  284. bubbles: true,
  285. });
  286. document.dispatchEvent(event);
  287. }
  288.  
  289.  
  290.  
  291. // Example of adding another function
  292. function anotherCustomFunction() {
  293. console.log('Another custom function executed');
  294. }
  295.  
  296. // Function to scan for keywords and access local storage
  297. function scanForKeywords(inputElement) {
  298. const currentProfile = getCurrentProfile();
  299. if (currentProfile) {
  300. // Retrieve all messages before iterating through storage keys
  301. const messageItems = document.querySelectorAll('div[class*="messageContent_"]');
  302. let relevantMessages = Array.from(messageItems).slice(-15); // Last 15 messages
  303. const lastMessage = Array.from(messageItems).slice(-1); // Last message only
  304.  
  305.  
  306. // Iterate over the last 15 messages to extract hidden bracket content
  307. relevantMessages = relevantMessages.map(msg => {
  308. // Retrieve all span elements within the message
  309. const spans = msg.querySelectorAll('span');
  310.  
  311. // Filter out the spans based on both style conditions: opacity and position
  312. const hiddenSpans = Array.from(spans).filter(span => {
  313. const style = window.getComputedStyle(span);
  314. return style.opacity === '0' && style.position === 'absolute';
  315. });
  316.  
  317. // Join the text content of all matching spans
  318. const bracketContent = hiddenSpans.map(span => span.textContent).join('');
  319.  
  320. // Extract content within square brackets, if any
  321. const match = bracketContent.match(/\[(.*?)\]/);
  322. return match ? match[1] : null;
  323. }).filter(content => content !== null);
  324.  
  325. // Log the filtered messages for debugging purposes
  326. console.log("Filtered Relevant Messages (content in brackets, last 15):", relevantMessages);
  327. console.log("Last Message:", lastMessage.map(msg => msg.textContent));
  328.  
  329. // Track how many entries have been appended
  330. let appendedCount = 0;
  331. const maxAppends = 3;
  332.  
  333. // Check if the last message contains a specific link pattern
  334. const mp3LinkPattern = /https:\/\/files\.shapes\.inc\/.*\.mp3/;
  335. let mp3LinkValue = null;
  336.  
  337. if (lastMessage.length > 0) {
  338. const lastMessageText = lastMessage[0].textContent;
  339. const mp3LinkMatch = lastMessageText.match(mp3LinkPattern);
  340. if (mp3LinkMatch) {
  341. const mp3LinkKey = mp3LinkMatch[0];
  342. mp3LinkValue = localStorage.getItem(mp3LinkKey);
  343. console.log(`MP3 Link detected: ${mp3LinkKey}. Retrieved value: ${mp3LinkValue}`);
  344. }
  345. }
  346.  
  347. // Create an array to hold all entry keys that need to be checked
  348. let allEntryKeys = [];
  349.  
  350. // Iterate through all localStorage keys that match the profile-lorebook prefix
  351. Object.keys(localStorage).forEach(storageKey => {
  352. if (storageKey.startsWith(`${currentProfile}-lorebook:`)) {
  353. const entryKeys = storageKey.replace(`${currentProfile}-lorebook:`, '').split(',');
  354. const entryValue = localStorage.getItem(storageKey);
  355.  
  356. // Log the entry keys for debugging purposes
  357. console.log(`Entry Keys: `, entryKeys);
  358.  
  359. entryKeys.forEach(entryKey => {
  360. allEntryKeys.push({ entryKey, entryValue });
  361. });
  362. }
  363. });
  364.  
  365. // If mp3LinkValue is present, parse it for keywords as well
  366. if (mp3LinkValue) {
  367. console.log(`Scanning MP3 link value for keywords: ${mp3LinkValue}`);
  368. const mp3Keywords = mp3LinkValue.split(',');
  369. mp3Keywords.forEach(keyword => {
  370. const trimmedKeyword = keyword.trim();
  371. console.log(`Adding keyword from MP3 value: ${trimmedKeyword}`);
  372. // Add mp3 keywords but set entryValue to an empty string instead of null
  373. allEntryKeys.push({ entryKey: trimmedKeyword, entryValue: '' });
  374. });
  375. }
  376.  
  377. // Iterate over all collected entry keys and perform the checks
  378. allEntryKeys.forEach(({ entryKey, entryValue }) => {
  379. if (appendedCount >= maxAppends) return; // Stop if max appends reached
  380.  
  381. // Log each keyword being checked
  382. console.log(`Checking keyword: ${entryKey}`);
  383.  
  384. // Check input element text for complete word match of keyword (case-insensitive)
  385. const inputText = inputElement.value || inputElement.textContent;
  386.  
  387. // Combine check for keyword in input, in the last message, or in the mp3 link value
  388. const isKeywordInInput = inputText && new RegExp(`\\b${entryKey}\\b`, 'i').test(inputText);
  389. const isKeywordInLastMessage = lastMessage.some(message => {
  390. const lastMessageText = message.textContent;
  391. return new RegExp(`\\b${entryKey}\\b`, 'i').test(lastMessageText);
  392. });
  393. const isKeywordInMp3LinkValue = mp3LinkValue && mp3LinkValue.includes(entryKey);
  394.  
  395. console.log(`Keyword '${entryKey}' in input: ${isKeywordInInput}, in last message: ${isKeywordInLastMessage}, in MP3 value: ${isKeywordInMp3LinkValue}`);
  396.  
  397. if ((isKeywordInInput || isKeywordInLastMessage || isKeywordInMp3LinkValue) && entryValue) {
  398. const keywordAlreadyUsed = relevantMessages.some(bracketContent => {
  399. return new RegExp(`\\b${entryKey}\\b`, 'i').test(bracketContent);
  400. });
  401.  
  402. if (!keywordAlreadyUsed) {
  403. // Append the entryValue to the input element only if entryValue is not null or empty
  404. if (inputElement.nodeName === 'TEXTAREA') {
  405. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  406. nativeInputValueSetter.call(inputElement, inputElement.value + '\n' + entryValue);
  407.  
  408. const inputEvent = new Event('input', {
  409. bubbles: true,
  410. cancelable: true,
  411. });
  412. inputElement.dispatchEvent(inputEvent);
  413. } else {
  414. const inputEvent = new InputEvent('beforeinput', {
  415. bubbles: true,
  416. cancelable: true,
  417. inputType: 'insertText',
  418. data: '\n' + entryValue,
  419. });
  420. inputElement.dispatchEvent(inputEvent);
  421. }
  422. appendedCount++; // Increment the count
  423. console.log(`Keyword '${entryKey}' detected. Appended lorebook entry to the input.`);
  424. } else {
  425. console.log(`Keyword '${entryKey}' already found in recent bracketed messages or entryValue is null/empty. Skipping append.`);
  426. }
  427. }
  428. });
  429.  
  430. // Log the total number of entries appended
  431. console.log(`Total lorebook entries appended: ${appendedCount}`);
  432. }
  433. }
  434.  
  435.  
  436. // Function to get the current profile from local storage
  437. function getCurrentProfile() {
  438. return localStorage.getItem('currentProfile');
  439. }
  440.  
  441.  
  442. function getRandomEntry(inputElement) {
  443. const selectedProfile = localStorage.getItem('events.selectedProfile');
  444. if (selectedProfile) {
  445. let profileEntries = [];
  446. const currentHour = new Date().getHours();
  447. for (let key in localStorage) {
  448. if (key.startsWith(`events.${selectedProfile}:`)) {
  449. const entryData = JSON.parse(localStorage.getItem(key));
  450. const [startHour, endHour] = entryData.timeRange.split('-').map(Number);
  451. // Check if current hour is within the time range
  452. if (
  453. (startHour <= endHour && currentHour >= startHour && currentHour < endHour) || // Normal range
  454. (startHour > endHour && (currentHour >= startHour || currentHour < endHour)) // Crosses midnight
  455. ) {
  456. profileEntries.push({ key, ...entryData });
  457. }
  458. }
  459. }
  460.  
  461. if (profileEntries.length > 0) {
  462. const probability = parseInt(localStorage.getItem('events.probability') || '100', 10);
  463. let selectedEntry = null;
  464. while (profileEntries.length > 0) {
  465. // Randomly select an entry from the available entries
  466. const randomIndex = Math.floor(Math.random() * profileEntries.length);
  467. const randomEntry = profileEntries[randomIndex];
  468. // Check if the entry passes the individual probability check
  469. if (Math.random() * 100 < randomEntry.probability) {
  470. selectedEntry = randomEntry;
  471. break;
  472. } else {
  473. // Remove the entry from the list if it fails the probability check
  474. profileEntries.splice(randomIndex, 1);
  475. }
  476. }
  477. if (selectedEntry && Math.random() * 100 < probability) {
  478. console.log(`Random Entry Selected: ${selectedEntry.value}`);
  479. appendToInput(inputElement, selectedEntry.value); // Append the random entry to the input element
  480. }
  481. } else {
  482. console.log('No entries available for the selected profile in the current time range.');
  483. }
  484. } else {
  485. console.log('No profile selected. Please select a profile to retrieve a random entry.');
  486. }
  487. }
  488.  
  489. // Helper function to append text to the input element
  490. function appendToInput(inputElement, text) {
  491. const lineBreak = '\n';
  492. if (!inputElement) {
  493. console.error('Input element not found.');
  494. return;
  495. }
  496.  
  497. if (inputElement.nodeName === 'TEXTAREA') {
  498. // Mobile: Append text to <textarea>
  499. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  500. nativeInputValueSetter.call(inputElement, inputElement.value + `${lineBreak}${text}`);
  501.  
  502. const inputEvent = new Event('input', {
  503. bubbles: true,
  504. cancelable: true,
  505. });
  506. inputElement.dispatchEvent(inputEvent);
  507. } else if (inputElement.hasAttribute('data-slate-editor')) {
  508. // Desktop: Append text for Slate editor
  509. const inputEvent = new InputEvent('beforeinput', {
  510. bubbles: true,
  511. cancelable: true,
  512. inputType: 'insertText',
  513. data: `${lineBreak}${text}`,
  514. });
  515. inputElement.dispatchEvent(inputEvent);
  516. } else {
  517. console.error('Unsupported input element type.');
  518. }
  519. }
  520.  
  521.  
  522.  
  523. // Expose the function to be accessible from other scripts
  524. unsafeWindow.getRandomEntry = getRandomEntry;
  525.  
  526.  
  527. }
  528. })();