diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 6dc22278..ce36ca12 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,3 @@ -# These are supported funding model platforms - -custom: ['https://www.buymeacoffee.com/vitim'] -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username +github: [victornpb, abbydiode] ko_fi: victornpb -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username +custom: https://www.buymeacoffee.com/vitim \ No newline at end of file diff --git a/LICENSE b/LICENSE index 22ad2483..26af2e71 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Victornpb +Copyright (c) 2019-2022 victornpb, abbydiode, and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 00000000..9ee46a37 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +![Undiscord Logo](https://user-images.githubusercontent.com/16174954/166003147-6014a83e-e2b8-45b4-8961-f878d4b466bf.png) + +# Wipe your Discord messages fast and easy + +> ⚠️ **Any tool that automates actions on user accounts, including this one, could result in account termination.** [Use at your own risk!](https://support.discordapp.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-) + +## Instructions + +1. Install a browser extension for managing user scripts. + + - Chrome: [Violentmonkey](https://chrome.google.com/webstore/detail/violent-monkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) + - Firefox: [Greasemonkey](https://addons.mozilla.org/firefox/addon/greasemonkey/), [Tampermonkey](https://addons.mozilla.org/firefox/addon/tampermonkey/), or [Violentmonkey](https://addons.mozilla.org/firefox/addon/violentmonkey/) + - Microsoft Edge: [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) or [Violentmonkey](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) + - Opera: [Tampermonkey](https://addons.opera.com/extensions/details/tampermonkey-beta/) or [Violentmonkey](https://addons.opera.com/extensions/details/violent-monkey/) + +2. Install [Undiscord](https://github.com/abbydiode/UndiscordPlus/raw/main/Undiscord.user.js) from this repository. + +3. Open [Discord](https://discord.com/channels/@me) in your __browser__ and go to the channel or private conversation that you would like to be wiped. + +4. Click the 🗑️ button that was added in the top right corner. + +5. Click on the Get buttons near **Authorization**, **Author** and **Channel**. + + - *Optionally you can get [Authorization Tokens](../../wiki/Authorization-Tokens), [User IDs](../../wiki/User-IDs), [Guild and Channel IDs](../../wiki/Guild-and-Channel-IDs), and [Message IDs](../../wiki/Message-IDs) manually.* + +6. Click the Start button. + +![Screenshot](https://user-images.githubusercontent.com/3372598/86538983-b60c7980-becf-11ea-8cad-1a33950e77fc.gif) + +## Information + +If you find an issue please [open an issue here](https://github.com/abbydiode/UndiscordPlus/issues). + +If you need help check out the [wiki](https://github.com/abbydiode/UndiscordPlus/wiki) for helpful articles, or feel free to [start a discussion](https://github.com/abbydiode/UndiscordPlus/discussions). + +This project is a fork of [victornpb's deleteDiscordMessages](https://github.com/victornpb/deleteDiscordMessages). + +## Disclaimer + +**DO NOT SHARE YOUR AUTHORIZATION TOKEN!** + +Sharing your authorization token on the internet will give full access to your Discord account! [There are bots gathering credentials all over the internet](https://github.com/rndinfosecguy/Scavenger). +If you post your token by accident, log out from Discord on the **same browser** where you got the token immediately. +Changing your password will make sure that you get logged out of every device. It is advised that you turn on [2FA](https://support.discord.com/hc/en-us/articles/219576828-Setting-up-Two-Factor-Authentication) afterwards. + +If you are unsure, do not share screenshots or logs on the internet. diff --git a/Undiscord.user.js b/Undiscord.user.js new file mode 100644 index 00000000..6e5c3dc8 --- /dev/null +++ b/Undiscord.user.js @@ -0,0 +1,559 @@ +// ==UserScript== +// @name Undiscord +// @description Wipe your Discord messages fast and easy +// @author https://github.com/abbydiode +// @namespace https://github.com/abbydiode/UndiscordPlus +// @version 5.2.6 +// @match https://*.discord.com/* +// @downloadURL https://raw.githubusercontent.com/abbydiode/undiscordPlus/main/Undiscord.user.js +// @homepageURL https://github.com/abbydiode/UndiscordPlus +// @supportURL https://github.com/abbydiode/UndiscordPlus/issues +// @license MIT +// ==/UserScript== + +/** + * 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 {boolean} ascendingOrder Search in ascending order + * @param {function(string, Array)} extLogger Function for logging + * @param {function} stopHndl stopHndl used for stopping + * @author abbydiode + * @see https://github.com/abbydiode/UndiscordPlus + */ +async function deleteMessages(authToken, authorId, guildId, channelId, minId, maxId, content, hasLink, hasFile, includeNSFW, ascendingOrder, includePinned, searchDelay, deleteDelay, extLogger, stopHndl, onProgress) { + const start = new Date(); + let deleteCount = 0; + let failCount = 0; + let averagePing; + 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: ${averagePing | 0}ms`); + const toSnowflake = (date) => /:/.test(date) ? ((new Date(date).getTime() - 1420070400000) * Math.pow(2, 22)) : date; + + const log = { + debug() { extLogger ? extLogger('debug', arguments) : console.debug.apply(console, arguments); }, + info() { extLogger ? extLogger('info', arguments) : console.info.apply(console, arguments); }, + verb() { extLogger ? extLogger('verb', arguments) : console.log.apply(console, arguments); }, + warn() { extLogger ? extLogger('warn', arguments) : console.warn.apply(console, arguments); }, + error() { extLogger ? extLogger('error', arguments) : console.error.apply(console, arguments); }, + success() { extLogger ? extLogger('success', arguments) : console.info.apply(console, arguments); }, + }; + + const headers = { + 'Authorization': authToken + }; + + const deleteHeaders = { + headers, + method: 'DELETE' + }; + + async function deleteReactions(messages, wait) { + async function deleteReaction(cid, mid, id) { + const API_DELETE_URL = `https://discord.com/api/v9/channels/${cid}/messages/${mid}/reactions/${encodeURI(id)}/%40me`; + fetch(API_DELETE_URL, deleteHeaders) + .catch(console.error); + } + + for (const msg of messages) { + for (const emoji of msg.reactions.filter(r => r.me).map(r => r.emoji)) { + const id = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; + deleteReaction(msg.channel_id, msg.id, id); + // Delay could be improved + await wait(2000); + } + } + } + + async function deletePins(messages, wait) { + async function deletePin(cid, mid) { + const API_DELETE_URL = `https://discord.com/api/v9/channels/${cid}/pins/${mid}`; + fetch(API_DELETE_URL, deleteHeaders) + .catch(console.error); + } + + for (const msg of messages) { + deletePin(msg.channel_id, msg.id); + // Delay could be improved + await wait(2000); + } + } + + let fetchPoint = maxId ? toSnowflake(maxId) : undefined; + let useSearch = true; + + // Add these to the GUI + const deleteFetchedPins = false; + const deleteFetchedReactions = false; + const tryFetch = false; + + async function recurse() { + let response; + try { + let API_SEARCH_URL; + if (useSearch) { + if (guildId === '@me') { + API_SEARCH_URL = `https://discord.com/api/v9/channels/${channelId}/messages/`; + } + else { + API_SEARCH_URL = `https://discord.com/api/v9/guilds/${guildId}/messages/`; + } + 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', ascendingOrder ? 'asc' : 'desc'], + ['offset', offset], + ['has', hasLink ? 'link' : undefined], + ['has', hasFile ? 'file' : undefined], + ['content', content || undefined], + ['include_nsfw', includeNSFW ? true : undefined], + ]); + } else { + // We could fetch up to 100 messages to reduce our API calls + // However the official client always uses limit 50 + API_SEARCH_URL = `https://discord.com/api/v9/channels/${channelId}/messages?before=${fetchPoint}&limit=50`; + } + const s = Date.now(); + response = await fetch(API_SEARCH_URL, { headers }); + lastPing = (Date.now() - s); + averagePing = averagePing > 0 ? (averagePing * 0.9) + (lastPing * 0.1) : lastPing; + } catch (err) { + return log.error('Search request threw an error:', err); + } + + // Not indexed yet + if (response.status === 202) { + const w = (await response.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 (!response.ok) { + // Searching messages too fast + if (response.status === 429) { + const delay = (await response.json()).retry_after * 1000; + throttledCount++; + throttledTotalTime += delay; + searchDelay += delay; + log.warn(`Being rate limited by the API for ${delay}ms! Increasing search delay...`); + printDelayStats(); + log.verb(`Cooling down for ${delay * 2}ms before retrying...`); + + await wait(delay * 2); + return await recurse(); + } else { + return log.error(`Error searching messages, API responded with status ${response.status}!\n`, await response.json()); + } + } + + const data = await response.json(); + let total; + let discoveredMessages; + + 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 ${deleteCount} messages, ${failCount} failed.\n`); + } + + + if (useSearch) { + total = data.total_results; + if (!grandTotal) grandTotal = total; + discoveredMessages = data.messages.map(convo => convo.find(message => message.hit===true)); + + if (discoveredMessages.length === 0) { + log.warn('Ended because API returned an empty page.'); + return end(); + } + + // Next search will use fetch + fetchPoint = discoveredMessages[discoveredMessages.length - 1].id; + } else { + function skipMessages(m) { + if (!authorId) return false; + return m.author.id !== authorId; + } + if (deleteFetchedPins) await deletePins(data.filter(m => skipMessages(m) && m.pinned, wait)); + if (deleteFetchedReactions) await deleteReactions(data.filter(m => skipMessages(m) && m.reactions && m.reactions.some(r => r.me)), wait); + + discoveredMessages = data.filter(m => !skipMessages(m)); + if (discoveredMessages.length === 0) { + log.info("Fetch found no deletable messages. Starting search."); + useSearch = true; + return await recurse(); + } + + // Find earliest, assume message ids are descending + fetchPoint = data[data.length - 1].id; + } + + const messagesToDelete = discoveredMessages.filter(msg => { + return msg.type === 0 || (msg.type >= 6 && msg.type <= 21) || (msg.pinned && includePinned); + }); + const skippedMessages = discoveredMessages.filter(msg=>!messagesToDelete.find(m=> m.id===msg.id)); + const estimatedTimeRemaining = msToHMS((searchDelay * Math.round(total / 25)) + ((deleteDelay + averagePing) * total)); + + if (useSearch) { + 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}`); + log.verb(`Estimated time remaining: ${estimatedTimeRemaining}`) + useSearch = tryFetch; // Use fetch next time + } + printDelayStats(); + + + if (messagesToDelete.length > 0) { + + if (++iterations < 1) { + log.verb(`Waiting for your confirmation...`); + if (!await ask(`Do you want to delete ~${total} messages?\nEstimated time: ${estimatedTimeRemaining}\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() === false) return end(log.error('Stopped by you!')); + + log.debug(`${((deleteCount + 1) / grandTotal * 100).toFixed(2)}% (${deleteCount + 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(deleteCount + 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, deleteHeaders); + lastPing = (Date.now() - s); + averagePing = (averagePing * 0.9) + (lastPing * 0.1); + deleteCount++; + } catch (err) { + log.error('Delete request threw 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; + 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--; + } 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() === false) 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(); +} + +let popup; +let button; +let stop; + +function initializeUI() { + + const insertCSS = (css) => { + const style = document.createElement('style'); + style.appendChild(document.createTextNode(css)); + document.head.appendChild(style); + return style; + } + + const createElement = (html) => { + const temp = document.createElement('div'); + temp.innerHTML = html; + return temp.removeChild(temp.firstElementChild); + } + + /*css*/ + insertCSS(` + #undicord-btn{position: relative; height: 24px;width: auto;-webkit-box-flex: 0;-ms-flex: 0 0 auto;flex: 0 0 auto;margin: 0 8px;cursor:pointer; color: var(--interactive-normal);} + #undiscord{position:fixed;top:100px;right:10px;bottom:10px;width:780px;z-index:99;color:var(--text-normal);background-color:var(--background-secondary);box-shadow:var(--elevation-stroke),var(--elevation-high);border-radius:4px;display:flex;flex-direction:column} + #undiscord a{color:#00b0f4} + #undiscord.redact .priv{display:none!important} + #undiscord:not(.redact) .mask{display:none!important} + #undiscord.redact [priv]{-webkit-text-security:disc!important} + #undiscord .toolbar span{margin-right:8px} + #undiscord button,#undiscord .btn{color:#fff;background:#7289da;border:0;border-radius:4px;font-size:14px} + #undiscord button:disabled{display:none} + #undiscord input[type="text"],#undiscord input[type="search"],#undiscord input[type="password"],#undiscord input[type="datetime-local"],#undiscord input[type="number"]{background-color:#202225;color:#b9bbbe;border-radius:4px;border:0;padding:0 .5em;height:24px;width:144px;margin:2px} + #undiscord input#file{display:none} + #undiscord hr{border-color:rgba(255,255,255,0.1)} + #undiscord .header{padding:12px 16px;background-color:var(--background-tertiary);color:var(--text-muted)} + #undiscord .form{padding:8px;background:var(--background-secondary);box-shadow:0 1px 0 rgba(0,0,0,.2),0 1.5px 0 rgba(0,0,0,.05),0 2px 0 rgba(0,0,0,.05)} + #undiscord .logarea{overflow:auto;font-size:.75rem;font-family:Consolas,Liberation Mono,Menlo,Courier,monospace;flex-grow:1;padding:10px} + `); + + /*html*/ + popup = createElement(` + + `); + + document.body.appendChild(popup); + + /*html*/ + button = createElement(` +
+ + + + +
+
+ `); + + button.onclick = function togglePopover() { + if (popup.style.display !== 'none') { + popup.style.display = 'none'; + button.style.color = 'var(--interactive-normal)'; + } + else { + popup.style.display = ''; + button.style.color = '#f04747'; + } + }; + + function mountButton() { + const toolbar = document.querySelector('[class^=toolbar]'); + if (toolbar) toolbar.appendChild(button); + } + + const observer = new MutationObserver(function (_mutationsList, _observer) { + if (!document.body.contains(button)) mountButton(); + }); + observer.observe(document.body, { attributes: false, childList: true, subtree: true }); + + mountButton(); + + const $ = s => popup.querySelector(s); + const logArea = $('pre'); + const startButton = $('button#start'); + const stopButton = $('button#stop'); + const autoScroll = $('#autoScroll'); + + startButton.onclick = async _ => { + const authToken = $('input#authToken').value.trim(); + const authorId = $('input#authorId').value.trim(); + const guildId = $('input#guildId').value.trim(); + let channelIds = $('input#channelId').value.trim().split(/\s*,\s*/); + const minId = $('input#minId').value.trim(); + const maxId = $('input#maxId').value.trim(); + const minDate = $('input#minDate').value.trim(); + const maxDate = $('input#maxDate').value.trim(); + const content = $('input#content').value.trim(); + const hasLink = $('input#hasLink').checked; + const hasFile = $('input#hasFile').checked; + const includeNsfw = $('input#includeNsfw').checked; + const includePinned = $('input#includePinned').checked; + const ascendingOrder = $('input#ascendingOrder').checked; + const searchDelay = parseInt($('input#searchDelay').value.trim()); + const deleteDelay = parseInt($('input#deleteDelay').value.trim()); + const progress = $('#progress'); + const progress2 = button.querySelector('progress'); + const percent = $('.percent'); + + const fileSelection = $("input#file"); + fileSelection.addEventListener("change", () => { + 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(","); + }); + } + }, false); + + const stopHandle = () => !(stop === true); + + const onProgress = (value, max) => { + if (value && max && value > max) max = value; + progress.setAttribute('max', max); + progress.value = value; + progress.style.display = max ? '' : 'none'; + progress2.setAttribute('max', max); + progress2.value = value; + progress2.style.display = max ? '' : 'none'; + percent.innerHTML = value && max ? Math.round(value / max * 100) + '%' : ''; + }; + + + async function listAllDMS(authToken) { + const headers = { + 'Authorization': authToken + }; + try { + const resp = await fetch(`https://discord.com/api/v9/users/%40me/channels`, { headers }); + const ids = (await resp.json()).map(c => c.id); + return ids; + } catch (e) { + return []; + } + } + + if (guildId === '@me' && channelIds.filter(c => c).length == 0) { + channelIds = await listAllDMS(authToken); + } + + stop = stopButton.disabled = !(startButton.disabled = true); + for (let i = 0; i < channelIds.length; i++) { + await deleteMessages(authToken, authorId, guildId, channelIds[i], minId || minDate, maxId || maxDate, content, hasLink, hasFile, includeNsfw, ascendingOrder, includePinned, searchDelay, deleteDelay, logger, stopHandle, onProgress); + stop = stopButton.disabled = !(startButton.disabled = false); + } + }; + stopButton.onclick = _ => stop = stopButton.disabled = !(startButton.disabled = false); + $('button#clear').onclick = _ => { logArea.innerHTML = ''; }; + $('button#getToken').onclick = _ => { + window.dispatchEvent(new Event('beforeunload')); + const ls = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage; + $('input#authToken').value = JSON.parse(localStorage.token); + }; + $('button#getAuthor').onclick = _ => { + $('input#authorId').value = JSON.parse(localStorage.user_id_cache); + }; + $('button#getGuildAndChannel').onclick = _ => { + const m = location.href.match(/channels\/([\w@]+)\/(\d+)/); + $('input#guildId').value = m[1]; + $('input#channelId').value = m[2]; + }; + $('#redact').onchange = _ => { + popup.classList.toggle('redact') && + window.alert('This will attempt to hide personal information, but make sure to double check before sharing screenshots.'); + }; + + 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); + }; + + window.localStorage = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage; +} + +initializeUI(); diff --git a/deleteDiscordMessages.user.js b/deleteDiscordMessages.user.js deleted file mode 100644 index 4be84526..00000000 --- a/deleteDiscordMessages.user.js +++ /dev/null @@ -1,1414 +0,0 @@ -// ==UserScript== -// @name Undiscord -// @description Delete all messages in a Discord channel or DM (Bulk deletion) -// @version 5.0.0 -// @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 -// @downloadURL https://raw.githubusercontent.com/victornpb/deleteDiscordMessages/master/deleteDiscordMessages.user.js -// @contributionURL https://www.buymeacoffee.com/vitim -// @grant none -// ==/UserScript== -(function () { - 'use strict'; - - var version = "5.0.0"; - - 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: 610px; - min-width: 610px; - max-width: 100%; - height: 448px; - min-height: 448px; - max-height: 100%; - 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; - 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; - 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() === false) 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; - 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() === false) 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; - const stopHndl = () => !(_stopFlag === true); - - async function start() { - console.log('start'); - - // 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); - progress.value = value; - progress.style.display = max ? '' : 'none'; - progress2.setAttribute('max', max); - progress2.value = value; - progress2.style.display = max ? '' : 'none'; - percent.innerHTML = value && max ? Math.round(value / max * 100) + '%' : ''; - if (value === -1) progress.removeAttribute('value'); - if (value === -1) progress2.removeAttribute('value'); - }; - - 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 (!authorId) return logger('error', ['You must provide an Author ID!']); - else if (!guildId) return logger('error', ['You must provide a Server ID!']); - - for (let i = 0; i < channelIds.length; i++) { - $('#start').style.display = 'none'; - $('#stop').style.display = 'block'; - 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').style.display = 'block'; - $('#stop').style.display = 'none'; - - $('#progressBar').style.display = 'none'; - undiscordBtn.querySelector('progress').style.display = 'none'; - } - - initUI(); - - - // ---- END Undiscord ---- - -})(); diff --git a/help/authToken.md b/help/authToken.md deleted file mode 100644 index 94ba9fcd..00000000 --- a/help/authToken.md +++ /dev/null @@ -1,22 +0,0 @@ -# authToken - - - -1. On the DevTools, Open the "Network" tab -3. Click on "XHR" -4. Type `api/v9` on the filter box. -5. Click on any request in the list, and then click on "Headers" tab in the side panel. - You're looking for something like this **authorization:** `MTX5MzQ1MjAyMjU0NjA2MzM2.ROFLMAO.UvqZqBMXLpDuOY3Z456J3JRIfbk`. - - ----- - -# DO NOT SHARE YOUR `authToken`! - -> Sharing your authToken on the internet will give full access to your account! [There are bots gathering credentials all over the internet](https://github.com/rndinfosecguy/Scavenger). -If you post your token by accident, LOGOUT from discord on that **same browser** you got that token imediately. -Changing your password will make sure that you get logged out of every device. I advice that you turn on [2FA](https://support.discord.com/hc/en-us/articles/219576828-Setting-up-Two-Factor-Authentication) afterwards. - -If you are unsure do not share screenshots, or copy paste logs on the internet. - ----- diff --git a/help/authorId.md b/help/authorId.md deleted file mode 100644 index 55b581ca..00000000 --- a/help/authorId.md +++ /dev/null @@ -1,18 +0,0 @@ -# authorId - -## The easy way - - - Just click **Me** button next to "Author" in the popup window, - it should auto detect your ID. - - -## The manual way -- Right click your **avatar** in a message you sent in the chat, [Copy ID](./developerMode.md) - - NOT THE MESSAGE THE AVATAR. - (You cannot delete the other's person messages a in DM channel, you will get Error 403) - - ------ - -> If the `Copy ID` doesn't show up, you need to enable [Developer mode](./developerMode.md) first. diff --git a/help/channelId.md b/help/channelId.md deleted file mode 100644 index 11c50149..00000000 --- a/help/channelId.md +++ /dev/null @@ -1,45 +0,0 @@ -# guildId / channelId - -## The easy way - -### DMs or a Channel -1. Go to the desired Channel or DM conversation on discord -2. Click the ![Get](https://user-images.githubusercontent.com/3372598/72776133-cf568d80-3bef-11ea-8ee7-4f1b9da9670d.png) button - -### Entire Server -1. Go to any Channel in the Server you want to delete -2. Click the ![Get](https://user-images.githubusercontent.com/3372598/72776133-cf568d80-3bef-11ea-8ee7-4f1b9da9670d.png) button -3. Leave the *Channel ID* box empty - ![image](https://user-images.githubusercontent.com/3372598/72776409-dcc04780-3bf0-11ea-91da-e722d6f2f064.png) - ----- - -## The manual way - -### For public channels: -- Right click a channel, [Copy ID](./developerMode.md) - - - -### For a DM/Direct messages: - -- copy the number after /@me/ in the URL) - - ---- - -You can target multiple channels in sequence by separating them with a comma. - ---- - -## Deleting all messages by using the "Request a Copy of your Data" option - -To delete all message from every (user) channel do following: -1. Go to "User Settings -> Privacy and Safety" and click on "Request all my Data." -2. You should receive an email within the next 30 days -3. Click on the "Import JSON" button the right JSON file is called "index.json" and is located in the messages folder (messages/index.json). -4. The channel IDs will be imported separated by a comma. - ------ - -> If the `Copy ID` doesn't show up, you need to enable [Developer mode](./developerMode.md) first. diff --git a/help/copyPaste.md b/help/copyPaste.md deleted file mode 100644 index 08d9c3dc..00000000 --- a/help/copyPaste.md +++ /dev/null @@ -1,29 +0,0 @@ -# Delete all messages in a Discord channel or DM - - -**TLDR:** Watch this [40s video instructions](https://imgur.com/a/vYmDNSZ) - -1. Select and Copy this script: [deleteDiscordMessages.js](https://raw.githubusercontent.com/victornpb/deleteDiscordMessages/master/deleteDiscordMessages.js) - -2. Open [Discord](https://discord.com/channels/@me) in a __browser__ (like Chrome, Safari or Firefox) -and go to a #Channel or a DM conversation - -3. Open DevTools pressing: - - Chrome (Windows, Linux, Chrome OS): - F12 or Control+Shift+J - - Firefox (Windows, Linux, Chrome OS): - F12 or Control+Shift+K - - Chrome (Mac): - Command+Option+J - - Safari (Mac): Command+Option+C - -4. Paste (Ctrl+V) the script in the "Console" tab, then press ENTER, a popup window will open; - -5. Click on the blue buttons near **Authorization**, **Author** and **Channel**. - *(Optional: getting [authToken](./help/authToken.md), [authorId](./help/authorId.md), [channelId](./help/channelId.md) and [messageId](./help/messageId.md) manually)* - -6. Click the **START** button. - - -![Screenshot](https://user-images.githubusercontent.com/3372598/85232240-e6362180-b3d3-11ea-9e28-f675d62e29e9.gif) - diff --git a/help/delay.md b/help/delay.md deleted file mode 100644 index 73331805..00000000 --- a/help/delay.md +++ /dev/null @@ -1,3 +0,0 @@ -# Search and Delete Delay - -This setting controls the initial delay for for searching messages. Sometimes the Discord API will quickly rate limit you, in this case setting the delays to something higher might help. This is especially true for the **Delete Delay**. diff --git a/help/developerMode.md b/help/developerMode.md deleted file mode 100644 index e23de63c..00000000 --- a/help/developerMode.md +++ /dev/null @@ -1,7 +0,0 @@ -# Developer Mode - -If the `Copy ID` menu doesn't show up when right clicking: - - Enable developer mode in discord - Go to user **Settings** > **Appearance** in discord and enable **Developer mode**. - - \ No newline at end of file diff --git a/help/filters.md b/help/filters.md deleted file mode 100644 index 75f027fa..00000000 --- a/help/filters.md +++ /dev/null @@ -1,16 +0,0 @@ -# filters - -You can filter only messages that has links or contain files - - -## has link -- If you select this option, only messages with links are going to be deleted. - -## has file -- If you select this option, only messages with files are going to be deleted. - Images, videos are also files. - - ----- -This feature uses the Discord search to find messages: -https://support.discord.com/hc/en-us/articles/115000468588-Using-Search \ No newline at end of file diff --git a/help/messageId.md b/help/messageId.md deleted file mode 100644 index 63945bd2..00000000 --- a/help/messageId.md +++ /dev/null @@ -1,19 +0,0 @@ -# messageId -You can delete all messages after a specific message, before a specific message, or everything between two points. For that you need to provide a messageID: -- Right click a message, and click [Copy ID](./developerMode.md) -> If the `Copy ID` doesn't show up, you need to enable [Developer mode](./developerMode.md) first. - ----- - -## Delete everything after a message - - -## Delete everything before a message - - -## Delete everything between two messages - - -## Delete everything in a channel - -Just leave both `after` and `before` empty diff --git a/readme.md b/readme.md deleted file mode 100644 index ea50bde7..00000000 --- a/readme.md +++ /dev/null @@ -1,122 +0,0 @@ -# Undiscord - Delete all messages in a Discord channel or DM - -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/victornpb/undiscord?color=%235865f2&display_name=tag&label=Undiscord&style=flat-square)][greasyfork_url] -[![GitHub Release Date](https://img.shields.io/github/release-date/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/releases) -[![GitHub License](https://img.shields.io/github/license/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/blob/master/LICENSE) -[![CodeFactor](https://www.codefactor.io/repository/github/victornpb/undiscord/badge?style=flat-square)](https://www.codefactor.io/repository/github/victornpb/undiscord?style=flat-square) -[![GitHub Stars](https://img.shields.io/github/stars/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/stargazers) -[![GitHub Forks](https://img.shields.io/github/forks/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/network/members) -[![GitHub Discussions](https://img.shields.io/github/discussions/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/discussions) -[![GitHub open issues](https://img.shields.io/github/issues/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/issues?q=is%3Aopen+is%3Aissue) -[![GitHub closed issues](https://img.shields.io/github/issues-closed/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/issues?q=is%3Aissue+is%3Aclosed) -[![GitHub pull requests](https://img.shields.io/github/issues-pr/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/pulls) -[![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/victornpb/undiscord?style=flat-square)](https://github.com/victornpb/undiscord/pulls?q=is%3Apr+is%3Aclosed) - - -> ⚠️ **Any tool that automates actions on user accounts, including this one, could result in account termination.** (see [self-bots][self-bots]). -> Use at your own risk! ([discussion](https://github.com/victornpb/undiscord/discussions/273)). - -1. First you need a Browser Extension for managing UserScripts[[1]][userscrips_faq] (skip if you already have one): - * Chrome: [Violentmonkey][chrome_violentmonkey] or [Tampermonkey][chrome_tampermonkey] - * Firefox: [Greasemonkey][firefox_greasemonkey], [Tampermonkey][firefox_tampermonkey], or [Violentmonkey][firefox_violentmonkey] - * Opera: [Tampermonkey][opera_tampermonkey] or [Violentmonkey][opera_violentmonkey] - * Brave: [Violentmonkey][chrome_violentmonkey] or [Tampermonkey][chrome_tampermonkey] - * Edge: [Tampermonkey][edge_tampermonkey] - * Safari: ~[Tampermonkey][safari_tampermonkey]~ - -1. Install Undiscord: - [![][greasyfork_icon]][greasyfork_url] or [![][openuserjs_icon]][openuserjs_url] - -1. Open Discord in your __browser__ (Not the App) and go to the channel or direct message you would like to be wiped. - -1. Click the 🗑️ button that was added in the top right corner. - -1. Click on the get buttons near **Authorization**, **Author** and **Guild/Channel**. - -1. Click the Start button to begin wipping! - -![Screenshot](https://user-images.githubusercontent.com/3372598/86538983-b60c7980-becf-11ea-8cad-1a33950e77fc.gif) - -I made this tool just for you ❤️ , it would be awesome if you could just click the [⭐️ Star button](https://github.com/victornpb/undiscord) at the top! - -> A few extra generous people asked for this, so here you can [buy me a coffee](https://www.buymeacoffee.com/vitim). Thank you! You'll be in my special list ^_^ - ----- -### Need help? -Check out the [wiki](https://github.com/victornpb/undiscord/wiki) for helpful articles, or read existing [questions](https://github.com/victornpb/undiscord/discussions), or post a new one. - -### Have an Idea or Feature request? -Check out the [Ideas][ideas] section, if your idea _hasn't been posted before_, please post a new one. - -### Found a bug? -Is prefered that _issues_ follow a certain format. If you're not familiar with bug reports, please use the [discussions][discussions] tab instead. - -If you believe you found a bug please file an [issue](https://github.com/victornpb/undiscord/issues), but please fill the issue template. - -### Copy paste version -Looking for the old Copy/Paste version? [here](./help/copyPaste.md) - - ----- - -Originally from https://gist.github.com/victornpb/135f5b346dea4decfc8f63ad7d9cc182 - ----- -## ⛔️ DO NOT SHARE YOUR AUTH TOKEN! ⛔️ ## - -Sharing your authToken on the internet will give full access to your account! [There are bots gathering credentials all over the internet](https://github.com/rndinfosecguy/Scavenger). -If you post your token by accident, LOGOUT from discord on that **same browser** you got that token imediately. -Changing your password will make sure that you get logged out of every device. I advice that you turn on [2FA](https://support.discord.com/hc/en-us/articles/219576828-Setting-up-Two-Factor-Authentication) afterwards. - -If you are unsure do not post screenshots, or logs on the internet. - ----- -## Security Concerns - -Using third-party scripts means you trust that the script’s developer hasn’t inserted malicious functionality into the code and has secured it against attackers trying to do the same. You should never run code you don't trust. - -Please read: [what I'm doing to ensure this is safe for users][security_policy]. - ----- -#### DISCLAIMER - -> THE SOFTWARE AND ALL INFORMATION HERE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -> -> By using any code or information provided here you are agreeing to all parts of the above Disclaimer. - - - - [self-bots]: https://support.discordapp.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots- - [userscrips_faq]: https://en.wikipedia.org/wiki/Userscript - [greasyfork_icon]: https://user-images.githubusercontent.com/3372598/166113712-1bc3d654-1342-4f1e-9845-21c3b21524b1.png - [openuserjs_icon]: https://user-images.githubusercontent.com/3372598/166113714-5a2ede39-8d66-43a8-b5da-8f1897cb3121.png - [greasyfork_moderation]: https://greasyfork.org/en/moderator_actions - - [issues]: https://github.com/victornpb/undiscord/issues - [issues_open]: https://github.com/victornpb/undiscord/issues - [issues_closed]: https://github.com/victornpb/undiscord/issues - [prs]: https://github.com/victornpb/undiscord/pulls - [pr_open]: https://github.com/victornpb/undiscord/pulls - [prs_closed]: https://github.com/victornpb/undiscord/pulls - [forks]: https://github.com/victornpb/undiscord/network/members - - [wiki]: https://github.com/victornpb/undiscord/wiki - [discussions]: https://github.com/victornpb/undiscord/discussions - [ideas]: https://github.com/victornpb/undiscord/discussions/categories/2-ideas - [questions]: https://github.com/victornpb/undiscord/discussions/categories/1-questions-answers - [security_policy]: https://github.com/victornpb/undiscord/wiki/Security-Policy - - - [chrome_violentmonkey]: https://chrome.google.com/webstore/detail/violent-monkey/jinjaccalgkegednnccohejagnlnfdag - [chrome_tampermonkey]: https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo - [firefox_greasemonkey]: https://addons.mozilla.org/firefox/addon/greasemonkey/ - [firefox_tampermonkey]: https://addons.mozilla.org/firefox/addon/tampermonkey/ - [firefox_violentmonkey]: https://addons.mozilla.org/firefox/addon/violentmonkey/ - [safari_tampermonkey]: https://github.com/victornpb/undiscord/issues/91#issuecomment-654514364 - [edge_tampermonkey]: https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd - [opera_tampermonkey]: https://addons.opera.com/extensions/details/tampermonkey-beta/ - [opera_violentmonkey]: https://addons.opera.com/extensions/details/violent-monkey/ - - - [greasyfork_url]: "Get Undiscord from GreasyFork" - [openuserjs_url]: "Get Undiscord from OpenUserJS"