// ==UserScript== // @name Undiscord // @description Delete all messages in a Discord channel or DM (Bulk deletion) // @version 5.0.3 // @author victornpb // @homepageURL https://github.com/victornpb/undiscord // @supportURL https://github.com/victornpb/undiscord/issues // @match https://*.discord.com/app // @match https://*.discord.com/channels/* // @match https://*.discord.com/login // @license MIT // @namespace https://github.com/victornpb/deleteDiscordMessages // @contributionURL https://www.buymeacoffee.com/vitim // @grant none // @downloadURL none // ==/UserScript== (function () { 'use strict'; var version = "5.0.3"; var discordStyles = (` /* undiscord window */ #undiscord.browser { box-shadow: var(--elevation-stroke), var(--elevation-high); overflow: hidden; } #undiscord.container, #undiscord .container { background-color: var(--background-secondary); border-radius: 8px; box-sizing: border-box; cursor: default; flex-direction: column; } #undiscord .header { background-color: var(--background-tertiary); height: 48px; align-items: center; min-height: 48px; padding: 0 16px; display: flex; color: var(--header-secondary); } #undiscord .header .icon { color: var(--interactive-normal); margin-right: 8px; flex-shrink: 0; width: 24; height: 24; } #undiscord .header .icon:hover { color: var(--interactive-hover); } #undiscord .header h3 { font-size: 16px; line-height: 20px; font-weight: 500; font-family: var(--font-display); color: var(--header-primary); flex-shrink: 0; margin-right: 16px; } #undiscord .header .spacer { flex-grow: 1; } #undiscord .header .vert-divider { width: 1px; height: 24px; background-color: var(--background-modifier-accent); margin-right: 16px; flex-shrink: 0; } #undiscord legend, #undiscord label { display: block; width: 100%; color: var(--header-secondary); font-size: 12px; line-height: 16px; font-weight: 500; text-transform: uppercase; cursor: default; font-family: var(--font-display); margin-bottom: 8px; } #undiscord .multiInput { display: flex; align-items: center; font-size: 16px; box-sizing: border-box; width: 100%; border-radius: 3px; color: var(--text-normal); background-color: var(--input-background); border: none; transition: border-color 0.2s ease-in-out 0s; } #undiscord .multiInput :first-child { flex-grow: 1; } #undiscord .multiInput button:last-child { margin-right: 4px; } #undiscord .input { font-size: 16px; box-sizing: border-box; width: 100%; border-radius: 3px; color: var(--text-normal); background-color: var(--input-background); border: none; transition: border-color 0.2s ease-in-out 0s; padding: 10px; height: 40px; } #undiscord fieldset { margin-top: 16px; } #undiscord .input-wrapper { display: flex; align-items: center; font-size: 16px; box-sizing: border-box; width: 100%; border-radius: 3px; color: var(--text-normal); background-color: var(--input-background); border: none; transition: border-color 0.2s ease-in-out 0s; } #undiscord input[type="text"], #undiscord input[type="search"], #undiscord input[type="password"], #undiscord input[type="datetime-local"], #undiscord input[type="number"] { font-size: 16px; box-sizing: border-box; width: 100%; border-radius: 3px; color: var(--text-normal); background-color: var(--input-background); border: none; transition: border-color 0.2s ease-in-out 0s; padding: 10px; height: 40px; } #undiscord .divider, #undiscord hr { border: none; margin-bottom: 24px; padding-bottom: 4px; border-bottom: 1px solid var(--background-modifier-accent); } #undiscord .sectionDescription { margin-bottom: 16px; color: var(--header-secondary); font-size: 14px; line-height: 20px; font-weight: 400; } #undiscord a { color: var(--text-link); text-decoration: none; } #undiscord .btn, #undiscord button { position: relative; display: flex; -webkit-box-pack: center; justify-content: center; -webkit-box-align: center; align-items: center; box-sizing: border-box; background: none; border: none; border-radius: 3px; font-size: 14px; font-weight: 500; line-height: 16px; padding: 2px 16px; user-select: none; /* sizeSmall */ width: 60px; height: 32px; min-width: 60px; min-height: 32px; /* lookFilled colorPrimary */ color: rgb(255, 255, 255); background-color: var(--button-secondary-background); } #undiscord .sizeMedium { width: 96px; height: 38px; min-width: 96px; min-height: 38px; } /* lookFilled colorPrimary */ #undiscord .accent { background-color: var(--brand-experiment); } #undiscord .danger { background-color: var(--button-danger-background); } #undiscord .positive { background-color: var(--button-positive-background); } #undiscord .info { font-size: 12px; line-height: 16px; padding: 8px 10px; color: var(--text-muted); } /* Scrollbar */ #undiscord .scroll::-webkit-scrollbar { width: 8px; height: 8px; } #undiscord .scroll::-webkit-scrollbar-corner { background-color: transparent; } #undiscord .scroll::-webkit-scrollbar-thumb { background-clip: padding-box; border: 2px solid transparent; border-radius: 4px; background-color: var(--scrollbar-thin-thumb); min-height: 40px; } #undiscord .scroll::-webkit-scrollbar-track { border-color: var(--scrollbar-thin-track); background-color: var(--scrollbar-thin-track); border: 2px solid var(--scrollbar-thin-track); } /* fade scrollbar */ #undiscord .scroll::-webkit-scrollbar-thumb, #undiscord .scroll::-webkit-scrollbar-track { visibility: hidden; } #undiscord .scroll:hover::-webkit-scrollbar-thumb, #undiscord .scroll:hover::-webkit-scrollbar-track { visibility: visible; } `); var undiscordStyles = (` /**** Undiscord Button ****/ #undicord-btn { position: relative; width: auto; height: 24px; margin: 0 8px; cursor: pointer; color: var(--interactive-normal); flex: 0 0 auto; } #undicord-btn progress { position: absolute; top: 7px; left: 5px; width: 14px; height: 14px; } /**** Undiscord Interface ****/ #undiscord { position: fixed; z-index: 99; top: 44px; right: 10px; display: flex; flex-direction: column; width:800px; height: 80vh; min-width: 610px; max-width: 100vw; min-height: 448px; max-height: 100vh; color: var(--text-normal); border-radius: 4px; background-color: var(--background-secondary); box-shadow: var(--elevation-stroke), var(--elevation-high); will-change: top, left, width, height; } #undiscord .header .icon { cursor: pointer; } #undiscord .window-body { height: calc(100% - 48px); } #undiscord .sidebar { overflow: hidden scroll; overflow-y: auto; width: 270px; min-width: 250px; height: 100%; max-height: 100%; padding: 8px; background: var(--background-secondary); } #undiscord .main { display: flex; max-width: calc(100% - 250px); background-color: var(--background-primary); flex-grow: 1; } #undiscord #logArea { font-family: Consolas, Liberation Mono, Menlo, Courier, monospace; font-size: .75rem; overflow: auto; padding: 10px; user-select: text; flex-grow: 1; flex-grow: 1; } #undiscord .tbar { padding: 8px; background-color: var(--background-secondary-alt); } #undiscord .tbar button { margin-right: 4px; margin-bottom: 4px; } #undiscord .footer { cursor: se-resize; } /**** Elements ****/ #undiscord summary { font-size: 16px; font-weight: 500; line-height: 20px; position: relative; overflow: hidden; margin-bottom: 2px; padding: 6px 10px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; color: var(--interactive-normal); border-radius: 4px; flex-shrink: 0; } #undiscord fieldset { padding-left: 8px; } /* help link */ #undiscord legend a { float: right; text-transform: initial; } #undiscord progress { height: 8px; margin-top: 4px; flex-grow: 1; /* background-color: var(--background-primary); border-radius: 3px; */ } /* #undiscord progress::-webkit-progress-value{ background-color: var(--brand-experiment); } */ /**** functional classes ****/ #undiscord.redact .priv { display: none !important; } #undiscord:not(.redact) .mask { display: none !important; } #undiscord.redact [priv] { -webkit-text-security: disc !important; } #undiscord :disabled { display: none; } /**** layout misc ****/ #undiscord, #undiscord * { box-sizing: border-box; } #undiscord .col { display: flex; flex-direction: column; } #undiscord .row { display: flex; flex-direction: row; align-items: center; } #undiscord .mb1 { margin-bottom: 8px; } `); var buttonHtml = (`
`); var undiscordTemplate = (` `); /** * Delete all messages in a Discord channel or DM * @param {string} authToken Your authorization token * @param {string} authorId Author of the messages you want to delete * @param {string} guildId Server were the messages are located * @param {string} channelId Channel were the messages are located * @param {string} minId Only delete messages after this, leave blank do delete all * @param {string} maxId Only delete messages before this, leave blank do delete all * @param {string} content Filter messages that contains this text content * @param {boolean} hasLink Filter messages that contains link * @param {boolean} hasFile Filter messages that contains file * @param {boolean} includeNsfw Search in NSFW channels * @param {function(string, Array)} extLogger Function for logging * @param {function} stopHndl stopHndl used for stopping * @author Victornpb * @see https://github.com/victornpb/undiscord */ async function deleteMessages(authToken, authorId, guildId, channelId, minId, maxId, content, hasLink, hasFile, includeNsfw, includePinned, pattern, searchDelay, deleteDelay, extLogger, stopHndl, onProgress) { const start = new Date(); let delCount = 0; let failCount = 0; let avgPing; let lastPing; let grandTotal; let throttledCount = 0; let throttledTotalTime = 0; let offset = 0; let iterations = -1; const wait = async ms => new Promise(done => setTimeout(done, ms)); const msToHMS = s => `${s / 3.6e6 | 0}h ${(s % 3.6e6) / 6e4 | 0}m ${(s % 6e4) / 1000 | 0}s`; const escapeHTML = html => html.replace(/[&<"']/g, m => ({ '&': '&', '<': '<', '"': '"', '\'': ''' })[m]); const redact = str => `${escapeHTML(str)}REDACTED`; const queryString = params => params.filter(p => p[1] !== undefined).map(p => p[0] + '=' + encodeURIComponent(p[1])).join('&'); const ask = async msg => new Promise(resolve => setTimeout(() => resolve(window.confirm(msg)), 10)); const printDelayStats = () => log.verb(`Delete delay: ${deleteDelay}ms, Search delay: ${searchDelay}ms`, `Last Ping: ${lastPing}ms, Average Ping: ${avgPing | 0}ms`); const toSnowflake = (date) => /:/.test(date) ? ((new Date(date).getTime() - 1420070400000) * Math.pow(2, 22)) : date; const log = { debug() { return extLogger ? extLogger('debug', arguments) : console.debug.apply(console, arguments); }, info() { return extLogger ? extLogger('info', arguments) : console.info.apply(console, arguments); }, verb() { return extLogger ? extLogger('verb', arguments) : console.log.apply(console, arguments); }, warn() { return extLogger ? extLogger('warn', arguments) : console.warn.apply(console, arguments); }, error() { return extLogger ? extLogger('error', arguments) : console.error.apply(console, arguments); }, success() { return extLogger ? extLogger('success', arguments) : console.info.apply(console, arguments); }, }; async function recurse() { let API_SEARCH_URL; if (guildId === '@me') { API_SEARCH_URL = `https://discord.com/api/v9/channels/${channelId}/messages/`; // DMs } else { API_SEARCH_URL = `https://discord.com/api/v9/guilds/${guildId}/messages/`; // Server } const headers = { 'Authorization': authToken }; if (onProgress) onProgress(-1, 1); let resp; try { const s = Date.now(); resp = await fetch(API_SEARCH_URL + 'search?' + queryString([ ['author_id', authorId || undefined], ['channel_id', (guildId !== '@me' ? channelId : undefined) || undefined], ['min_id', minId ? toSnowflake(minId) : undefined], ['max_id', maxId ? toSnowflake(maxId) : undefined], ['sort_by', 'timestamp'], ['sort_order', 'desc'], ['offset', offset], ['has', hasLink ? 'link' : undefined], ['has', hasFile ? 'file' : undefined], ['content', content || undefined], ['include_nsfw', includeNsfw ? true : undefined], ]), { headers }); lastPing = (Date.now() - s); avgPing = avgPing > 0 ? (avgPing * 0.9) + (lastPing * 0.1) : lastPing; } catch (err) { return log.error('Search request threw an error:', err); } // not indexed yet if (resp.status === 202) { const w = (await resp.json()).retry_after * 1000; throttledCount++; throttledTotalTime += w; log.warn(`This channel wasn't indexed, waiting ${w}ms for discord to index it...`); await wait(w); return await recurse(); } if (!resp.ok) { // searching messages too fast if (resp.status === 429) { const w = (await resp.json()).retry_after * 1000; throttledCount++; throttledTotalTime += w; searchDelay += w; // increase delay log.warn(`Being rate limited by the API for ${w}ms! Increasing search delay...`); printDelayStats(); log.verb(`Cooling down for ${w * 2}ms before retrying...`); await wait(w * 2); return await recurse(); } else { return log.error(`Error searching messages, API responded with status ${resp.status}!\n`, await resp.json()); } } let regex; try { regex = new RegExp(pattern); } catch (e) { log.warn('Ignoring RegExp because pattern is malformed'); } const data = await resp.json(); const total = data.total_results; if (!grandTotal) grandTotal = total; const discoveredMessages = data.messages.map(convo => convo.find(message => message.hit === true)); const messagesToDelete = discoveredMessages.filter(msg => { return (msg.type === 0 || (msg.type >= 6 && msg.type <= 21) || (msg.pinned && includePinned)) && (!regex || msg.content.match(regex)); }); const skippedMessages = discoveredMessages.filter(msg => !messagesToDelete.find(m => m.id === msg.id)); const end = () => { log.success(`Ended at ${new Date().toLocaleString()}! Total time: ${msToHMS(Date.now() - start.getTime())}`); printDelayStats(); log.verb(`Rate Limited: ${throttledCount} times. Total time throttled: ${msToHMS(throttledTotalTime)}.`); log.debug(`Deleted ${delCount} messages, ${failCount} failed.\n`); }; const etr = msToHMS((searchDelay * Math.round(total / 25)) + ((deleteDelay + avgPing) * total)); log.info(`Total messages found: ${data.total_results}`, `(Messages in current page: ${data.messages.length}, To be deleted: ${messagesToDelete.length}, System: ${skippedMessages.length})`, `offset: ${offset}`); printDelayStats(); log.verb(`Estimated time remaining: ${etr}`); if (messagesToDelete.length > 0 || skippedMessages.length > 0) { if (++iterations < 1) { log.verb('Waiting for your confirmation...'); if (!await ask(`Do you want to delete ~${total} messages?\nEstimated time: ${etr}\n\n---- Preview ----\n` + messagesToDelete.map(m => `${m.author.username}#${m.author.discriminator}: ${m.attachments.length ? '[ATTACHMENTS]' : m.content}`).join('\n'))) return end(log.error('Aborted by you!')); log.verb('OK'); } for (let i = 0; i < messagesToDelete.length; i++) { const message = messagesToDelete[i]; if (stopHndl && stopHndl()) return end(log.error('Stopped by you!')); log.debug(`${((delCount + 1) / grandTotal * 100).toFixed(2)}% (${delCount + 1}/${grandTotal})`, `Deleting ID:${redact(message.id)} ${redact(message.author.username + '#' + message.author.discriminator)} (${redact(new Date(message.timestamp).toLocaleString())}): ${redact(message.content).replace(/\n/g, '↵')}`, message.attachments.length ? redact(JSON.stringify(message.attachments)) : ''); if (onProgress) onProgress(delCount + 1, grandTotal); let resp; try { const s = Date.now(); const API_DELETE_URL = `https://discord.com/api/v9/channels/${message.channel_id}/messages/${message.id}`; resp = await fetch(API_DELETE_URL, { headers, method: 'DELETE' }); lastPing = (Date.now() - s); avgPing = (avgPing * 0.9) + (lastPing * 0.1); delCount++; } catch (err) { log.error('Delete request throwed an error:', err); log.verb('Related object:', redact(JSON.stringify(message))); failCount++; } if (!resp.ok) { // deleting messages too fast if (resp.status === 429) { const w = (await resp.json()).retry_after * 1000; throttledCount++; throttledTotalTime += w; deleteDelay = w; // increase delay log.warn(`Being rate limited by the API for ${w}ms! Adjusted delete delay to ${deleteDelay}ms.`); printDelayStats(); log.verb(`Cooling down for ${w * 2}ms before retrying...`); await wait(w * 2); i--; // retry } else { log.error(`Error deleting message, API responded with status ${resp.status}!`, await resp.json()); log.verb('Related object:', redact(JSON.stringify(message))); failCount++; } } await wait(deleteDelay); } if (skippedMessages.length > 0) { grandTotal -= skippedMessages.length; offset += skippedMessages.length; log.verb(`Found ${skippedMessages.length} system messages! Decreasing grandTotal to ${grandTotal} and increasing offset to ${offset}.`); } log.verb(`Searching next messages in ${searchDelay}ms...`, (offset ? `(offset: ${offset})` : '')); await wait(searchDelay); if (stopHndl && stopHndl()) return end(log.error('Stopped by you!')); return await recurse(); } else { if (total - offset > 0) log.warn('Ended because API returned an empty page.'); return end(); } } log.success(`\nStarted at ${start.toLocaleString()}`); log.debug(`authorId="${redact(authorId)}" guildId="${redact(guildId)}" channelId="${redact(channelId)}" minId="${redact(minId)}" maxId="${redact(maxId)}" hasLink=${!!hasLink} hasFile=${!!hasFile}`); if (onProgress) onProgress(null, 1); return await recurse(); } class Drag { /** * Make an element draggable/resizable * @param {Element} targetElm The element that will be dragged/resized * @param {Element} handleElm The element that will listen to events (handdle/grabber) * @param {object} [options] Options * @param {string} [options.mode="move"] Define the type of operation (move/resize) * @param {number} [options.minWidth=200] Minimum width allowed to resize * @param {number} [options.maxWidth=Infinity] Maximum width allowed to resize * @param {number} [options.minHeight=100] Maximum height allowed to resize * @param {number} [options.maxHeight=Infinity] Maximum height allowed to resize * @param {string} [options.draggingClass="drag"] Class added to targetElm while being dragged * @param {boolean} [options.useMouseEvents=true] Use mouse events * @param {boolean} [options.useTouchEvents=true] Use touch events * * @author Victor N. wwww.vitim.us */ constructor(targetElm, handleElm, options) { this.options = Object.assign({ mode: 'move', minWidth: 200, maxWidth: Infinity, minHeight: 100, maxHeight: Infinity, xAxis: true, yAxis: true, draggingClass: 'drag', useMouseEvents: true, useTouchEvents: true, }, options); // Public properties this.minWidth = this.options.minWidth; this.maxWidth = this.options.maxWidth; this.minHeight = this.options.minHeight; this.maxHeight = this.options.maxHeight; this.xAxis = this.options.xAxis; this.yAxis = this.options.yAxis; this.draggingClass = this.options.draggingClass; /** @private */ this._targetElm = targetElm; /** @private */ this._handleElm = handleElm; const moveOp = (x, y) => { let l = x - offLeft; if (x - offLeft < 0) l = 0; //offscreen <- else if (x - offRight > vw) l = vw - this._targetElm.clientWidth; //offscreen -> let t = y - offTop; if (y - offTop < 0) t = 0; //offscreen /\ else if (y - offBottom > vh) t = vh - this._targetElm.clientHeight; //offscreen \/ if(this.xAxis) this._targetElm.style.left = `${l}px`; if(this.yAxis) this._targetElm.style.top = `${t}px`; // NOTE: profilling on chrome translate wasn't faster than top/left as expected. And it also permanently creates a new layer, increasing vram usage. // this._targetElm.style.transform = `translate(${l}px, ${t}px)`; }; const resizeOp = (x, y) => { let w = x - this._targetElm.offsetLeft - offRight; if (x - offRight > vw) w = Math.min(vw - this._targetElm.offsetLeft, this.maxWidth); //offscreen -> else if (x - offRight - this._targetElm.offsetLeft > this.maxWidth) w = this.maxWidth; //max width else if (x - offRight - this._targetElm.offsetLeft < this.minWidth) w = this.minWidth; //min width let h = y - this._targetElm.offsetTop - offBottom; if (y - offBottom > vh) h = Math.min(vh - this._targetElm.offsetTop, this.maxHeight); //offscreen \/ else if (y - offBottom - this._targetElm.offsetTop > this.maxHeight) h = this.maxHeight; //max height else if (y - offBottom - this._targetElm.offsetTop < this.minHeight) h = this.minHeight; //min height if(this.xAxis) this._targetElm.style.width = `${w}px`; if(this.yAxis) this._targetElm.style.height = `${h}px`; }; // define which operation is performed on drag const operation = this.options.mode === 'move' ? moveOp : resizeOp; // offset from the initial click to the target boundaries let offTop, offLeft, offBottom, offRight; let vw = window.innerWidth; let vh = window.innerHeight; function dragStartHandler(e) { const touch = e.type === 'touchstart'; if ((e.buttons === 1 || e.which === 1) || touch) { e.preventDefault(); const x = touch ? e.touches[0].clientX : e.clientX; const y = touch ? e.touches[0].clientY : e.clientY; const targetOffset = this._targetElm.getBoundingClientRect(); //offset from the click to the top-left corner of the target (drag) offTop = y - targetOffset.y; offLeft = x - targetOffset.x; //offset from the click to the bottom-right corner of the target (resize) offBottom = y - (targetOffset.y + targetOffset.height); offRight = x - (targetOffset.x + targetOffset.width); vw = window.innerWidth; vh = window.innerHeight; if (this.options.useMouseEvents) { document.addEventListener('mousemove', this._dragMoveHandler); document.addEventListener('mouseup', this._dragEndHandler); } if (this.options.useTouchEvents) { document.addEventListener('touchmove', this._dragMoveHandler, { passive: false, }); document.addEventListener('touchend', this._dragEndHandler); } this._targetElm.classList.add(this.draggingClass); } } function dragMoveHandler(e) { e.preventDefault(); let x, y; const touch = e.type === 'touchmove'; if (touch) { const t = e.touches[0]; x = t.clientX; y = t.clientY; } else { //mouse // If the button is not down, dispatch a "fake" mouse up event, to stop listening to mousemove // This happens when the mouseup is not captured (outside the browser) if ((e.buttons || e.which) !== 1) { this._dragEndHandler(); return; } x = e.clientX; y = e.clientY; } operation(x, y); } function dragEndHandler(e) { if (this.options.useMouseEvents) { document.removeEventListener('mousemove', this._dragMoveHandler); document.removeEventListener('mouseup', this._dragEndHandler); } if (this.options.useTouchEvents) { document.removeEventListener('touchmove', this._dragMoveHandler); document.removeEventListener('touchend', this._dragEndHandler); } this._targetElm.classList.remove(this.draggingClass); } // We need to bind the handlers to this instance and expose them to methods enable and destroy /** @private */ this._dragStartHandler = dragStartHandler.bind(this); /** @private */ this._dragMoveHandler = dragMoveHandler.bind(this); /** @private */ this._dragEndHandler = dragEndHandler.bind(this); this.enable(); } /** * Turn on the drag and drop of the instancea * @memberOf Drag */ enable() { // this.destroy(); // prevent events from getting binded twice if (this.options.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler); if (this.options.useTouchEvents) this._handleElm.addEventListener('touchstart', this._dragStartHandler, { passive: false }); } /** * Teardown all events bound to the document and elements * You can resurrect this instance by calling enable() * @memberOf Drag */ destroy() { this._targetElm.classList.remove(this.draggingClass); if (this.options.useMouseEvents) { this._handleElm.removeEventListener('mousedown', this._dragStartHandler); document.removeEventListener('mousemove', this._dragMoveHandler); document.removeEventListener('mouseup', this._dragEndHandler); } if (this.options.useTouchEvents) { this._handleElm.removeEventListener('touchstart', this._dragStartHandler); document.removeEventListener('touchmove', this._dragMoveHandler); document.removeEventListener('touchend', this._dragEndHandler); } } } function createElm(html) { const temp = document.createElement('div'); temp.innerHTML = html; return temp.removeChild(temp.firstElementChild); } function insertCss(css) { const style = document.createElement('style'); style.appendChild(document.createTextNode(css)); document.head.appendChild(style); return style; } const messagePickerCss = ` body.undiscord-pick-message [data-list-id="chat-messages"] { background-color: var(--background-secondary-alt); box-shadow: inset 0 0 0px 2px var(--button-outline-brand-border); } body.undiscord-pick-message [id^="message-content-"]:hover { cursor: pointer; cursor: cell; background: var(--background-message-automod-hover); } body.undiscord-pick-message [id^="message-content-"]:hover::after { position: absolute; top: calc(50% - 11px); left: 4px; z-index: 1; width: 65px; height: 22px; line-height: 22px; font-family: var(--font-display); background-color: var(--button-secondary-background); color: var(--header-secondary); font-size: 12px; font-weight: 500; text-transform: uppercase; text-align: center; border-radius: 3px; content: 'This 👉'; } body.undiscord-pick-message.before [id^="message-content-"]:hover::after { content: 'Before 👆'; } body.undiscord-pick-message.after [id^="message-content-"]:hover::after { content: 'After 👇'; } `; const messagePicker = { init() { insertCss(messagePickerCss); }, grab(auxiliary) { return new Promise((resolve, reject) => { document.body.classList.add('undiscord-pick-message'); if (auxiliary) document.body.classList.add(auxiliary); function clickHandler(e) { const message = e.target.closest('[id^="message-content-"]'); if (message) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (auxiliary) document.body.classList.remove(auxiliary); document.body.classList.remove('undiscord-pick-message'); document.removeEventListener('click', clickHandler); try { resolve(message.id.match(/message-content-(\d+)/)[1]); } catch (e) { resolve(null); } } } document.addEventListener('click', clickHandler); }); } }; var messagePicker$1 = messagePicker; window.messagePicker = messagePicker; function getToken() { window.dispatchEvent(new Event('beforeunload')); const LS = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage; return JSON.parse(LS.token); } function getAuthorId() { const LS = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage; return JSON.parse(LS.user_id_cache); } function getGuildId() { const m = location.href.match(/channels\/([\w@]+)\/(\d+)/); if (m) return m[1]; else alert('Could not the Guild ID!\nPlease make sure you are on a Server or DM.'); } function getChannelId() { const m = location.href.match(/channels\/([\w@]+)\/(\d+)/); if (m) return m[2]; else alert('Could not the Channel ID!\nPlease make sure you are on a Channel or DM.'); } // ------------------------- User interface ------------------------------ // const HOME = 'https://github.com/victornpb/undiscord'; const WIKI = 'https://github.com/victornpb/undiscord/wiki'; const $ = s => undiscordWindow.querySelector(s); let undiscordWindow; let undiscordBtn; function initUI() { insertCss(discordStyles); insertCss(undiscordStyles); function replaceInterpolations(str, obj, removeMissing = false) { return str.replace(/\{\{([\w_]+)\}\}/g, (m, key) => obj[key] || (removeMissing ? '' : m)); } const templateVariables = { VERSION: version, HOME, WIKI, }; // create undiscord window const undiscordUI = replaceInterpolations(undiscordTemplate, templateVariables); undiscordWindow = createElm(undiscordUI); document.body.appendChild(undiscordWindow); new Drag(undiscordWindow, $('.header'), { mode: 'move' }); new Drag(undiscordWindow, $('.footer'), { mode: 'resize' }); // create undiscord button undiscordBtn = createElm(buttonHtml); undiscordBtn.onclick = toggleWindow; function mountBtn() { const toolbar = document.querySelector('#app-mount [class^=toolbar]'); if (toolbar) toolbar.appendChild(undiscordBtn); } mountBtn(); // watch for changes and re-mount button if necessary const discordElm = document.querySelector('#app-mount'); let observerThrottle = null; const observer = new MutationObserver((_mutationsList, _observer) => { if (observerThrottle) return; observerThrottle = setTimeout(() => { observerThrottle = null; if (!discordElm.contains(undiscordBtn)) mountBtn(); // re-mount the button to the toolbar }, 3000); }); observer.observe(discordElm, { attributes: false, childList: true, subtree: true }); function toggleWindow() { if (undiscordWindow.style.display !== 'none') { undiscordWindow.style.display = 'none'; undiscordBtn.style.color = 'var(--interactive-normal)'; } else { undiscordWindow.style.display = ''; undiscordBtn.style.color = 'var(--interactive-active)'; } } messagePicker$1.init(); // register event listeners $('#hide').onclick = toggleWindow; $('button#start').onclick = start; $('button#stop').onclick = stop; $('button#clear').onclick = () => $('#logArea').innerHTML = ''; $('button#getAuthor').onclick = () => $('input#authorId').value = getAuthorId(); $('button#getGuild').onclick = () => { const guildId = $('input#guildId').value = getGuildId(); if (guildId === '@me') $('input#channelId').value = getChannelId(); }; $('button#getChannel').onclick = () => { $('input#channelId').value = getChannelId(); $('input#guildId').value = getGuildId(); }; $('#redact').onchange = () => { const b = undiscordWindow.classList.toggle('redact'); if (b) alert('This mode will attempt to hide personal information, so you can screen share / take screenshots.\nAlways double check you are not sharing sensitive information!'); }; $('#pickMessageAfter').onclick = async () => { // alert('Select a message on the chat.\nThe message below it will be deleted.'); const id = await messagePicker$1.grab('after'); if (id) $('input#minId').value = id; }; $('#pickMessageBefore').onclick = async () => { // alert('Select a message on the chat.\nThe message above it will be deleted.'); const id = await messagePicker$1.grab('before'); if (id) $('input#maxId').value = id; }; // const fileSelection = $('input#importJson'); // fileSelection.onchange = () => { // const files = fileSelection.files; // const channelIdField = $('input#channelId'); // if (files.length > 0) { // const file = files[0]; // file.text().then(text => { // let json = JSON.parse(text); // let channels = Object.keys(json); // channelIdField.value = channels.join(','); // }); // } // }; } let _stopFlag = false; const stopHndl = () => _stopFlag; async function start() { console.log('start'); _stopFlag = false; // general const authToken = getToken(); const authorId = $('input#authorId').value.trim(); const guildId = $('input#guildId').value.trim(); const channelIds = $('input#channelId').value.trim().split(/\s*,\s*/); const includeNsfw = $('input#includeNsfw').checked; // filter const content = $('input#search').value.trim(); const hasLink = $('input#hasLink').checked; const hasFile = $('input#hasFile').checked; const includePinned = $('input#includePinned').checked; const pattern = $('input#pattern').value; // message interval const minId = $('input#minId').value.trim(); const maxId = $('input#maxId').value.trim(); // date range const minDate = $('input#minDate').value.trim(); const maxDate = $('input#maxDate').value.trim(); //advanced const searchDelay = parseInt($('input#searchDelay').value.trim()); const deleteDelay = parseInt($('input#deleteDelay').value.trim()); // progress handler const progress = $('#progressBar'); const progress2 = undiscordBtn.querySelector('progress'); const percent = $('#progressPercent'); const onProg = (value, max) => { if (value && max && value > max) max = value; progress.setAttribute('max', max); progress2.setAttribute('max', max); progress.value = value; progress2.value = value; progress.style.display = max ? '' : 'none'; progress2.style.display = max ? '' : 'none'; percent.style.display = value && max ? '' : 'none'; percent.innerHTML = value >= 0 && max ? Math.round(value / max * 100) + '%' : ''; // indeterminate progress bar if (value === -1) { progress.removeAttribute('value'); progress2.removeAttribute('value'); percent.innerHTML = '...'; } }; let logArea = $('#logArea'); let autoScroll = $('#autoScroll'); const logger = (type = '', args) => { const style = { '': '', info: 'color:#00b0f4;', verb: 'color:#72767d;', warn: 'color:#faa61a;', error: 'color:#f04747;', success: 'color:#43b581;' }[type]; logArea.insertAdjacentHTML('beforeend', `
${Array.from(args).map(o => typeof o === 'object' ? JSON.stringify(o, o instanceof Error && Object.getOwnPropertyNames(o)) : o).join('\t')}
`); if (autoScroll.checked) logArea.querySelector('div:last-child').scrollIntoView(false); }; logArea.innerHTML = ''; // validate input if (!authToken) return logger('error', ['Could not detect the authorization token!']) || logger('info', ['Please make sure Undiscord is up to date']); else if (!guildId) return logger('error', ['You must provide a Server ID!']); for (let i = 0; i < channelIds.length; i++) { $('#start').disabled = true; $('#stop').disabled = false; await deleteMessages(authToken, authorId, guildId, channelIds[i], minId || minDate, maxId || maxDate, content, hasLink, hasFile, includeNsfw, includePinned, pattern, searchDelay, deleteDelay, logger, stopHndl, onProg); stop(); // clear the running state } } function stop() { _stopFlag = true; $('#start').disabled = false; $('#stop').disabled = true; $('#progressBar').style.display = 'none'; $('#progressPercent').style.display = 'none'; undiscordBtn.querySelector('progress').style.display = 'none'; } initUI(); // ---- END Undiscord ---- })();