Automates the use of Mudae bot in Discord
// ==UserScript== // @name AutoMudae // @description Automates the use of Mudae bot in Discord // @version 0.8.5 // @author Nxve // @license GNU GPLv3 // @namespace https://github.com/Nxve/AutoMudae // @supportURL https://github.com/Nxve/AutoMudae/issues // @match https://discord.com/channels/* // @exclude https://discord.com/channels/@me // @run-at document-start // @icon https://icons.duckduckgo.com/ip2/discord.com.ico // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_info // ==/UserScript== (function () { const window = unsafeWindow; const localStorage = window.localStorage; //// Logger const _logger = { _preffix: '%c[AUTO MUDAE]', _symbols: { error: '[!]', info: '[i]', log: '[*]', plus: '[+]', debug: '[!]', warn: '[!]' }, _color: { error: 'red', info: 'cyan', log: 'white', plus: 'lime', debug: 'cyan', warn: 'gold' }, _history: [], _lastMessageHash: null, _hash: s => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0), _print: function (type, ...etc) { const hash = [...arguments].toString(); // if (hash === this._lastMessageHash) return; console.log(`${this._preffix}%c${this._symbols[type]}`, 'background: black; color: magenta;', `background: black; color: ${this._color[type]}`, ...etc); if (type !== 'debug') this._history.push([type, [...etc]]); this._lastMessageHash = hash; }, _reprompt: function () { this._history.forEach(log => this[log[0]](...log[1])); } }; const logger = {}; ['error', 'info', 'log', 'plus', 'debug', 'warn'].forEach(method => { logger[method] = function () { this._print(method, ...arguments) }; }); /// I use prototype here to prevent exposing private properties in DevTools. Object.setPrototypeOf(logger, _logger); window.logger = logger; //// ENUM const E = {}; E.DISCORD_INFO = { CHANNEL_ID: 'channel_id', GUILD_ID: 'guild_id', SESSION_ID: 'session_id' }; E.AUTOMUDAE_STATE = { INJECT: 'inject', SETUP: 'setup', ERROR: 'error', IDLE: 'idle', RUN: 'run', }; E.MUDAE_INFO = { ROLLS_MAX: 'rolls_max', ROLLS_LEFT: 'rolls_left', POWER: 'power', CAN_RT: 'can_rt', CAN_MARRY: 'can_marry', CONSUMPTION: 'kakera_consumption' }; E.TOAST = { INFO: 'info', WARN: 'warn', CRITICAL: 'critical', KAKERA: 'kakera', CHARCLAIM: 'charclaim', SOULMATE: 'soulmate' }; E.EMOJI = { '💓': '%F0%9F%92%93', '💕': '%F0%9F%92%95', '💖': '%F0%9F%92%96', '💗': '%F0%9F%92%97', '💘': '%F0%9F%92%98', '❤️': '%E2%9D%A4%EF%B8%8F', '❣️': '%E2%9D%A3%EF%B8%8F', '💞': '%F0%9F%92%9E', '♥️': '%E2%99%A5%EF%B8%8F' }; E.EMOJI_KAKERA = { kakeraP: 'kakeraP%3A609264156347990016', kakera: 'kakera%3A469791929106956298', kakeraT: 'kakeraT%3A609264180851376132', kakeraG: 'kakeraG%3A609264166381027329', kakeraY: 'kakeraY%3A605112931168026629', kakeraO: 'kakeraO%3A605112954391887888', kakeraR: 'kakeraR%3A605112980295647242', kakeraW: 'kakeraW%3A608192076286263297', kakeraL: 'kakeraL%3A815961697918779422', }; E.KAKERA = { PURPLE: 'kakeraP', BLUE: 'kakera', CYAN: 'kakeraT', GREEN: 'kakeraG', YELLOW: 'kakeraY', ORANGE: 'kakeraO', RED: 'kakeraR', RAINBOW: 'kakeraW', LIGHT: 'kakeraL', }; E.GMVALUE = { PREFERENCES: 'preferences', VERSION: 'version', TOKENLIST: 'tokenlist' }; E.PREFERENCES = { KAKERA: 'kakera', MENTIONS: 'mentions', ROLL: 'roll', SOUND: 'sound', EXTRA: 'extra' }; E.INFO_FIELD = { KAKERA: 'kakera', COLLECTED_CHARACTERS: 'collected-characters', ROLLS_LEFT: 'rolls-left', ROLLS_MAX: 'rolls-max', POWER: 'power', POWER_CONSUMPTION: 'consumption', CAN_MARRY: 'marry', CAN_RT: 'rt' }; E.SLASH_COMMANDS = { "wx": { version: "832172261968314389", id: "832172261968314388" }, "wa": { version: "832172151729422418", id: "832172151729422417" }, "wg": { version: "832172216665374751", id: "832172216665374750" }, "hx": { version: "832172373536669707", id: "832172373536669706" }, "ha": { version: "832172457028747337", id: "832172457028747336" }, "hg": { version: "832172416192872459", id: "832172416192872458" }, }; //// SOUND const audioCtx = new AudioContext(); function beep(gain, hz, ms, times = 1){ for (let i = 0; i < times; i++) { const v = audioCtx.createOscillator(); const u = audioCtx.createGain(); v.connect(u); v.frequency.value = hz; v.type = "square"; u.connect(audioCtx.destination); u.gain.value = gain * 0.01; const durationInSeconds = ms * .001; v.start(audioCtx.currentTime + i * (durationInSeconds*1.5)); v.stop(audioCtx.currentTime + durationInSeconds + i * (durationInSeconds*1.5)); } }; const SOUND = { foundCharacter: () => {beep(5, 400, 100, 1)}, marry: () => {beep(10, 600, 100, 1)}, critical: () => {beep(15, 70, 80, 6)}, lastResetNoRolls: () => {beep(10, 60, 250, 2)}, newSoulmate: () => {beep(10, 600, 100, 2)} }; //// CSS const CSS = {}; CSS.decorators = ` li[id^=chat-message]:is(.plus, .critical){ position: relative; } li[id^=chat-message]:is(.plus, .critical)::after{ position: absolute; bottom: 0; width: 22px; height: 100%; display: flex; align-items: center; justify-content: center; } li[id^=chat-message].plus{ background-color: hsl(138deg 100% 50% / 10%); } li[id^=chat-message].plus::after { content: '+'; background-color: hsl(109deg 45% 18%); color: lime; } li[id^=chat-message].critical{ background-color: hsl(0deg 100% 50% / 10%); } li[id^=chat-message].critical::after { content: '!'; background-color: hsl(0deg 45% 18%); color: red; } `; CSS.general = ` ::-webkit-scrollbar { width: 2px; } ::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.8); } .automudae-hide, .automudae-hide *, .automudae-hide::before, .automudae-hide::after { display: none !important; } `; CSS.stateText = ` #automudae-state { display: flex; gap: 10px; margin-right: 10px; color: var(--text-normal); } `; CSS.runButton = ` #automudae-run-button { display: flex; align-items: center; gap: 4px; padding: 1px 5px; margin-right: 20px; background-color: var(--button-outline-brand-background-active); cursor: pointer; transition: 200ms; } #automudae-run-button:hover { background-color: var(--button-outline-brand-background-hover); transform: scale(1.1); } #automudae-run-button::before { content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='15' fill='%23DCDDDE' viewBox='0 0 16 12'%3E%3Cpath d='m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z'/%3E%3C/svg%3E"); } #automudae-run-button.running::before { content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23DCDDDE' viewBox='0 0 16 12'%3E%3Cpath d='M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z'/%3E%3C/svg%3E"); } #automudae-run-button::after { content: "Run"; } #automudae-run-button.running::after { content: "Pause"; } `; CSS.injectionsAndError = ` #automudae-injections-wrapper, #automudae-error { position: absolute; inset: auto 0; top: 8px; width: fit-content; margin-inline: auto; padding: 5px; z-index: 9999; } #automudae-injections-wrapper { display: flex; gap: 10px; padding: 0; } #automudae-injections-wrapper > div { padding: 5px; background-color: var(--button-outline-brand-background-active); color: var(--text-normal); font-weight: 500; cursor: pointer; transition: 200ms; } #automudae-injections-wrapper > div:hover { background-color: var(--button-outline-brand-background-hover); transform: scale(1.1); } #automudae-error { background-color: var(--button-danger-background); color: white; animation: popIn 150ms forwards; transform: scale(0); } @keyframes popIn { to { top: 40px; transform: scale(1); } } `; CSS.sidePanels = ` [id^=automudae-panel] { background-color: var(--background-primary); font-weight: 500; display: flex; flex-direction: column; gap: 10px; transition: 500ms; overflow: hidden; height: fit-content; } [id^=automudae-panel] > * { background-color: var(--interactive-muted); } [id^=automudae-panel] :is(h1, h2) { background-color: var(--background-tertiary); color: var(--text-normal); display: flex; align-items: center; justify-content: center; } [id^=automudae-panel] h1 { font-size: large; height: 1.5rem; background-color: var(--button-outline-brand-background-active); cursor: pointer; } [id^=automudae-panel] h1:hover { background-color: var(--button-outline-brand-background-hover) !important; } [id^=automudae-panel] h2 { font-size: medium; height: 1rem; } [id^=automudae-panel] textarea { font-weight: 900; max-height: 100px; } [id^=automudae-panel] span { font-size: small; color: var(--text-normal); } [id^=automudae-panel] ul { width: 100%; font-size: small; color: var(--text-normal); max-height: 10rem; overflow-x: clip; overflow-y: auto; } [id^=automudae-panel] li:nth-child(odd) { background-color: var(--background-primary); } .automudae-section { margin-bottom: 5px; } .automudae-section-body { display: flex; padding: 4px; flex-wrap: wrap; } .automudae-section-body > div { display: flex; padding-inline: 3px; border-radius: 5px; align-items: center; } .automudae-section-body > div:hover { background-color: var(--button-secondary-background-hover); } #automudae-panel-info .automudae-section-body { flex-direction: column; gap: 8px; } #automudae-section-kakera > div { justify-content: space-between; } #automudae-section-kakera > div > div { flex-direction: column; padding: 0; } #automudae-section-status .automudae-section-body { padding: 0; } .automudae-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .automudae-row > div { display: flex; align-items: center; } .automudae-row-expandable { padding: 3px; display: block !important; } .automudae-row-expandable > div:not(:first-child) { margin-top: 2px; background-color: var(--background-primary); max-height: 0px; overflow: hidden; transition: max-height 300ms linear; } .automudae-row-expandable:hover > div:not(:first-child) { max-height: 300px; } .automudae-row-expandable > div:not(:first-child) > .automudae-row:hover { background-color: var(--background-accent); } [id^=automudae-panel] > div { max-height: 600px; transition: max-height 400ms cubic-bezier(0, 1, 1, 1); } [id^=automudae-panel].collapsed { gap: 0px; } [id^=automudae-panel].collapsed > div { max-height: 0px; } [data-requirerestart] { position: relative; } [data-requirerestart]::before { content: '*'; color: yellow; position: absolute; left: 0px; } [data-requirerestart]:hover::before { content: '* Require restart to apply changes!'; position: absolute; bottom: 20px; background-color: var(--background-tertiary); font-size: x-small; padding: 2px 10px; color: yellow; border-radius: 5px; pointer-events: none; } `; CSS.toasts = ` #automudae-toasts-wrapper { position: absolute; right: 15px; width: 37%; height: 97%; display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-end; gap: 8px; z-index: 9; } .automudae-toast { background-color: white; padding: 5px; font-weight: 500; animation: slide-in-blurred-left 0.6s cubic-bezier(0.230, 1.000, 0.320, 1.000) both; } .automudae-toast.info { --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%235865f2' viewBox='0 0 16 16'%3E%3Cpath d='M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm8.93 4.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM8 5.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2z'/%3E%3C/svg%3E"); background-color: var(--button-outline-brand-border); } .automudae-toast.kakera { --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%23dee0fc' viewBox='0 0 16 16'%3E%3Cpath d='M3.1.7a.5.5 0 0 1 .4-.2h9a.5.5 0 0 1 .4.2l2.976 3.974c.149.185.156.45.01.644L8.4 15.3a.5.5 0 0 1-.8 0L.1 5.3a.5.5 0 0 1 0-.6l3-4zm11.386 3.785-1.806-2.41-.776 2.413 2.582-.003zm-3.633.004.961-2.989H4.186l.963 2.995 5.704-.006zM5.47 5.495 8 13.366l2.532-7.876-5.062.005zm-1.371-.999-.78-2.422-1.818 2.425 2.598-.003zM1.499 5.5l5.113 6.817-2.192-6.82L1.5 5.5zm7.889 6.817 5.123-6.83-2.928.002-2.195 6.828z'/%3E%3C/svg%3E"); background-color: var(--brand-experiment-200); } .automudae-toast.charclaim { --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%2346c46e' viewBox='0 0 16 16'%3E%3Cpath d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/%3E%3C/svg%3E"); background-color: var(--text-positive); } .automudae-toast.soulmate { --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='violet' viewBox='0 0 16 16'%3E%3Cpath d='M7.657 6.247c.11-.33.576-.33.686 0l.645 1.937a2.89 2.89 0 0 0 1.829 1.828l1.936.645c.33.11.33.576 0 .686l-1.937.645a2.89 2.89 0 0 0-1.828 1.829l-.645 1.936a.361.361 0 0 1-.686 0l-.645-1.937a2.89 2.89 0 0 0-1.828-1.828l-1.937-.645a.361.361 0 0 1 0-.686l1.937-.645a2.89 2.89 0 0 0 1.828-1.828l.645-1.937zM3.794 1.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387A1.734 1.734 0 0 0 4.593 5.69l-.387 1.162a.217.217 0 0 1-.412 0L3.407 5.69A1.734 1.734 0 0 0 2.31 4.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387A1.734 1.734 0 0 0 3.407 2.31l.387-1.162zM10.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732L9.1 2.137a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L10.863.1z'/%3E%3C/svg%3E"); background-color: violet; } .automudae-toast.warn{ --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%23faa81a' class='bi bi-exclamation-diamond-fill' viewBox='0 0 16 16'%3E%3Cpath d='M9.05.435c-.58-.58-1.52-.58-2.1 0L.436 6.95c-.58.58-.58 1.519 0 2.098l6.516 6.516c.58.58 1.519.58 2.098 0l6.516-6.516c.58-.58.58-1.519 0-2.098L9.05.435zM8 4c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995A.905.905 0 0 1 8 4zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z'/%3E%3C/svg%3E"); background-color: var(--text-warning); } .automudae-toast.critical{ --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='20' fill='%23ed4245' class='bi bi-exclamation-diamond-fill' viewBox='0 0 16 16'%3E%3Cpath d='M9.05.435c-.58-.58-1.52-.58-2.1 0L.436 6.95c-.58.58-.58 1.519 0 2.098l6.516 6.516c.58.58 1.519.58 2.098 0l6.516-6.516c.58-.58.58-1.519 0-2.098L9.05.435zM8 4c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995A.905.905 0 0 1 8 4zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z'/%3E%3C/svg%3E"); background-color: var(--status-danger); } .automudae-toast.link { cursor: alias; } .automudae-toast.missing { animation: wobble-hor-bottom 0.8s both; } .automudae-toast:hover::before { --width: 18px; --height: 100; content: var(--svg); position: absolute; left: calc(calc(-1 * var(--width)) - 5px); top: calc(.5% * calc(100 - var(--height))); height: calc(1% * var(--height)); width: var(--width); display: flex; align-items: center; justify-content: center; animation: flipHorz 1s cubic-bezier(0.455, 0.030, 0.515, 0.955) both; pointer-events: none; } .automudae-toast:nth-last-child(12) { opacity: 0.6; } .automudae-toast:nth-last-child(13) { opacity: 0.5; } .automudae-toast:nth-last-child(14) { opacity: 0.4; } .automudae-toast:nth-last-child(15) { opacity: 0.3; } .automudae-toast:nth-last-child(16) { opacity: 0.2; } .automudae-toast:nth-last-child(17) { opacity: 0.1; } .automudae-toast:nth-last-child(n+18) { opacity: 0; } @keyframes flipHorz { 0% { transform: rotateY(0); } 100% { transform: rotateY(-360deg); } } @keyframes slide-in-blurred-left { 0% { transform: translateX(-1000px) scaleX(2.5) scaleY(0.2); transform-origin: 100% 50%; filter: blur(40px); } 100% { transform: translateX(0) scaleY(1) scaleX(1); transform-origin: 50% 50%; filter: blur(0); } } @keyframes wobble-hor-bottom { 0%, 100% { transform: translateX(0%); transform-origin: 50% 50%; } 15% { transform: translateX(-30px) rotate(-6deg); } 30% { transform: translateX(15px) rotate(6deg); } 45% { transform: translateX(-15px) rotate(-3.6deg); } 60% { transform: translateX(9px) rotate(2.4deg); } 75% { transform: translateX(-6px) rotate(-1.2deg); } } `; CSS.tokenList = ` #automudae-tokenlist-wrapper { position: absolute; width: 100%; height: 100%; z-index: 9999; display: flex; align-items: center; justify-content: center; } #automudae-tokenlist { background-color: var(--background-tertiary); width: 400px; height: 80vh; display: flex; flex-direction: column; font-weight: 400; color: white; justify-content: space-between; align-items: center; padding: 5px; } #automudae-tokenlist-accept { width: 100%; text-align: center; padding-block: 5px; background-color: var(--status-positive-background); transition: 200ms; cursor: pointer; } #automudae-tokenlist h3 { font-size: x-large; position: relative; } #automudae-tokenlist h3::after { content: ''; position: absolute; left: 0px; bottom: -4px; height: 2px; width: 100%; background-color: var(--background-modifier-accent); } #automudae-tokenlist ul { width: 400px; height: 60vh; background-color: var(--background-floating); display: flex; flex-direction: column; gap: 5px; overflow-y: overlay; } #automudae-tokenlist-accept:hover { background-color: var(--status-positive); } #automudae-tokenlist-controls { display: flex; flex-direction: row; justify-content: flex-end; gap: 1rem; padding-right: 10px; } #automudae-tokenlist-controls > div { padding: 3px; font-size: small; cursor: pointer; transition: 200ms; } #automudae-tokenlist-controls > div:hover { background-color: white; color: black; } #automudae-tokenlist input { width: 99%; border: none; background: none; color: var(--text-normal); } #automudae-tokenlist li:nth-child(odd) { background-color: var(--background-accent); } #automudae-tokenlist li:nth-child(even) { background-color: var(--background-modifier-selected); } #automudae-tokenlist li { position: relative; } #automudae-tokenlist li:hover:not(:focus-within) input { opacity: .1; } #automudae-tokenlist li:hover:not(:focus-within)::before { content: attr(data-username); position: absolute; height: 100%; display: flex; align-items: center; margin-left: 5px; } `; GM_addStyle(Object.values(CSS).join(' ')); //// Utils const pickRandom = (arr) => arr[arr.length * Math.random() | 0]; const getLast = (arr) => arr[arr.length - 1]; //// DOM Elements const DOM = { el_ChannelList: null, el_MemberList: null, el_Chat: null, el_ChatWrapper: null, el_InjectionsWrapper: null, el_RunButton: null, el_StateSpan: null, el_ToastsWrapper: null, el_ErrorPopup: null }; //// CONSTS const INTERVAL_SEND_MESSAGE = 1500; const INTERVAL_ROLL = 2000; const INTERVAL_THINK = 200; const MUDAE_USER_ID = '432610292342587392'; //// Discord Data & Utils const Discord = { info: new Map(), /// Map<E.DISCORD_INFO, >() lastMessageTime: 0, cdSendMessage: 0, nonce: Math.floor(Math.random() * 1000000), Message: { getDate: (el_Message) => { const messageDate = new Date(this.el_Message.querySelector("time[id^='message-timestamp']")?.dateTime); if (messageDate.toString() === "Invalid Date") { logger.error("Couldn't retrieve timestamp for this Discord message:", el_Message); return; } return messageDate; }, getAuthorId: (el_Message) => { let el_TargetMessage = el_Message; let el_Avatar; while (!el_Avatar) { el_Avatar = el_TargetMessage.querySelector(`img[class^='avatar']`); if (el_Avatar) break; el_TargetMessage = el_TargetMessage.previousElementSibling; while (el_TargetMessage && el_TargetMessage.tagName !== "LI") { el_TargetMessage = el_TargetMessage.previousElementSibling; } if (!el_Avatar && !el_TargetMessage) return logger.error("Couldn't get avatar for this Discord message:", el_Message); } const match = /avatars\/(\d+)\//.exec(el_Avatar.src); if (match) return match[1]; }, getId: (el_Message) => getLast(el_Message.id.split("-")), isFromMudae: function (el_Message) { return this.getAuthorId(el_Message) === MUDAE_USER_ID }, isFromMe: function (el_Message) { return AutoMudae.users.find(user => user.id === this.getAuthorId(el_Message), this); } } }; /// AutoMudae class MudaeUser { id username avatar token nick info sendTUTimer constructor(token, id, username, avatar) { this.token = token; this.info = new Map(); return new Promise(async (resolve) => { if (id){ this.id = id; this.username = username; this.avatar = avatar; await this.fetchNick(); return resolve(this); } fetch("https://discord.com/api/v9/users/@me", { "headers": { "authorization": token } }) .then(response => response.json()) .then(async (data) => { this.id = data.id; this.username = data.username; this.avatar = data.avatar; await this.fetchNick(); }) .catch(err => logger.error(`Couldn't retrieve info for some user.`, err)) .finally(() => resolve(this)); }); } async fetchNick(){ return new Promise(resolve => { const guildId = window.location.pathname.split("/")[2]; fetch(`https://discord.com/api/v9/users/${this.id}/profile?guild_id=${guildId}`, { "headers": { "authorization": this.token } }) .then(response => response.json()) .then(data => { const { guild_member: { nick } } = data; this.nick = nick; }) .catch(err => logger.error(`Couldn't retrieve the nick for user [${this.username}]`, err)) .finally(() => resolve()); }); } hasNeededInfo() { return [E.MUDAE_INFO.ROLLS_MAX, E.MUDAE_INFO.ROLLS_LEFT, E.MUDAE_INFO.POWER, E.MUDAE_INFO.CAN_RT, E.MUDAE_INFO.CAN_MARRY, E.MUDAE_INFO.CONSUMPTION].every(info => this.info.has(info), this); } send(content) { const now = performance.now(); if (now - Discord.cdSendMessage < INTERVAL_SEND_MESSAGE) return; fetch(`https://discord.com/api/v9/channels/${Discord.info.get(E.DISCORD_INFO.CHANNEL_ID)}/messages`, { "method": "POST", "headers": { "authorization": this.token, "content-type": "application/json" }, "body": `{"content":"${content || '?'}","nonce":"${++Discord.nonce}","tts":false}` }); Discord.cdSendMessage = now; } react(el_Message, E_EMOJI = E.EMOJI["💓"]) { fetch(`https://discord.com/api/v9/channels/${Discord.info.get(E.DISCORD_INFO.CHANNEL_ID)}/messages/${Discord.Message.getId(el_Message)}/reactions/${E_EMOJI}/%40me`, { "method": "PUT", "headers": { "authorization": this.token, } }); } setTUTimer(ms) { if (this.sendTUTimer) clearTimeout(this.sendTUTimer); this.sendTUTimer = setTimeout((user) => { user.send("$tu") }, ms, this); } roll() { const rollPreferences = AutoMudae.preferences.get(E.PREFERENCES.ROLL); const command = E.SLASH_COMMANDS[rollPreferences.type]; fetch("https://discord.com/api/v9/interactions", { "method": "POST", "headers": { "authorization": this.token, "content-type": "multipart/form-data; boundary=----BDR", }, "body": `------BDR\r\nContent-Disposition: form-data; name="payload_json"\r\n\r\n{"type":2,"application_id":"${MUDAE_USER_ID}","guild_id":"${Discord.info.get(E.DISCORD_INFO.GUILD_ID)}","channel_id":"${Discord.info.get(E.DISCORD_INFO.CHANNEL_ID)}","session_id":"${Discord.info.get(E.DISCORD_INFO.SESSION_ID)}","data":{"version":"${command.version}","id":"${command.id}","name":"${rollPreferences.type}","type":1},"nonce":"${++Discord.nonce}"}\r\n------BDR--\r\n` }); } } const AutoMudae = { users: [], /// MudaeUser[] preferences: null, /// Map<string, any> state: E.AUTOMUDAE_STATE.INJECT, chatObserver: new MutationObserver(ms => ms.forEach(m => { if (m.addedNodes.length) { handleNewChatAppend(m.addedNodes) } })), cdGatherInfo: 0, cdRoll: 0, lastResetHash: '', timers: { _t: new Map(), set(identifier, callback, ms, isInterval = false) { if (this._t.has(identifier)) clearTimeout(identifier); const timer = isInterval ? setInterval(callback, ms) : setTimeout(callback, ms); this._t.set(identifier, timer); }, clear() { [...this._t.values()].forEach(t => { clearTimeout(t); clearInterval(t) }); this._t.clear(); } }, toasts: { add(E_TOAST, formattableText, el_SubjectMessage = null) { if (!DOM.el_ToastsWrapper) return; const text = formattableText.replace(/\[(.+?)\]/g, "<strong>$1</strong>"); const el_Toast = document.createElement("div"); el_Toast.classList.add("automudae-toast", E_TOAST); el_Toast.innerHTML = `<span>${text}</span>`; if (el_SubjectMessage){ el_Toast.classList.add("link"); el_Toast.onclick = function(){ if (this.classList.contains("missing")){ this.classList.remove("missing"); void this.offsetWidth; this.classList.add("missing"); return; } if (!el_SubjectMessage) return this.classList.add("missing"); const loadedMessages = [...el_SubjectMessage.parentElement.children]; const messageIndex = loadedMessages.indexOf(el_SubjectMessage); const distanceFromBottom = loadedMessages.length - messageIndex; const quantityMargin = 19; if (messageIndex >= quantityMargin && distanceFromBottom <= quantityMargin){ el_SubjectMessage.scrollIntoView(); return; } this.classList.add("missing"); }; } DOM.el_ToastsWrapper.appendChild(el_Toast); }, clear() { if (DOM.el_ToastsWrapper) DOM.el_ToastsWrapper.innerHTML = ""; } }, /// Info hasNeededInfo() { return this.users.every(user => user.hasNeededInfo()); }, isLastReset() { const now = new Date(), h = now.getHours(), m = now.getMinutes(); return (h % 3 == 2 && m >= 36) || (h % 3 == 0 && m < 36) }, /// Utils mudaeTimeToMs(timeString) { if (!timeString.includes("h")) return Number(timeString) * 60 * 1000; const match = /(\d+h)?\s?(\d+)?/.exec(timeString); if (!match) return; const h = match[1]; const m = match[2]; let totalMs = 0; if (h) totalMs += Number(h.replace(/\D/g, '')) * 60 * 60 * 1000; if (m) totalMs += Number(m) * 60 * 1000; return totalMs; }, getMarriageableUser(preferableNicknames) { if (!preferableNicknames || preferableNicknames.length === 0){ return this.users.find(user => user.info.get(E.MUDAE_INFO.CAN_MARRY)); } let marriageableUser; for (let i = 0; i < this.users.length; i++) { const user = this.users[i]; if (user.info.get(E.MUDAE_INFO.CAN_MARRY)){ marriageableUser = user; if (preferableNicknames.includes(user.nick)) break; } } return marriageableUser; }, clearError(){ if (DOM.el_ErrorPopup) DOM.el_ErrorPopup = DOM.el_ErrorPopup.remove(); }, error(msg) { this.clearError(); if (!msg) return; const el_ErrorPopup = document.createElement("div"); el_ErrorPopup.id = "automudae-error"; el_ErrorPopup.innerHTML = `<span>${msg}</span>`; document.body.appendChild(el_ErrorPopup); DOM.el_ErrorPopup = el_ErrorPopup; }, /// Workflow renderTokenList(){ const isTokenValid = token => token && token.length >= 70 && token.length < 80 && /\w+\.\w+\.[-\w]+$/.test(token); function handleTokenInput(){ if (!isTokenValid(this.value)) this.parentElement.remove(); } const el_TokenListWrapper = document.createElement("div"); el_TokenListWrapper.id = "automudae-tokenlist-wrapper"; el_TokenListWrapper.innerHTML = `<div id="automudae-tokenlist"><h3>Token List</h3><div><ul></ul><div id="automudae-tokenlist-controls"><div id="automudae-tokenlist-add">Add</div><div id="automudae-tokenlist-clear">Clear</div></div></div><div id="automudae-tokenlist-accept">Accept</div></div>`; document.body.appendChild(el_TokenListWrapper); const el_TokenList = document.querySelector("#automudae-tokenlist ul"); const addInputField = (defaultValue) => { if (el_TokenList.childElementCount < 20){ const el_TokenInput = document.createElement("input"); el_TokenInput.onblur = handleTokenInput; if (defaultValue) el_TokenInput.value = defaultValue; el_TokenList.appendChild(document.createElement("li").appendChild(el_TokenInput).parentElement); } }; document.getElementById("automudae-tokenlist-clear").onclick = () => el_TokenList.innerHTML = ""; document.getElementById("automudae-tokenlist-add").onclick = () => addInputField(); document.getElementById("automudae-tokenlist-accept").onclick = () => { const tokenSet = new Set(); document.querySelectorAll("#automudae-tokenlist input").forEach(el_Input => { const token = el_Input.value; if (isTokenValid(token)) tokenSet.add(token); }); if (tokenSet.size === 0){ AutoMudae.error("Please provide a valid token."); return; } const tokenList = [...tokenSet]; GM_setValue(E.GMVALUE.TOKENLIST, tokenList.join(";")); el_TokenListWrapper.remove(); AutoMudae.inject(tokenList); }; GM_getValue(E.GMVALUE.TOKENLIST)?.split(";").forEach(token => addInputField(token)); }, toggleInjectionButtons(){ if (DOM.el_InjectionsWrapper){ DOM.el_InjectionsWrapper.classList.toggle("automudae-hide"); return; } const el_LoggedUsersButton = document.createElement("div"); el_LoggedUsersButton.id = "automudae-use-logged-button"; el_LoggedUsersButton.innerHTML = "<span>Use Logged Users</span>"; const el_TokenListButton = document.createElement("div"); el_TokenListButton.id = "automudae-use-tokenlist-button"; el_TokenListButton.innerHTML = "<span>Use Token List</span>"; el_LoggedUsersButton.onclick = (_e) => AutoMudae.inject(false); el_TokenListButton.onclick = (_e) => AutoMudae.renderTokenList(); const el_InjectionsWrapper = document.createElement("div"); el_InjectionsWrapper.id = "automudae-injections-wrapper"; el_InjectionsWrapper.appendChild(el_LoggedUsersButton); el_InjectionsWrapper.appendChild(el_TokenListButton); DOM.el_InjectionsWrapper = el_InjectionsWrapper; document.body.appendChild(el_InjectionsWrapper); }, preRender() { const el_DiscordToolBar = document.querySelector("[class^='toolbar']"); el_DiscordToolBar.innerHTML = ""; /// Run Button const el_RunButton = document.createElement("div"); el_RunButton.id = "automudae-run-button"; el_RunButton.classList.add("automudae-hide"); el_DiscordToolBar.appendChild(el_RunButton); DOM.el_RunButton = el_RunButton; /// State Text const el_StateWrapper = document.createElement("div"); el_StateWrapper.id = "automudae-state"; el_StateWrapper.innerHTML = "<b>AutoMudae:</b>"; const el_StateSpan = document.createElement("span"); el_StateSpan.appendChild(document.createTextNode("Idle")); el_StateWrapper.appendChild(el_StateSpan); el_DiscordToolBar.appendChild(el_StateWrapper); DOM.el_StateSpan = el_StateSpan; /// Injection Buttons this.toggleInjectionButtons(); }, inject(tokenList) { this.toggleInjectionButtons(); logger.info("Injecting..."); this.setState(E.AUTOMUDAE_STATE.SETUP); AutoMudae.setup(tokenList) .then(() => { this.clearError(); const requirements = "Required:\n- All your accounts should have custom avatars\n- Arrange your $TU to expose all needed information: $ta claim rolls daily keys kakerareact kakerapower kakerainfo kakerastock rt dk rollsreset\n- Set your claim feedback to default: $rc none\n- Set your rolls left message to default: $rollsleft 0\nCan only roll with slash commands.\nDon't search for messages in Discord.\n- Don't scroll up the channel."; const recommendations = "Recommended:\n- Use slash rolls.\n- Don't use non-slash rolls while the channel is in peak usage by other members.\n- Set your user order priorizing roll and kakera claiming."; const exposeLogger = this.preferences.get(E.PREFERENCES.EXTRA).logger; if (exposeLogger) { const doNothing = () => { }; for (const method in logger) { if (!Object.hasOwn(logger, method)) continue; window.console[method] = doNothing; } console.clear(); window.logger = logger; logger.debug("Turned off native console. Use logger instead. I recommend disabling network log, since Discord usualy prompt a lot of these."); logger.debug(requirements); logger.debug(recommendations); logger._reprompt(); } this.render(); this.tryEnable(); if (!exposeLogger) { logger.info(requirements); logger.info(recommendations); } }) .catch(err => { logger.error(err); this.error(err); this.setState(E.AUTOMUDAE_STATE.INJECT); this.toggleInjectionButtons(); }); }, async setup(tokenList){ return new Promise(async (resolve, reject) => { const windowPathname = window.location?.pathname; if (!windowPathname) { reject("Couldn't retrieve current window URL."); } const [_, pathDiscriminator, guildId, channelId] = windowPathname.split("/"); if (pathDiscriminator !== "channels") { reject("You must be viewing the desired channel."); } if (!guildId || !channelId) { reject("Couldn't retrieve active guild or channel."); } DOM.el_ChannelList = document.querySelector("#channels > ul"); DOM.el_MemberList = document.querySelector("div[class^='members'] > div"); DOM.el_Chat = document.querySelector("ol[class^='scrollerInner']"); DOM.el_ChatWrapper = document.querySelector("main[class^='chatContent']"); if (!DOM.el_Chat || !DOM.el_MemberList || !DOM.el_ChannelList || !DOM.el_ChatWrapper) { reject("Make sure you're viewing the desired channel and the page is fully loaded."); } if (!localStorage || !localStorage.MultiAccountStore || !localStorage.tokens) { reject("Couldn't retrieve information from Discord."); } const users = []; if (tokenList){ for (let i = 0; i < tokenList.length; i++) { users.push(await new MudaeUser(tokenList[i])); } } else { const storeUsers = JSON.parse(localStorage.MultiAccountStore)?._state.users; const tokens = JSON.parse(localStorage.tokens); if (!storeUsers || !tokens) { return "Couldn't retrieve information about your accounts."; } for (let i = 0; i < storeUsers.length; i++) { const { id, username, avatar } = storeUsers[i]; const token = tokens[id]; if (!token) { return `Couldn't retrieve information about user [${username}]`; } users.push(await new MudaeUser(token, id, username, avatar)); } } this.users = users; Discord.info.set(E.DISCORD_INFO.CHANNEL_ID, channelId); Discord.info.set(E.DISCORD_INFO.GUILD_ID, guildId); const defaultPreferences = `[ ["${E.PREFERENCES.KAKERA}", {"kakeraP": false, "kakera": false, "kakeraT": false, "kakeraG": false, "kakeraY": false, "kakeraO": false, "kakeraR": false, "kakeraW": false, "kakeraL": false}], ["${E.PREFERENCES.MENTIONS}", ""], ["${E.PREFERENCES.ROLL}", {"enabled":true,"type":"wx"}], ["${E.PREFERENCES.SOUND}", {"foundcharacter":true,"marry":true,"cantmarry":true, "lastresetnorolls":true,"soulmate":true,"wishsteal":true}], ["${E.PREFERENCES.EXTRA}", {"logger":true}] ]`; const savedVersion = GM_getValue(E.GMVALUE.VERSION, null); const isPreferencesOutdated = !savedVersion || savedVersion !== GM_info.script.version; const stringifiedPreferences = isPreferencesOutdated ? defaultPreferences : GM_getValue(E.GMVALUE.PREFERENCES, defaultPreferences); this.preferences = new Map(JSON.parse(stringifiedPreferences)); GM_setValue(E.GMVALUE.VERSION, GM_info.script.version); resolve(); }); }, render() { logger.info("Rendering..."); const el_InfoPanel = document.createElement("div"); el_InfoPanel.id = "automudae-panel-info"; el_InfoPanel.innerHTML = ` <h1>Auto-Mudae Info</h1> <div> <div class="automudae-section"> <h2>Collected</h2> <div class="automudae-section-body"> <div class="automudae-row"> <span>Kakera:</span> <div><img class="emoji" src="https://cdn.discordapp.com/emojis/469835869059153940.webp?quality=lossless"><span id="automudae-field-${E.INFO_FIELD.KAKERA}">0</span></div> </div> <div class="automudae-row"> <span>Characters:</span> </div> <ul id="automudae-field-${E.INFO_FIELD.COLLECTED_CHARACTERS}"></ul> </div> </div> <div class="automudae-section" id="automudae-section-status"> <h2>Status</h2> <div class="automudae-section-body"> <div class="automudae-row-expandable"> <div class="automudae-row"> <span>Rolls:</span> <div><span>(</span><span id="automudae-field-${E.INFO_FIELD.ROLLS_LEFT}">?</span><span>/</span><span id="automudae-field-${E.INFO_FIELD.ROLLS_MAX}">?</span><span>)</span></div> </div> <div> ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><span>(</span><span id="automudae-field-${E.INFO_FIELD.ROLLS_LEFT}-${user.id}">?</span><span>/</span><span id="automudae-field-${E.INFO_FIELD.ROLLS_MAX}-${user.id}">?</span><span>)</span></div></div>`).join("")} </div> </div> <div class="automudae-row-expandable"> <div class="automudae-row"> <span>Power:</span> <div><span id="automudae-field-${E.INFO_FIELD.POWER}">?</span><span>%</span></div> </div> <div> ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><div><span id="automudae-field-${E.INFO_FIELD.POWER}-${user.id}">?</span><span>%</span></div></div></div>`).join("")} </div> </div> <div class="automudae-row-expandable"> <div class="automudae-row"> <span>Kakera Power Consumption:</span> <div><span id="automudae-field-${E.INFO_FIELD.POWER_CONSUMPTION}">?</span><span>%</span></div> </div> <div> ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><div><span id="automudae-field-${E.INFO_FIELD.POWER_CONSUMPTION}-${user.id}">?</span><span>%</span></div></div></div>`).join("")} </div> </div> <div class="automudae-row-expandable"> <div class="automudae-row"> <span>Can Marry?</span> <span id="automudae-field-${E.INFO_FIELD.CAN_MARRY}">?</span> </div> <div> ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><span id="automudae-field-${E.INFO_FIELD.CAN_MARRY}-${user.id}">?</span></div></div>`).join("")} </div> </div> <div class="automudae-row-expandable"> <div class="automudae-row"> <span>Can RT?</span> <span id="automudae-field-${E.INFO_FIELD.CAN_RT}">?</span> </div> <div> ${this.users.map(user => `<div class="automudae-row"><span>${user.username}:</span><div><span id="automudae-field-${E.INFO_FIELD.CAN_RT}-${user.id}">?</span></div></div>`).join("")} </div> </div> </div> </div> </div> `; const el_ConfigPanel = document.createElement("div"); el_ConfigPanel.id = "automudae-panel-config"; el_ConfigPanel.innerHTML = ` <h1>Auto-Mudae Config</h1> <div> <div class="automudae-section" id="automudae-section-kakera"> <h2>Kakera to Collect</h2> <div class="automudae-section-body"> <div><input type="checkbox" id="opt-kakera-kakeraP"><label for="opt-kakera-kakeraP"><img class="emoji" src="https://cdn.discordapp.com/emojis/609264156347990016.webp?quality=lossless"></label></div> <div><input type="checkbox" id="opt-kakera-kakera"><label for="opt-kakera-kakera"><img class="emoji" src="https://cdn.discordapp.com/emojis/469835869059153940.webp?quality=lossless"></label></div> <div><input type="checkbox" id="opt-kakera-kakeraT"><label for="opt-kakera-kakeraT"><img class="emoji" src="https://cdn.discordapp.com/emojis/609264180851376132.webp?quality=lossless"></label></div> <div><input type="checkbox" id="opt-kakera-kakeraG"><label for="opt-kakera-kakeraG"><img class="emoji" src="https://cdn.discordapp.com/emojis/609264166381027329.webp?quality=lossless"></label></div> <div><input type="checkbox" id="opt-kakera-kakeraY"><label for="opt-kakera-kakeraY"><img class="emoji" src="https://cdn.discordapp.com/emojis/605112931168026629.webp?quality=lossless"></label></div> <div><input type="checkbox" id="opt-kakera-kakeraO"><label for="opt-kakera-kakeraO"><img class="emoji" src="https://cdn.discordapp.com/emojis/605112954391887888.webp?quality=lossless"></label></div> <div><input type="checkbox" id="opt-kakera-kakeraR"><label for="opt-kakera-kakeraR"><img class="emoji" src="https://cdn.discordapp.com/emojis/605112980295647242.webp?quality=lossless"></label></div> <div><input type="checkbox" id="opt-kakera-kakeraW"><label for="opt-kakera-kakeraW"><img class="emoji" src="https://cdn.discordapp.com/emojis/608192076286263297.webp?quality=lossless"></label></div> <div><input type="checkbox" id="opt-kakera-kakeraL"><label for="opt-kakera-kakeraL"><img class="emoji" src="https://cdn.discordapp.com/emojis/815961697918779422.webp?quality=lossless"></label></div> </div> </div> <div class="automudae-section"> <h2>Interesting Mentions</h2> <div class="automudae-section-body"> <textarea spellcheck="false" id="opt-mentions"></textarea> </div> </div> <div class="automudae-section"> <h2>Roll</h2> <div class="automudae-section-body"> <div> <input type="checkbox" id="opt-roll-enabled"><label for="opt-roll-enabled"><span>Enabled</span></label> </div> <div> <select id="opt-roll-type"> <option value="wx">wx</option> <option value="wa">wa</option> <option value="wg">wg</option> <option value="hx">hx</option> <option value="ha">ha</option> <option value="hg">hg</option> </select> </div> </div> </div> <div class="automudae-section"> <h2>Sound</h2> <div class="automudae-section-body"> <div> <input type="checkbox" id="opt-sound-foundcharacter"><label for="opt-sound-foundcharacter"><span>Found character</span></label> </div> <div> <input type="checkbox" id="opt-sound-marry"><label for="opt-sound-marry"><span>Marry</span></label> </div> <div> <input type="checkbox" id="opt-sound-cantmarry"><label for="opt-sound-cantmarry"><span>Can't marry</span></label> </div> <div> <input type="checkbox" id="opt-sound-lastresetnorolls"><label for="opt-sound-lastresetnorolls"><span>Can't roll in the last reset</span></label> </div> <div> <input type="checkbox" id="opt-sound-soulmate"><label for="opt-sound-soulmate"><span>New soulmates</span></label> </div> <div> <input type="checkbox" id="opt-sound-wishsteal"><label for="opt-sound-wishsteal"><span>Wish steals</span></label> </div> </div> </div> <div class="automudae-section"> <h2>Extra</h2> <div class="automudae-section-body"> <div data-requirerestart> <input type="checkbox" id="opt-extra-logger"><label for="opt-extra-logger"><span>Replace Console with Logger</span></label> </div> </div> </div> </div> `; const el_ToastsWrapper = document.createElement("div"); el_ToastsWrapper.id = "automudae-toasts-wrapper"; DOM.el_ChannelList.prepend(el_InfoPanel); DOM.el_MemberList.prepend(el_ConfigPanel); DOM.el_ChatWrapper.prepend(el_ToastsWrapper); DOM.el_ToastsWrapper = el_ToastsWrapper; document.querySelector("[class^='channelTextArea']").style.width = "60%"; /// Make side panels collapsable function collapse() { this.parentElement.classList.toggle("collapsed") }; document.querySelectorAll("[id^='automudae-panel'] > h1").forEach(el_Header => el_Header.onclick = collapse); /// Config Update & Functionality function handleCheckboxPreference() { const [_, category, key] = this.id.split("-"); const categoryPreferences = AutoMudae.preferences.get(category); categoryPreferences[key] = this.checked; AutoMudae.preferences.set(category, categoryPreferences); AutoMudae.savePreferences(); }; document.querySelectorAll("input[type='checkbox'][id^='opt-']").forEach(el_OptCheckbox => { const [_, category, key] = el_OptCheckbox.id.split("-"); el_OptCheckbox.checked = AutoMudae.preferences.get(category)[key]; el_OptCheckbox.onchange = handleCheckboxPreference; }); const el_OptMentions = document.getElementById("opt-mentions"); el_OptMentions.value = this.preferences.get(E.PREFERENCES.MENTIONS); el_OptMentions.onblur = function () { AutoMudae.preferences.set(E.PREFERENCES.MENTIONS, this.value); AutoMudae.savePreferences(); }; const el_OptRollType = document.getElementById("opt-roll-type"); el_OptRollType.value = this.preferences.get(E.PREFERENCES.ROLL).type; el_OptRollType.onchange = function () { const rollPreferences = AutoMudae.preferences.get(E.PREFERENCES.ROLL); rollPreferences.type = this.value; AutoMudae.preferences.set(E.PREFERENCES.ROLL, rollPreferences); AutoMudae.savePreferences(); }; }, tryEnable() { if (this.state !== E.AUTOMUDAE_STATE.SETUP) return; if (!Object.values(E.DISCORD_INFO).every(info => Discord.info.has(info))) return; this.setState(E.AUTOMUDAE_STATE.IDLE); DOM.el_RunButton.onclick = (_e) => AutoMudae.toggle(); DOM.el_RunButton.classList.remove("automudae-hide"); logger.plus("Ready to go!"); }, toggle() { if (this.state !== E.AUTOMUDAE_STATE.IDLE && this.state !== E.AUTOMUDAE_STATE.RUN) return; if (this.state === E.AUTOMUDAE_STATE.IDLE) { this.clearError(); let msToStartResetHandler = 1; const now = new Date(); if (now.getMinutes() !== 37) { const nextReset = new Date(now); nextReset.setHours(now.getMinutes() > 37 ? now.getHours() + 1 : now.getHours(), 37); msToStartResetHandler = nextReset - now; } this.timers.set("think", this.think, INTERVAL_THINK, true); this.timers.set("initHourlyResetHandler", () => { AutoMudae.handleHourlyReset(); AutoMudae.timers.set("HandleHourlyReset", AutoMudae.handleHourlyReset, 1 * 60 * 60 * 1000, true) }, msToStartResetHandler); this.chatObserver.observe(DOM.el_Chat, { childList: true }); this.setState(E.AUTOMUDAE_STATE.RUN); logger.log("Running.."); return; } this.chatObserver.disconnect(); this.timers.clear(); this.users.forEach(user => { if (user.sendTUTimer) clearTimeout(user.sendTUTimer); user.info.clear(); }); this.setState(E.AUTOMUDAE_STATE.IDLE); logger.log("Turned off."); }, setState(E_STATE) { this.state = E_STATE; const stateTexts = {}; stateTexts[E.AUTOMUDAE_STATE.INJECT] = "Idle"; stateTexts[E.AUTOMUDAE_STATE.SETUP] = "Setting up..."; stateTexts[E.AUTOMUDAE_STATE.ERROR] = "Error!"; stateTexts[E.AUTOMUDAE_STATE.IDLE] = "Idle"; stateTexts[E.AUTOMUDAE_STATE.RUN] = "Running..."; DOM.el_StateSpan.innerText = stateTexts[E_STATE]; if ((E_STATE === E.AUTOMUDAE_STATE.RUN || E_STATE === E.AUTOMUDAE_STATE.IDLE) && DOM.el_RunButton){ const isRun = E_STATE === E.AUTOMUDAE_STATE.RUN; DOM.el_RunButton.classList[isRun ? "add" : "remove"]("running"); } }, think() { const now = performance.now(); const dateNow = new Date(), h = dateNow.getHours(), m = dateNow.getMinutes(); if (!AutoMudae.hasNeededInfo()) { if (now - AutoMudae.cdGatherInfo < 1000) return; for (let i = 0; i < AutoMudae.users.length; i++) { const user = AutoMudae.users[i]; if (!user.hasNeededInfo()) { logger.log(`Gathering needed info for user [${user.username}]..`); user.send("$tu"); break; } } AutoMudae.cdGatherInfo = now; return; } const userWithRolls = AutoMudae.users.find(user => user.info.get(E.MUDAE_INFO.ROLLS_LEFT) > 0); if (AutoMudae.preferences.get(E.PREFERENCES.ROLL).enabled) { if (userWithRolls && now - Discord.lastMessageTime > INTERVAL_ROLL && now - AutoMudae.cdRoll > (INTERVAL_ROLL * .5)) { userWithRolls.roll(); AutoMudae.cdRoll = now; } } if (!userWithRolls && m > 38 && AutoMudae.isLastReset() && AutoMudae.getMarriageableUser()) { const currentResetHash = `${dateNow.toDateString()} ${h}`; if (AutoMudae.lastResetHash !== currentResetHash) { AutoMudae.lastResetHash = currentResetHash; //# Add option to auto-use $us or $rolls const warnMessage = "You have no more rolls, can still marry and it's the last reset. You could use $us or $rolls, then $tu."; logger.warn(warnMessage); AutoMudae.toasts.add(E.TOAST.WARN, warnMessage); if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).lastresetnorolls) SOUND.lastResetNoRolls(); } } }, savePreferences() { GM_setValue(E.GMVALUE.PREFERENCES, JSON.stringify(this.preferences)); }, updateInfoPanel(E_INFO_FIELD, content, user) { const el_OverallField = document.getElementById(`automudae-field-${E_INFO_FIELD}`); if (E_INFO_FIELD === E.INFO_FIELD.KAKERA) { const newKakera = Number(el_OverallField.innerText) + Number(content); el_OverallField.innerText = newKakera; return; } if (E_INFO_FIELD === E.INFO_FIELD.COLLECTED_CHARACTERS) { const el_CharacterItem = document.createElement("li"); el_CharacterItem.appendChild(document.createTextNode((user ? `[${user.username}] ` : '') + content)); el_OverallField.appendChild(el_CharacterItem); return; } const el_UserField = document.getElementById(`automudae-field-${E_INFO_FIELD}-${user.id}`); el_UserField.innerText = content; if (E_INFO_FIELD === E.INFO_FIELD.ROLLS_LEFT || E_INFO_FIELD === E.INFO_FIELD.ROLLS_MAX) { const numeralFields = [...document.querySelectorAll(`[id^='automudae-field-${E_INFO_FIELD}-']`)].map(el_UserField => el_UserField.innerText).filter(text => /\d+/.test(text)); if (numeralFields.length > 0) el_OverallField.innerText = numeralFields.reduce((total, current) => Number(total) + Number(current)); return; } if (E_INFO_FIELD === E.INFO_FIELD.POWER) { const highestPower = getLast([...document.querySelectorAll(`[id^='automudae-field-${E_INFO_FIELD}-']`)].map(el_UserField => el_UserField.innerText).filter(text => /\d+/.test(text)).sort((a, b) => Number(a) - Number(b))); if (highestPower) el_OverallField.innerText = `↓ ${highestPower}`; return; } if (E_INFO_FIELD === E.INFO_FIELD.POWER_CONSUMPTION) { const lowestConsumption = [...document.querySelectorAll(`[id^='automudae-field-${E_INFO_FIELD}-']`)].map(el_UserField => el_UserField.innerText).filter(text => /\d+/.test(text)).sort((a, b) => Number(a) - Number(b))[0]; if (lowestConsumption) el_OverallField.innerText = `↑ ${lowestConsumption}`; return; } if (E_INFO_FIELD === E.INFO_FIELD.CAN_MARRY || E_INFO_FIELD === E.INFO_FIELD.CAN_RT) { const hasAny = [...document.querySelectorAll(`[id^='automudae-field-${E_INFO_FIELD}-']`)].some(el_UserField => el_UserField.innerText === "Yes"); el_OverallField.innerText = hasAny ? "Yes" : "No"; return; } }, handleHourlyReset() { if (!AutoMudae.hasNeededInfo()) return; logger.log("Hourly reset. Gathering updated status.."); AutoMudae.users.forEach(user => user.info.delete(E.MUDAE_INFO.ROLLS_LEFT)); } }; //# Remove this exposure window.Discord = Discord; window.AutoMudae = AutoMudae; function observeToReact(el_Message, userToReact) { let runs = 0; const observer = setInterval(() => { if (!el_Message || runs++ >= 30) return clearInterval(observer); const el_ReactionImg = el_Message.querySelector(`div[class^='reactionInner']${userToReact ? "" : "[aria-label^='kakera']"}[aria-label*='1 rea'] img`); if (!el_ReactionImg) return; clearInterval(observer); if (userToReact) { const emoji = E.EMOJI[el_ReactionImg.alt]; if (!emoji) { const errMessage = `Couldn't find emoji code for [${el_ReactionImg.alt}]. Address this to AutoMudae's creator, please.`; logger.error(errMessage); AutoMudae.toasts.add(E.TOAST.CRITICAL, errMessage, el_Message); return; } userToReact.react(el_Message, emoji); return; } const kakeraCode = el_ReactionImg.alt; if (!AutoMudae.preferences.get(E.PREFERENCES.KAKERA)[kakeraCode]) return; const userWithEnoughPower = kakeraCode === E.KAKERA.PURPLE ? AutoMudae.users[0] : AutoMudae.users.find(user => user.info.get(E.MUDAE_INFO.POWER) >= user.info.get(E.MUDAE_INFO.CONSUMPTION)); if (userWithEnoughPower) userWithEnoughPower.react(el_Message, E.EMOJI_KAKERA[kakeraCode]); }, 100); }; function handleNewChatAppend(el_Children) { document.querySelector("div[class^='scrollerSpacer']")?.scrollIntoView(); el_Children.forEach(el_Child => { if (el_Child.tagName !== "LI") return; Discord.lastMessageTime = performance.now(); const el_Message = el_Child; if (!Discord.Message.isFromMudae(el_Message)) return; const el_PreviousElement = el_Message.previousElementSibling ? (el_Message.previousElementSibling.id === "---new-messages-bar" ? el_Message.previousElementSibling.previousElementSibling : el_Message.previousElementSibling) : null; /// Handle player commands if (el_PreviousElement) { const el_PreviousMessage = el_PreviousElement; const user = Discord.Message.isFromMe(el_PreviousMessage); if (user) { const command = el_PreviousMessage.querySelector("div[id^='message-content']")?.innerText; const mudaeResponse = el_Message.querySelector("div[id^='message-content']")?.innerText; if (command && mudaeResponse && mudaeResponse.startsWith(`${user.username}, `)) { if (command === "$tu") { const matchRolls = /tem (\d+) rolls/.exec(mudaeResponse); if (matchRolls) { const rolls = Number(matchRolls[1]); const hasRollsMax = user.info.has(E.MUDAE_INFO.ROLLS_MAX); if (!hasRollsMax || user.info.get(E.MUDAE_INFO.ROLLS_MAX) < rolls) { user.info.set(E.MUDAE_INFO.ROLLS_MAX, rolls); AutoMudae.updateInfoPanel(E.INFO_FIELD.ROLLS_MAX, rolls, user); } user.info.set(E.MUDAE_INFO.ROLLS_LEFT, rolls); AutoMudae.updateInfoPanel(E.INFO_FIELD.ROLLS_LEFT, rolls, user); } const matchPower = /Power: (\d+)%/.exec(mudaeResponse); if (matchPower) { const power = Number(matchPower[1]); user.info.set(E.MUDAE_INFO.POWER, power); AutoMudae.updateInfoPanel(E.INFO_FIELD.POWER, power, user); } if (/\$rt/.test(mudaeResponse)) { const cooldownRTMatch = /: (.+) min. \(\$rtu\)/.exec(mudaeResponse); user.info.set(E.MUDAE_INFO.CAN_RT, !cooldownRTMatch); if (cooldownRTMatch) { logger.log(`Scheduled a RT check for user [${user.username}]. [${cooldownRTMatch[1]}]`); user.setTUTimer(AutoMudae.mudaeTimeToMs(cooldownRTMatch[1]) + 500); } const canRT = user.info.get(E.MUDAE_INFO.CAN_RT); AutoMudae.updateInfoPanel(E.INFO_FIELD.CAN_RT, canRT ? "Yes" : "No", user); } else { user.info.set(E.MUDAE_INFO.CAN_RT, false); AutoMudae.updateInfoPanel(E.INFO_FIELD.CAN_RT, "No", user); } if (/casar/.test(mudaeResponse)) { const cantMarry = /se casar novamente (.+) min/.exec(mudaeResponse); user.info.set(E.MUDAE_INFO.CAN_MARRY, !cantMarry); AutoMudae.updateInfoPanel(E.INFO_FIELD.CAN_MARRY, cantMarry ? "No" : "Yes", user); } const matchKakeraConsumption = /kakera consume (\d+)%/.exec(mudaeResponse); if (matchKakeraConsumption) { const consumption = Number(matchKakeraConsumption[1]); user.info.set(E.MUDAE_INFO.CONSUMPTION, consumption); AutoMudae.updateInfoPanel(E.INFO_FIELD.POWER_CONSUMPTION, consumption, user); } if (!user.hasNeededInfo()) { AutoMudae.toggle(); const errMsg = `Couldn't retrieve needed info for user [${user.username}]. Make sure your $tu configuration exposes every information.`; logger.error(errMsg); AutoMudae.error(errMsg); return; } logger.info(`Got all needed info for user [${user.username}].`); return; }; } } } if (!AutoMudae.hasNeededInfo()) return; const el_MessageContent = el_Message.querySelector("div[id^='message-content']"); if (el_MessageContent) { const messageContent = el_MessageContent.innerText; /// Handle character claims & steals const characterClaimMatch = /(.+) e (.+) agora são casados!/.exec(messageContent.trim()); if (characterClaimMatch || messageContent.includes("(Silver IV Bônus)")) { let usernameThatClaimed, characterName; if (characterClaimMatch) { [_, usernameThatClaimed, characterName] = characterClaimMatch; } let user; if (usernameThatClaimed) { user = AutoMudae.users.find(user => user.username === usernameThatClaimed); } /// Claim if (user) { user.info.set(E.MUDAE_INFO.CAN_MARRY, false); AutoMudae.updateInfoPanel(E.INFO_FIELD.CAN_MARRY, "No", user); AutoMudae.updateInfoPanel(E.INFO_FIELD.COLLECTED_CHARACTERS, characterName, user); if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).marry) SOUND.marry(); const logMessage = `User [${usernameThatClaimed}] claimed character [${characterName}]!`; logger.plus(logMessage); AutoMudae.toasts.add(E.TOAST.CHARCLAIM, logMessage, el_Message); el_Message.classList.add("plus"); document.querySelectorAll("[class^='embedAuthorName']").forEach(el_AuthorName => { if (el_AuthorName.innerText === characterName) { const el_ParentMessage = el_AuthorName.closest("li"); el_ParentMessage.classList.add("plus"); } }); } else { const el_Mentions = el_Message.querySelectorAll("span.mention"); let isIncludingMe = false; for (let i = 0; i < el_Mentions.length; i++) { const mentionedNick = el_Mentions[i].innerText.substr(1); if (AutoMudae.users.some(user => user.nick === mentionedNick)) { isIncludingMe = true; break; } } /// Steal if (isIncludingMe) { if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).wishsteal) SOUND.critical(); el_Message.classList.add("critical"); if (characterName) { document.querySelectorAll("[class^='embedAuthorName']").forEach(el_AuthorName => { if (el_AuthorName.innerText === characterName) { const el_ParentMessage = el_AuthorName.closest("li"); el_ParentMessage.classList.add("critical"); } }); } const stealWarn = characterClaimMatch ? `User [${usernameThatClaimed}] claimed character [${characterName}] wished by you.` : "A character wished by you was claimed by another user."; logger.warn(stealWarn); AutoMudae.toasts.add(E.TOAST.CRITICAL, stealWarn, el_Message); } } return; } /// Handle "no more rolls" messages const noMoreRollsMatch = /(.+), os rolls são limitado/.exec(messageContent); if (noMoreRollsMatch) { const user = AutoMudae.users.find(user => user.username === noMoreRollsMatch[1]); return user && setTimeout(() => user.send("$tu"), 250); } const el_KakeraClaimStrong = el_Message.querySelector("div[id^='message-content'] span[class^='emojiContainer'] + strong"); /// Handle kakera claiming if (el_KakeraClaimStrong) { const kakeraClaimMatch = /^(.+)\s\+(\d+)$/.exec(el_KakeraClaimStrong.innerText); if (kakeraClaimMatch) { const [_, messageUsername, kakeraQuantity] = kakeraClaimMatch; const user = AutoMudae.users.find(user => user.username === messageUsername); if (user) { const kakeraType = el_KakeraClaimStrong.previousElementSibling?.firstElementChild?.alt.replace(/:/g, ''); const powerCost = kakeraType === E.KAKERA.PURPLE ? 0 : user.info.get(E.MUDAE_INFO.CONSUMPTION); if (powerCost > 0) { const newPower = user.info.get(E.MUDAE_INFO.POWER) - powerCost; user.info.set(E.MUDAE_INFO.POWER, newPower); AutoMudae.updateInfoPanel(E.INFO_FIELD.POWER, newPower, user); } el_Message.classList.add("plus"); AutoMudae.updateInfoPanel(E.INFO_FIELD.KAKERA, kakeraQuantity); logger.plus(`+${kakeraQuantity} kakera! [Remaining Power for user [${user.username}]: ${user.info.get(E.MUDAE_INFO.POWER)}%]`); AutoMudae.toasts.add(E.TOAST.KAKERA, `+[${kakeraQuantity}] Kakera`, el_Message); } return; } } } const el_ImageWrapper = el_Message.querySelector("div[class^='embedDescription'] + div[class^='imageContent'] div[class^='imageWrapper']"); /// Handle character messages if (el_ImageWrapper) { const el_Footer = el_Message.querySelector("span[class^='embedFooterText']"); const isCharacterLookupMessage = (el_Footer && (/^\d+ \/ \d+$/.test(el_Footer.innerText) || /^Pertence a .+ ~~ \d+ \/ \d+$/.test(el_Footer.innerText))); if (isCharacterLookupMessage) return; const characterName = el_Message.querySelector("span[class^='embedAuthorName']").innerText; const el_ReplyAvatar = el_Message.querySelector("img[class^='executedCommandAvatar']"); let replyUserId; if (el_ReplyAvatar) { replyUserId = /avatars\/(\d+)\//.exec(el_ReplyAvatar.src); if (!replyUserId) return logger.error("Couldn't get reply user ID for", el_Message); const user = AutoMudae.users.find(user => user.id === replyUserId[1]); if (user) { const rollsLeft = user.info.get(E.MUDAE_INFO.ROLLS_LEFT) - 1; user.info.set(E.MUDAE_INFO.ROLLS_LEFT, rollsLeft); AutoMudae.updateInfoPanel(E.INFO_FIELD.ROLLS_LEFT, rollsLeft, user); if (el_Message.querySelector("div[class^='embedDescription']").innerText.includes("Sua nova ALMA")) { if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).soulmate) SOUND.newSoulmate(); const logMessage = `New soulmate: [${characterName}]!`; logger.plus(logMessage); AutoMudae.toasts.add(E.TOAST.SOULMATE, logMessage, el_Message); } } } if (!el_Footer || el_Footer.innerText.includes("2 ROLLS RESTANTES") && !el_Footer.innerText.includes("Pertence")) { let el_InterestingCharacter, isWished; const mentionedNicknames = [...el_Message.querySelectorAll("span.mention")].map(el_Mention => el_Mention.innerText.substr(1)); for (let i = 0; i < mentionedNicknames.length; i++) { const mentionedNick = mentionedNicknames[i]; if (AutoMudae.users.some(user => user.nick === mentionedNick) || AutoMudae.preferences.get(E.PREFERENCES.MENTIONS).split(",").map(nick => nick.trim()).includes(mentionedNick)) { el_InterestingCharacter = el_Message; isWished = true; break; } } const marriageableUser = AutoMudae.getMarriageableUser(mentionedNicknames); if (marriageableUser && !el_InterestingCharacter && AutoMudae.isLastReset()) { //# Search in a database if (characterName === "hmm") { el_InterestingCharacter = el_Message; }; } if (el_InterestingCharacter) { const logMessage = `Found character [${characterName}]`; logger.info(logMessage); AutoMudae.toasts.add(E.TOAST.INFO, logMessage, el_Message); if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).foundcharacter) SOUND.foundCharacter(); if (marriageableUser) { //# Make it verify if marriageableUser can still marry after all delay calculations (In case of multiple marriageable characters at the same time) if (!isWished) { setTimeout(() => marriageableUser.react(el_Message, pickRandom(Object.values(E.EMOJI))), 8500); return; } const isProtected = !!el_Message.querySelector("img[alt=':wishprotect:']"); if (!isProtected || isProtected && marriageableUser.id === replyUserId){ observeToReact(el_Message, marriageableUser); return; } setTimeout(() => observeToReact(el_Message, marriageableUser), 2905); return; } if (AutoMudae.preferences.get(E.PREFERENCES.SOUND).cantmarry) SOUND.critical(); const warnMessage = `Can't marry right now. You may lose character [${characterName}]`; logger.warn(warnMessage); AutoMudae.toasts.add(E.TOAST.WARN, warnMessage, el_Message); } return; } /// Owned characters if (el_Footer.innerText.includes("Pertence")) { /// Observe kakera reactions append observeToReact(el_Message); } return; } }); } //// SessionId Hook window.console.info = function () { for (const arg of arguments) { const match = /\[READY\] (?:.+) as (.+)/.exec(arg) || /r###ming session (.+),/.exec(arg); if (match) { window.console.info = console.info; Discord.info.set(E.DISCORD_INFO.SESSION_ID, match[1]); AutoMudae.tryEnable(); } } console.info(...arguments); }; //// Main window.addEventListener("load", main, false); function main() { const findToolbarTimer = setInterval(() => { if (document.querySelector("[class^='toolbar']")){ clearInterval(findToolbarTimer); AutoMudae.preRender(); } }, 200); }; })();