From a038c206f71efdaee24eaf33d0f110efaaabdcd2 Mon Sep 17 00:00:00 2001
From: dekooah <139932915+dekooah@users.noreply.github.com>
Date: Sun, 19 Apr 2026 18:29:49 +1000
Subject: [PATCH] Completely overhaulled
This commit fixes various items, and adds features, such as;
- Issues with accent colours; Synced with theme
- Added hover animation for buttons
- Slight tweaks to theme
- Fixed button not registering
+ more
Signed-off-by: dekooah <139932915+dekooah@users.noreply.github.com>
---
deleteDiscordMessages.user.js | 2833 ++++++++++++++++-----------------
package-lock.json | 123 +-
package.json | 3 +
src/ui/main.css | 14 +-
src/ui/theme.css | 69 +-
src/ui/undiscord.html | 13 +-
src/undiscord-ui.js | 6 +-
7 files changed, 1509 insertions(+), 1552 deletions(-)
diff --git a/deleteDiscordMessages.user.js b/deleteDiscordMessages.user.js
index d023965c..7f179960 100644
--- a/deleteDiscordMessages.user.js
+++ b/deleteDiscordMessages.user.js
@@ -23,1506 +23,1403 @@
const VERSION = "5.2.6";
var themeCss = (`
-/* undiscord window */
-#undiscord.browser { box-shadow: var(--shadow-border), var(--shadow-high); border: 1px solid var(--border-subtle); overflow: hidden; }
-#undiscord.container,
-#undiscord .container { background-color: var(--background-surface-high); 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); cursor: grab; }
-#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 .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 { 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-default); 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; width: 100%; transition: border-color 0.2s ease-in-out 0s; padding: 10px; height: 44px; background-color: var(--input-background); border: 1px solid var(--input-border); border-radius: 8px; box-sizing: border-box; color: var(--text-default); }
-#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-default); 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"],
-#undiscord input[type="range"] { background-color: var(--input-background); border: 1px solid var(--input-border); border-radius: 8px; box-sizing: border-box; color: var(--text-default); font-size: 16px; height: 44px; padding: 12px 10px; transition: border-color .2s ease-in-out; width: 100%; }
-#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; }
-#undiscord .sizeMedium.icon { width: 38px; min-width: 38px; }
-#undiscord sup { vertical-align: top; }
-/* 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; }
-/**** functional classes ****/
-#undiscord.redact .priv { display: none !important; }
-#undiscord.redact x:not(:active) { color: transparent !important; background-color: var(--primary-700) !important; cursor: default; user-select: none; }
-#undiscord.redact x:hover { position: relative; }
-#undiscord.redact x:hover::after { content: "Redacted information (Streamer mode: ON)"; position: absolute; display: inline-block; top: -32px; left: -20px; padding: 4px; width: 150px; font-size: 8pt; text-align: center; white-space: pre-wrap; background-color: var(--background-floating); -webkit-box-shadow: var(--elevation-high); box-shadow: var(--elevation-high); color: var(--text-default); border-radius: 5px; pointer-events: none; }
-#undiscord.redact [priv] { -webkit-text-security: disc !important; }
-#undiscord :disabled { display: none; }
-/**** layout and utility classes ****/
-#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; }
-#undiscord .log { margin-bottom: 0.25em; }
-#undiscord .log-debug { color: inherit; }
-#undiscord .log-info { color: #00b0f4; }
-#undiscord .log-verb { color: #72767d; }
-#undiscord .log-warn { color: #faa61a; }
-#undiscord .log-error { color: #f04747; }
-#undiscord .log-success { color: #43b581; }
+/* undiscord window */
#undiscord.browser {
box-shadow: var(--shadow-border), var(--shadow-high);
border: 1px solid var(--border-subtle);
overflow: hidden;
}
#undiscord.container,
#undiscord .container {
background-color: var(--background-surface-high);
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(--text-default);
cursor: grab;
}
#undiscord.drag .header {
cursor: grabbing;
}
#undiscord .header .icon {
color: var(--scrollbar-thin-thumb);
margin-right: 8px;
flex-shrink: 0;
width: 24;
height: 24;
transition: color .3s ease;
}
#undiscord .header .logo {
color: var(--text-default);
}
#undiscord .header .disbtn:hover {
color: var(--interactive-text-hover);
}
#undiscord .header h3 {
font-size: 16px;
line-height: 20px;
font-weight: 500;
font-family: var(--font-display);
color: var(--text-default);
flex-shrink: 0;
margin-right: 16px;
}
#undiscord .spacer {
flex-grow: 1;
}
#undiscord .header .vert-divider {
width: 1px;
height: 24px;
background-color: var(--background-base-lower);
margin-right: 16px;
flex-shrink: 0;
}
#undiscord legend,
#undiscord label {
color: var(--scrollbar-thin-thumb);
font-size: 12px;
line-height: 16px;
font-weight: 500;
text-transform: uppercase;
cursor: default;
font-family: var(--font-display);
margin-bottom: 8px;
transition: color .3s ease;
}
#undiscord legend:hover,
#undiscord label:hover{
color: var(--interactive-text-hover);
}
#undiscord .multiInput {
display: flex;
align-items: center;
font-size: 16px;
box-sizing: border-box;
width: 100%;
border-radius: 3px;
color: var(--text-default);
background-color: var(--input-background-default);
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;
width: 100%;
transition: border-color 0.2s ease-in-out 0s;
padding: 10px;
height: 44px;
background-color: var(--input-background-default);
border: 1px solid var(--input-border-default);
border-radius: 8px;
box-sizing: border-box;
color: var(--text-default);
}
#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-default);
border: none;
transition: border-color 0.2s ease-in-out 0s;
padding-right: 3px;
}
#undiscord input[type="text"],
#undiscord input[type="search"],
#undiscord input[type="password"],
#undiscord input[type="datetime-local"],
#undiscord input[type="number"],
#undiscord input[type="range"] {
background-color: var(--input-background-default);
border: 1px solid var(--input-border-default);
border-radius: 8px;
box-sizing: border-box;
color: var(--text-default);
font-size: 16px;
height: 44px;
padding: 12px 10px;
transition: border-color .2s ease-in-out;
width: 100%;
}
#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(--scrollbar-thin-thumb);
font-size: 14px;
line-height: 20px;
font-weight: 400;
transition: color .3s ease;
}
#undiscord .sectionDescription:hover {
color: var(--interactive-text-hover);
}
#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: var(--text-default);
background-color: var(--control-secondary-background-default);
transition: background-color var(--custom-button-transition-duration) ease,color var(--custom-button-transition-duration) ease;
}
#undiscord button:hover {
background-color: var(--control-secondary-background-hover);
}
#undiscord .sizeMedium {
width: 96px;
height: 38px;
min-width: 96px;
min-height: 38px;
}
#undiscord .sizeMedium.icon {
width: 38px;
min-width: 38px;
}
#undiscord sup {
vertical-align: top;
}
#undiscord .danger {
background-color: var(--control-critical-primary-background-default);
color: var(--white);
}
#undiscord .danger:hover {
background-color: var(--control-critical-primary-background-hover);
}
#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;
}
/**** functional classes ****/
#undiscord.redact .priv {
display: none !important;
}
#undiscord.redact x:not(:active) {
color: transparent !important;
background-color: var(--primary-700) !important;
cursor: default;
user-select: none;
}
#undiscord.redact x:hover {
position: relative;
}
#undiscord.redact x:hover::after {
content: "Redacted information (Streamer mode: ON)";
position: absolute;
display: inline-block;
top: -32px;
left: -20px;
padding: 4px;
width: 150px;
font-size: 8pt;
text-align: center;
white-space: pre-wrap;
background-color: var(--background-floating);
-webkit-box-shadow: var(--elevation-high);
box-shadow: var(--elevation-high);
color: var(--text-default);
border-radius: 5px;
pointer-events: none;
}
#undiscord.redact [priv] {
-webkit-text-security: disc !important;
}
#undiscord :disabled {
display: none;
}
/**** layout and utility classes ****/
#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;
}
#undiscord .log {
margin-bottom: 0.25em;
}
#undiscord .log-debug {
color: inherit;
}
#undiscord .log-info {
color: #00b0f4;
}
#undiscord .log-verb {
color: #72767d;
}
#undiscord .log-warn {
color: #faa61a;
}
#undiscord .log-error {
color: #f04747;
}
#undiscord .log-success {
color: #43b581;
}
`);
var mainCss = (`
-/**** 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: 23px; left: -4px; width: 32px; height: 12px; display: none; }
-#undicord-btn.running { color: var(--button-danger-background) !important; }
-#undicord-btn.running progress { display: block; }
-/**** Undiscord Interface ****/
-#undiscord { position: fixed; z-index: 100; top: 58px; 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(--bg-overlay-4, var(--background-base-lowest)); }
-#undiscord .sidebar legend,
-#undiscord .sidebar label { display: block; width: 100%; }
-#undiscord .main { display: flex; max-width: calc(100% - 250px); background-color: var(--bg-overlay-chat, var(--background-base-lower)); flex-grow: 1; }
-#undiscord.hide-sidebar .sidebar { display: none; }
-#undiscord.hide-sidebar .main { max-width: 100%; }
-#undiscord #logArea { font-family: Consolas, Liberation Mono, Menlo, Courier, monospace; font-size: 0.75rem; overflow: auto; padding: 10px; user-select: text; flex-grow: 1; flex-grow: 1; cursor: auto; }
-#undiscord .tbar { padding: 8px; background-color: var(--bg-overlay-2, var(--__header-bar-background)); }
-#undiscord .tbar button { margin-right: 4px; margin-bottom: 4px; }
-#undiscord .footer { cursor: se-resize; padding-right: 30px; }
-#undiscord .footer #progressPercent { padding: 0 1em; font-size: small; color: var(--interactive-muted); flex-grow: 1; }
-.resize-handle { position: absolute; bottom: -15px; right: -15px; width: 30px; height: 30px; transform: rotate(-45deg); background: repeating-linear-gradient(0, var(--background-modifier-accent), var(--background-modifier-accent) 1px, transparent 2px, transparent 4px); cursor: nwse-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; }
-#undiscord legend a { float: right; text-transform: initial; }
-#undiscord progress { height: 8px; margin-top: 4px; flex-grow: 1; }
-#undiscord .importJson { display: flex; flex-direction: row; }
-#undiscord .importJson button { margin-left: 5px; width: fit-content; }
+/**** Undiscord Button ****/
#undicord-btn {
position: relative;
width: auto;
height: 24px;
margin: 0 8px;
cursor: pointer;
color: var(--scrollbar-thin-thumb);
flex: 0 0 auto;
}
#undicord-btn progress {
position: absolute;
top: 23px;
left: -4px;
width: 32px;
height: 12px;
display: none;
}
#undicord-btn.running {
color: var(--control-critical-primary-background-default) !important;
}
#undicord-btn.running progress {
display: block;
}
/**** Undiscord Interface ****/
#undiscord {
position: fixed;
z-index: 100;
top: 58px;
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-default);
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(--bg-overlay-4, var(--background-base-lowest));
}
#undiscord .sidebar legend,
#undiscord .sidebar label {
display: block;
width: 100%;
}
#undiscord .main {
display: flex;
max-width: calc(100% - 250px);
background-color: var(--bg-overlay-chat, var(--background-base-lower));
flex-grow: 1;
}
#undiscord.hide-sidebar .sidebar {
display: none;
}
#undiscord.hide-sidebar .main {
max-width: 100%;
}
#undiscord #logArea {
font-family: Consolas, Liberation Mono, Menlo, Courier, monospace;
font-size: 0.75rem;
overflow: auto;
padding: 10px;
user-select: text;
flex-grow: 1;
flex-grow: 1;
cursor: auto;
}
#undiscord .tbar {
padding: 8px;
background-color: var(--bg-overlay-2, var(--__header-bar-background));
}
#undiscord .tbar button {
margin-right: 4px;
margin-bottom: 4px;
}
#undiscord .footer {
cursor: se-resize;
padding-right: 30px;
}
#undiscord .footer #progressPercent {
padding: 0 1em;
font-size: small;
color: var(--interactive-muted);
flex-grow: 1;
}
.resize-handle {
position: absolute;
bottom: -15px;
right: -15px;
width: 30px;
height: 30px;
transform: rotate(-45deg);
background: repeating-linear-gradient(0, var(--background-modifier-accent), var(--background-modifier-accent) 1px, transparent 2px, transparent 4px);
cursor: nwse-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(--scrollbar-thin-thumb);
border-radius: 4px;
flex-shrink: 0;
transition: color .3s ease;
}
#undiscord summary:hover {
color: var(--interactive-text-hover);
}
#undiscord fieldset {
padding-left: 8px;
}
#undiscord legend a {
float: right;
text-transform: initial;
}
#undiscord progress {
height: 8px;
margin-top: 4px;
flex-grow: 1;
}
#undiscord .importJson {
display: flex;
flex-direction: row;
}
#undiscord .importJson button {
margin-left: 5px;
width: fit-content;
}
`);
var dragCss = (`
-[name^="grab-"] { position: absolute; --size: 6px; --corner-size: 16px; --offset: -1px; z-index: 9; }
-[name^="grab-"]:hover{ background: rgba(128,128,128,0.1); }
-[name="grab-t"] { top: 0px; left: var(--corner-size); right: var(--corner-size); height: var(--size); margin-top: var(--offset); cursor: ns-resize; }
-[name="grab-r"] { top: var(--corner-size); bottom: var(--corner-size); right: 0px; width: var(--size); margin-right: var(--offset);
- cursor: ew-resize; }
-[name="grab-b"] { bottom: 0px; left: var(--corner-size); right: var(--corner-size); height: var(--size); margin-bottom: var(--offset); cursor: ns-resize; }
-[name="grab-l"] { top: var(--corner-size); bottom: var(--corner-size); left: 0px; width: var(--size); margin-left: var(--offset); cursor: ew-resize; }
-[name="grab-tl"] { top: 0px; left: 0px; width: var(--corner-size); height: var(--corner-size); margin-top: var(--offset); margin-left: var(--offset); cursor: nwse-resize; }
-[name="grab-tr"] { top: 0px; right: 0px; width: var(--corner-size); height: var(--corner-size); margin-top: var(--offset); margin-right: var(--offset); cursor: nesw-resize; }
-[name="grab-br"] { bottom: 0px; right: 0px; width: var(--corner-size); height: var(--corner-size); margin-bottom: var(--offset); margin-right: var(--offset); cursor: nwse-resize; }
-[name="grab-bl"] { bottom: 0px; left: 0px; width: var(--corner-size); height: var(--corner-size); margin-bottom: var(--offset); margin-left: var(--offset); cursor: nesw-resize; }
+[name^="grab-"] {
position: absolute;
--size: 6px;
--corner-size: 16px;
--offset: -1px;
z-index: 9;
}
[name^="grab-"]:hover{
background: rgba(128,128,128,0.1);
}
[name="grab-t"] {
top: 0px;
left: var(--corner-size);
right: var(--corner-size);
height: var(--size);
margin-top: var(--offset);
cursor: ns-resize;
}
[name="grab-r"] {
top: var(--corner-size);
bottom: var(--corner-size);
right: 0px;
width: var(--size);
margin-right: var(--offset);
cursor: ew-resize;
}
[name="grab-b"] {
bottom: 0px;
left: var(--corner-size);
right: var(--corner-size);
height: var(--size);
margin-bottom: var(--offset);
cursor: ns-resize;
}
[name="grab-l"] {
top: var(--corner-size);
bottom: var(--corner-size);
left: 0px;
width: var(--size);
margin-left: var(--offset);
cursor: ew-resize;
}
[name="grab-tl"] {
top: 0px;
left: 0px;
width: var(--corner-size);
height: var(--corner-size);
margin-top: var(--offset);
margin-left: var(--offset);
cursor: nwse-resize;
}
[name="grab-tr"] {
top: 0px;
right: 0px;
width: var(--corner-size);
height: var(--corner-size);
margin-top: var(--offset);
margin-right: var(--offset);
cursor: nesw-resize;
}
[name="grab-br"] {
bottom: 0px;
right: 0px;
width: var(--corner-size);
height: var(--corner-size);
margin-bottom: var(--offset);
margin-right: var(--offset);
cursor: nwse-resize;
}
[name="grab-bl"] {
bottom: 0px;
left: 0px;
width: var(--corner-size);
height: var(--corner-size);
margin-bottom: var(--offset);
margin-left: var(--offset);
cursor: nesw-resize;
}
`);
var buttonHtml = (`
-
-
-
-
-
-
+
`);
var undiscordTemplate = (`
-
-
-
-
-
-
-
-
- ▶︎ Delete
- 🛑 Stop
- Clear log
-
- Streamer mode
-
-
-
-
-
- Notice: Undiscord may be working slower than usual andrequire multiple attempts due to a recent Discord update. We're working on a fix, and we thank you for your patience.
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ ▶︎ Delete
+ 🛑 Stop
+ Clear log
+
+ Streamer mode
+
+
+
+
+
+ Notice: Undiscord may be working slower than usual andrequire multiple attempts due to a recent Discord update. We're working on a fix, and we thank you for your patience.
+
+
+
+
+
+
+
+
+
`);
- const log = {
- debug() { return logFn ? logFn('debug', arguments) : console.debug.apply(console, arguments); },
- info() { return logFn ? logFn('info', arguments) : console.info.apply(console, arguments); },
- verb() { return logFn ? logFn('verb', arguments) : console.log.apply(console, arguments); },
- warn() { return logFn ? logFn('warn', arguments) : console.warn.apply(console, arguments); },
- error() { return logFn ? logFn('error', arguments) : console.error.apply(console, arguments); },
- success() { return logFn ? logFn('success', arguments) : console.info.apply(console, arguments); },
- };
-
- var logFn; // custom console.log function
+ const log = {
+ debug() { return logFn ? logFn('debug', arguments) : console.debug.apply(console, arguments); },
+ info() { return logFn ? logFn('info', arguments) : console.info.apply(console, arguments); },
+ verb() { return logFn ? logFn('verb', arguments) : console.log.apply(console, arguments); },
+ warn() { return logFn ? logFn('warn', arguments) : console.warn.apply(console, arguments); },
+ error() { return logFn ? logFn('error', arguments) : console.error.apply(console, arguments); },
+ success() { return logFn ? logFn('success', arguments) : console.info.apply(console, arguments); },
+ };
+
+ var logFn; // custom console.log function
const setLogFn = (fn) => logFn = fn;
- // Helpers
- 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 => String(html).replace(/[&<"']/g, m => ({ '&': '&', '<': '<', '"': '"', '\'': ''' })[m]);
- const redact = str => `
${escapeHTML(str)} `;
- 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 toSnowflake = (date) => /:/.test(date) ? ((new Date(date).getTime() - 1420070400000) * Math.pow(2, 22)) : date;
+ // Helpers
+ 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 => String(html).replace(/[&<"']/g, m => ({ '&': '&', '<': '<', '"': '"', '\'': ''' })[m]);
+ const redact = str => `
${escapeHTML(str)} `;
+ 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 toSnowflake = (date) => /:/.test(date) ? ((new Date(date).getTime() - 1420070400000) * Math.pow(2, 22)) : date;
const replaceInterpolations = (str, obj, removeMissing = false) => str.replace(/\{\{([\w_]+)\}\}/g, (m, key) => obj[key] || (removeMissing ? '' : m));
- const PREFIX$1 = '[UNDISCORD]';
-
- /**
- * Delete all messages in a Discord channel or DM
- * @author Victornpb
- * @see https://github.com/victornpb/undiscord
- */
- class UndiscordCore {
-
- options = {
- authToken: null, // Your authorization token
- authorId: null, // Author of the messages you want to delete
- guildId: null, // Server were the messages are located
- channelId: null, // Channel were the messages are located
- minId: null, // Only delete messages after this, leave blank do delete all
- maxId: null, // Only delete messages before this, leave blank do delete all
- content: null, // Filter messages that contains this text content
- hasLink: null, // Filter messages that contains link
- hasFile: null, // Filter messages that contains file
- includeNsfw: null, // Search in NSFW channels
- includePinned: null, // Delete messages that are pinned
- pattern: null, // Only delete messages that match the regex (insensitive)
- searchDelay: null, // Delay each time we fetch for more messages
- deleteDelay: null, // Delay between each delete operation
- maxAttempt: 2, // Attempts to delete a single message if it fails
- askForConfirmation: true,
- };
-
- state = {
- running: false,
- delCount: 0,
- failCount: 0,
- grandTotal: 0,
- offset: 0,
- iterations: 0,
-
- _seachResponse: null,
- _messagesToDelete: [],
- _skippedMessages: [],
- };
-
- stats = {
- startTime: new Date(), // start time
- throttledCount: 0, // how many times you have been throttled
- throttledTotalTime: 0, // the total amount of time you spent being throttled
- lastPing: null, // the most recent ping
- avgPing: null, // average ping used to calculate the estimated remaining time
- etr: 0,
- };
-
- // events
- onStart = undefined;
- onProgress = undefined;
- onStop = undefined;
-
- resetState() {
- this.state = {
- running: false,
- delCount: 0,
- failCount: 0,
- grandTotal: 0,
- offset: 0,
- iterations: 0,
-
- _seachResponse: null,
- _messagesToDelete: [],
- _skippedMessages: [],
- };
-
- this.options.askForConfirmation = true;
- }
-
- /** Automate the deletion process of multiple channels */
- async runBatch(queue) {
- if (this.state.running) return log.error('Already running!');
-
- log.info(`Runnning batch with queue of ${queue.length} jobs`);
- for (let i = 0; i < queue.length; i++) {
- const job = queue[i];
- log.info('Starting job...', `(${i + 1}/${queue.length})`);
-
- // set options
- this.options = {
- ...this.options, // keep current options
- ...job, // override with options for that job
- };
-
- await this.run(true);
- if (!this.state.running) break;
-
- log.info('Job ended.', `(${i + 1}/${queue.length})`);
- this.resetState();
- this.options.askForConfirmation = false;
- this.state.running = true; // continue running
- }
-
- log.info('Batch finished.');
- this.state.running = false;
- }
-
- /** Start the deletion process */
- async run(isJob = false) {
- if (this.state.running && !isJob) return log.error('Already running!');
-
- this.state.running = true;
- this.stats.startTime = new Date();
-
- log.success(`\nStarted at ${this.stats.startTime.toLocaleString()}`);
- log.debug(
- `authorId = "${redact(this.options.authorId)}"`,
- `guildId = "${redact(this.options.guildId)}"`,
- `channelId = "${redact(this.options.channelId)}"`,
- `minId = "${redact(this.options.minId)}"`,
- `maxId = "${redact(this.options.maxId)}"`,
- `hasLink = ${!!this.options.hasLink}`,
- `hasFile = ${!!this.options.hasFile}`,
- );
-
- if (this.onStart) this.onStart(this.state, this.stats);
-
- do {
- this.state.iterations++;
-
- log.verb('Fetching messages...');
- // Search messages
- await this.search();
-
- // Process results and find which messages should be deleted
- await this.filterResponse();
-
- log.verb(
- `Grand total: ${this.state.grandTotal}`,
- `(Messages in current page: ${this.state._seachResponse.messages.length}`,
- `To be deleted: ${this.state._messagesToDelete.length}`,
- `Skipped: ${this.state._skippedMessages.length})`,
- `offset: ${this.state.offset}`
- );
- this.printStats();
-
- // Calculate estimated time
- this.calcEtr();
- log.verb(`Estimated time remaining: ${msToHMS(this.stats.etr)}`);
-
- // if there are messages to delete, delete them
- if (this.state._messagesToDelete.length > 0) {
-
- if (await this.confirm() === false) {
- this.state.running = false; // break out of a job
- break; // immmediately stop this iteration
- }
-
- await this.deleteMessagesFromList();
- }
- else if (this.state._skippedMessages.length > 0) {
- // There are stuff, but nothing to delete (example a page full of system messages)
- // check next page until we see a page with nothing in it (end of results).
- const oldOffset = this.state.offset;
- this.state.offset += this.state._skippedMessages.length;
- log.verb('There\'s nothing we can delete on this page, checking next page...');
- log.verb(`Skipped ${this.state._skippedMessages.length} out of ${this.state._seachResponse.messages.length} in this page.`, `(Offset was ${oldOffset}, ajusted to ${this.state.offset})`);
- }
- else {
- log.verb('Ended because API returned an empty page.');
- log.verb('[End state]', this.state);
- if (isJob) break; // break without stopping if this is part of a job
- this.state.running = false;
- }
-
- // wait before next page (fix search page not updating fast enough)
- log.verb(`Waiting ${(this.options.searchDelay / 1000).toFixed(2)}s before next page...`);
- await wait(this.options.searchDelay);
-
- } while (this.state.running);
-
- this.stats.endTime = new Date();
- log.success(`Ended at ${this.stats.endTime.toLocaleString()}! Total time: ${msToHMS(this.stats.endTime.getTime() - this.stats.startTime.getTime())}`);
- this.printStats();
- log.debug(`Deleted ${this.state.delCount} messages, ${this.state.failCount} failed.\n`);
-
- if (this.onStop) this.onStop(this.state, this.stats);
- }
-
- stop() {
- this.state.running = false;
- if (this.onStop) this.onStop(this.state, this.stats);
- }
-
- /** Calculate the estimated time remaining based on the current stats */
- calcEtr() {
- this.stats.etr = (this.options.searchDelay * Math.round(this.state.grandTotal / 25)) + ((this.options.deleteDelay + this.stats.avgPing) * this.state.grandTotal);
- }
-
- /** As for confirmation in the beggining process */
- async confirm() {
- if (!this.options.askForConfirmation) return true;
-
- log.verb('Waiting for your confirmation...');
- const preview = this.state._messagesToDelete.map(m => `${m.author.username}#${m.author.discriminator}: ${m.attachments.length ? '[ATTACHMENTS]' : m.content}`).join('\n');
-
- const answer = await ask(
- `Do you want to delete ~${this.state.grandTotal} messages? (Estimated time: ${msToHMS(this.stats.etr)})` +
- '(The actual number of messages may be less, depending if you\'re using filters to skip some messages)' +
- '\n\n---- Preview ----\n' +
- preview
- );
-
- if (!answer) {
- log.error('Aborted by you!');
- return false;
- }
- else {
- log.verb('OK');
- this.options.askForConfirmation = false; // do not ask for confirmation again on the next request
- return true;
- }
- }
-
- async search() {
- let API_SEARCH_URL;
- if (this.options.guildId === '@me') API_SEARCH_URL = `https://discord.com/api/v9/channels/${this.options.channelId}/messages/`; // DMs
- else API_SEARCH_URL = `https://discord.com/api/v9/guilds/${this.options.guildId}/messages/`; // Server
-
- let resp;
- try {
- this.beforeRequest();
- resp = await fetch(API_SEARCH_URL + 'search?' + queryString([
- ['author_id', this.options.authorId || undefined],
- ['channel_id', (this.options.guildId !== '@me' ? this.options.channelId : undefined) || undefined],
- ['min_id', this.options.minId ? toSnowflake(this.options.minId) : undefined],
- ['max_id', this.options.maxId ? toSnowflake(this.options.maxId) : undefined],
- ['sort_by', 'timestamp'],
- ['sort_order', 'desc'],
- ['offset', this.state.offset],
- ['has', this.options.hasLink ? 'link' : undefined],
- ['has', this.options.hasFile ? 'file' : undefined],
- ['content', this.options.content || undefined],
- ['include_nsfw', this.options.includeNsfw ? true : undefined],
- ]), {
- headers: {
- 'Authorization': this.options.authToken,
- }
- });
- this.afterRequest();
- } catch (err) {
- this.state.running = false;
- log.error('Search request threw an error:', err);
- throw err;
- }
-
- // not indexed yet
- if (resp.status === 202) {
- let w = (await resp.json()).retry_after * 1000;
- w = w || this.stats.searchDelay; // Fix retry_after 0
- this.stats.throttledCount++;
- this.stats.throttledTotalTime += w;
- log.warn(`This channel isn't indexed yet. Waiting ${w}ms for discord to index it...`);
- await wait(w);
- return await this.search();
- }
-
- if (!resp.ok) {
- // searching messages too fast
- if (resp.status === 429) {
- let w = (await resp.json()).retry_after * 1000;
- w = w || this.stats.searchDelay; // Fix retry_after 0
-
- this.stats.throttledCount++;
- this.stats.throttledTotalTime += w;
- this.stats.searchDelay += w; // increase delay
- w = this.stats.searchDelay;
- log.warn(`Being rate limited by the API for ${w}ms! Increasing search delay...`);
- this.printStats();
- log.verb(`Cooling down for ${w * 2}ms before retrying...`);
-
- await wait(w * 2);
- return await this.search();
- }
- else {
- this.state.running = false;
- log.error(`Error searching messages, API responded with status ${resp.status}!\n`, await resp.json());
- throw resp;
- }
- }
- const data = await resp.json();
- this.state._seachResponse = data;
- console.log(PREFIX$1, 'search', data);
- return data;
- }
-
- async filterResponse() {
- const data = this.state._seachResponse;
-
- // the search total will decrease as we delete stuff
- const total = data.total_results;
- if (total > this.state.grandTotal) this.state.grandTotal = total;
-
- // search returns messages near the the actual message, only get the messages we searched for.
- const discoveredMessages = data.messages.map(convo => convo.find(message => message.hit === true));
-
- // we can only delete some types of messages, system messages are not deletable.
- let messagesToDelete = discoveredMessages;
- messagesToDelete = messagesToDelete.filter(msg => msg.type === 0 || (msg.type >= 6 && msg.type <= 21));
- messagesToDelete = messagesToDelete.filter(msg => msg.pinned ? this.options.includePinned : true);
-
- // custom filter of messages
- try {
- const regex = new RegExp(this.options.pattern, 'i');
- messagesToDelete = messagesToDelete.filter(msg => regex.test(msg.content));
- } catch (e) {
- log.warn('Ignoring RegExp because pattern is malformed!', e);
- }
-
- // create an array containing everything we skipped. (used to calculate offset for next searches)
- const skippedMessages = discoveredMessages.filter(msg => !messagesToDelete.find(m => m.id === msg.id));
-
- this.state._messagesToDelete = messagesToDelete;
- this.state._skippedMessages = skippedMessages;
-
- console.log(PREFIX$1, 'filterResponse', this.state);
- }
-
- async deleteMessagesFromList() {
- for (let i = 0; i < this.state._messagesToDelete.length; i++) {
- const message = this.state._messagesToDelete[i];
- if (!this.state.running) return log.error('Stopped by you!');
-
- log.debug(
- // `${((this.state.delCount + 1) / this.state.grandTotal * 100).toFixed(2)}%`,
- `[${this.state.delCount + 1}/${this.state.grandTotal}] ` +
- `${new Date(message.timestamp).toLocaleString()} ` +
- `${redact(message.author.username + '#' + message.author.discriminator)} ` +
- `: ${redact(message.content).replace(/\n/g, '↵')} ` +
- (message.attachments.length ? redact(JSON.stringify(message.attachments)) : ''),
- `{ID:${redact(message.id)}} `
- );
-
- // Delete a single message (with retry)
- let attempt = 0;
- while (attempt < this.options.maxAttempt) {
- const result = await this.deleteMessage(message);
-
- if (result === 'RETRY') {
- attempt++;
- log.verb(`Retrying in ${this.options.deleteDelay}ms... (${attempt}/${this.options.maxAttempt})`);
- await wait(this.options.deleteDelay);
- }
- else break;
- }
-
- this.calcEtr();
- if (this.onProgress) this.onProgress(this.state, this.stats);
-
- await wait(this.options.deleteDelay);
- }
- }
-
- async deleteMessage(message) {
- const API_DELETE_URL = `https://discord.com/api/v9/channels/${message.channel_id}/messages/${message.id}`;
- let resp;
- try {
- this.beforeRequest();
- resp = await fetch(API_DELETE_URL, {
- method: 'DELETE',
- headers: {
- 'Authorization': this.options.authToken,
- },
- });
- this.afterRequest();
- } catch (err) {
- // no response error (e.g. network error)
- log.error('Delete request throwed an error:', err);
- log.verb('Related object:', redact(JSON.stringify(message)));
- this.state.failCount++;
- return 'FAILED';
- }
-
- if (!resp.ok) {
- if (resp.status === 429) {
- // deleting messages too fast
- const w = (await resp.json()).retry_after * 1000;
- this.stats.throttledCount++;
- this.stats.throttledTotalTime += w;
- this.options.deleteDelay = w; // increase delay
- log.warn(`Being rate limited by the API for ${w}ms! Adjusted delete delay to ${this.options.deleteDelay}ms.`);
- this.printStats();
- log.verb(`Cooling down for ${w * 2}ms before retrying...`);
- await wait(w * 2);
- return 'RETRY';
- } else {
- const body = await resp.text();
-
- try {
- const r = JSON.parse(body);
-
- if (resp.status === 400 && r.code === 50083) {
- // 400 can happen if the thread is archived (code=50083)
- // in this case we need to "skip" this message from the next search
- // otherwise it will come up again in the next page (and fail to delete again)
- log.warn('Error deleting message (Thread is archived). Will increment offset so we don\'t search this in the next page...');
- this.state.offset++;
- this.state.failCount++;
- return 'FAIL_SKIP'; // Failed but we will skip it next time
- }
-
- log.error(`Error deleting message, API responded with status ${resp.status}!`, r);
- log.verb('Related object:', redact(JSON.stringify(message)));
- this.state.failCount++;
- return 'FAILED';
- } catch (e) {
- log.error(`Fail to parse JSON. API responded with status ${resp.status}!`, body);
- }
- }
- }
-
- this.state.delCount++;
- return 'OK';
- }
-
- #beforeTs = 0; // used to calculate latency
- beforeRequest() {
- this.#beforeTs = Date.now();
- }
- afterRequest() {
- this.stats.lastPing = (Date.now() - this.#beforeTs);
- this.stats.avgPing = this.stats.avgPing > 0 ? (this.stats.avgPing * 0.9) + (this.stats.lastPing * 0.1) : this.stats.lastPing;
- }
-
- printStats() {
- log.verb(
- `Delete delay: ${this.options.deleteDelay}ms, Search delay: ${this.options.searchDelay}ms`,
- `Last Ping: ${this.stats.lastPing}ms, Average Ping: ${this.stats.avgPing | 0}ms`,
- );
- log.verb(
- `Rate Limited: ${this.stats.throttledCount} times.`,
- `Total time throttled: ${msToHMS(this.stats.throttledTotalTime)}.`
- );
- }
+ const PREFIX$1 = '[UNDISCORD]';
+
+ /**
+ * Delete all messages in a Discord channel or DM
+ * @author Victornpb
+ * @see https://github.com/victornpb/undiscord
+ */
+ class UndiscordCore {
+
+ options = {
+ authToken: null, // Your authorization token
+ authorId: null, // Author of the messages you want to delete
+ guildId: null, // Server were the messages are located
+ channelId: null, // Channel were the messages are located
+ minId: null, // Only delete messages after this, leave blank do delete all
+ maxId: null, // Only delete messages before this, leave blank do delete all
+ content: null, // Filter messages that contains this text content
+ hasLink: null, // Filter messages that contains link
+ hasFile: null, // Filter messages that contains file
+ includeNsfw: null, // Search in NSFW channels
+ includePinned: null, // Delete messages that are pinned
+ pattern: null, // Only delete messages that match the regex (insensitive)
+ searchDelay: null, // Delay each time we fetch for more messages
+ deleteDelay: null, // Delay between each delete operation
+ maxAttempt: 2, // Attempts to delete a single message if it fails
+ askForConfirmation: true,
+ };
+
+ state = {
+ running: false,
+ delCount: 0,
+ failCount: 0,
+ grandTotal: 0,
+ offset: 0,
+ iterations: 0,
+
+ _seachResponse: null,
+ _messagesToDelete: [],
+ _skippedMessages: [],
+ };
+
+ stats = {
+ startTime: new Date(), // start time
+ throttledCount: 0, // how many times you have been throttled
+ throttledTotalTime: 0, // the total amount of time you spent being throttled
+ lastPing: null, // the most recent ping
+ avgPing: null, // average ping used to calculate the estimated remaining time
+ etr: 0,
+ };
+
+ // events
+ onStart = undefined;
+ onProgress = undefined;
+ onStop = undefined;
+
+ resetState() {
+ this.state = {
+ running: false,
+ delCount: 0,
+ failCount: 0,
+ grandTotal: 0,
+ offset: 0,
+ iterations: 0,
+
+ _seachResponse: null,
+ _messagesToDelete: [],
+ _skippedMessages: [],
+ };
+
+ this.options.askForConfirmation = true;
+ }
+
+ /** Automate the deletion process of multiple channels */
+ async runBatch(queue) {
+ if (this.state.running) return log.error('Already running!');
+
+ log.info(`Runnning batch with queue of ${queue.length} jobs`);
+ for (let i = 0; i < queue.length; i++) {
+ const job = queue[i];
+ log.info('Starting job...', `(${i + 1}/${queue.length})`);
+
+ // set options
+ this.options = {
+ ...this.options, // keep current options
+ ...job, // override with options for that job
+ };
+
+ await this.run(true);
+ if (!this.state.running) break;
+
+ log.info('Job ended.', `(${i + 1}/${queue.length})`);
+ this.resetState();
+ this.options.askForConfirmation = false;
+ this.state.running = true; // continue running
+ }
+
+ log.info('Batch finished.');
+ this.state.running = false;
+ }
+
+ /** Start the deletion process */
+ async run(isJob = false) {
+ if (this.state.running && !isJob) return log.error('Already running!');
+
+ this.state.running = true;
+ this.stats.startTime = new Date();
+
+ log.success(`\nStarted at ${this.stats.startTime.toLocaleString()}`);
+ log.debug(
+ `authorId = "${redact(this.options.authorId)}"`,
+ `guildId = "${redact(this.options.guildId)}"`,
+ `channelId = "${redact(this.options.channelId)}"`,
+ `minId = "${redact(this.options.minId)}"`,
+ `maxId = "${redact(this.options.maxId)}"`,
+ `hasLink = ${!!this.options.hasLink}`,
+ `hasFile = ${!!this.options.hasFile}`,
+ );
+
+ if (this.onStart) this.onStart(this.state, this.stats);
+
+ do {
+ this.state.iterations++;
+
+ log.verb('Fetching messages...');
+ // Search messages
+ await this.search();
+
+ // Process results and find which messages should be deleted
+ await this.filterResponse();
+
+ log.verb(
+ `Grand total: ${this.state.grandTotal}`,
+ `(Messages in current page: ${this.state._seachResponse.messages.length}`,
+ `To be deleted: ${this.state._messagesToDelete.length}`,
+ `Skipped: ${this.state._skippedMessages.length})`,
+ `offset: ${this.state.offset}`
+ );
+ this.printStats();
+
+ // Calculate estimated time
+ this.calcEtr();
+ log.verb(`Estimated time remaining: ${msToHMS(this.stats.etr)}`);
+
+ // if there are messages to delete, delete them
+ if (this.state._messagesToDelete.length > 0) {
+
+ if (await this.confirm() === false) {
+ this.state.running = false; // break out of a job
+ break; // immmediately stop this iteration
+ }
+
+ await this.deleteMessagesFromList();
+ }
+ else if (this.state._skippedMessages.length > 0) {
+ // There are stuff, but nothing to delete (example a page full of system messages)
+ // check next page until we see a page with nothing in it (end of results).
+ const oldOffset = this.state.offset;
+ this.state.offset += this.state._skippedMessages.length;
+ log.verb('There\'s nothing we can delete on this page, checking next page...');
+ log.verb(`Skipped ${this.state._skippedMessages.length} out of ${this.state._seachResponse.messages.length} in this page.`, `(Offset was ${oldOffset}, ajusted to ${this.state.offset})`);
+ }
+ else {
+ log.verb('Ended because API returned an empty page.');
+ log.verb('[End state]', this.state);
+ if (isJob) break; // break without stopping if this is part of a job
+ this.state.running = false;
+ }
+
+ // wait before next page (fix search page not updating fast enough)
+ log.verb(`Waiting ${(this.options.searchDelay / 1000).toFixed(2)}s before next page...`);
+ await wait(this.options.searchDelay);
+
+ } while (this.state.running);
+
+ this.stats.endTime = new Date();
+ log.success(`Ended at ${this.stats.endTime.toLocaleString()}! Total time: ${msToHMS(this.stats.endTime.getTime() - this.stats.startTime.getTime())}`);
+ this.printStats();
+ log.debug(`Deleted ${this.state.delCount} messages, ${this.state.failCount} failed.\n`);
+
+ if (this.onStop) this.onStop(this.state, this.stats);
+ }
+
+ stop() {
+ this.state.running = false;
+ if (this.onStop) this.onStop(this.state, this.stats);
+ }
+
+ /** Calculate the estimated time remaining based on the current stats */
+ calcEtr() {
+ this.stats.etr = (this.options.searchDelay * Math.round(this.state.grandTotal / 25)) + ((this.options.deleteDelay + this.stats.avgPing) * this.state.grandTotal);
+ }
+
+ /** As for confirmation in the beggining process */
+ async confirm() {
+ if (!this.options.askForConfirmation) return true;
+
+ log.verb('Waiting for your confirmation...');
+ const preview = this.state._messagesToDelete.map(m => `${m.author.username}#${m.author.discriminator}: ${m.attachments.length ? '[ATTACHMENTS]' : m.content}`).join('\n');
+
+ const answer = await ask(
+ `Do you want to delete ~${this.state.grandTotal} messages? (Estimated time: ${msToHMS(this.stats.etr)})` +
+ '(The actual number of messages may be less, depending if you\'re using filters to skip some messages)' +
+ '\n\n---- Preview ----\n' +
+ preview
+ );
+
+ if (!answer) {
+ log.error('Aborted by you!');
+ return false;
+ }
+ else {
+ log.verb('OK');
+ this.options.askForConfirmation = false; // do not ask for confirmation again on the next request
+ return true;
+ }
+ }
+
+ async search() {
+ let API_SEARCH_URL;
+ if (this.options.guildId === '@me') API_SEARCH_URL = `https://discord.com/api/v9/channels/${this.options.channelId}/messages/`; // DMs
+ else API_SEARCH_URL = `https://discord.com/api/v9/guilds/${this.options.guildId}/messages/`; // Server
+
+ let resp;
+ try {
+ this.beforeRequest();
+ resp = await fetch(API_SEARCH_URL + 'search?' + queryString([
+ ['author_id', this.options.authorId || undefined],
+ ['channel_id', (this.options.guildId !== '@me' ? this.options.channelId : undefined) || undefined],
+ ['min_id', this.options.minId ? toSnowflake(this.options.minId) : undefined],
+ ['max_id', this.options.maxId ? toSnowflake(this.options.maxId) : undefined],
+ ['sort_by', 'timestamp'],
+ ['sort_order', 'desc'],
+ ['offset', this.state.offset],
+ ['has', this.options.hasLink ? 'link' : undefined],
+ ['has', this.options.hasFile ? 'file' : undefined],
+ ['content', this.options.content || undefined],
+ ['include_nsfw', this.options.includeNsfw ? true : undefined],
+ ]), {
+ headers: {
+ 'Authorization': this.options.authToken,
+ }
+ });
+ this.afterRequest();
+ } catch (err) {
+ this.state.running = false;
+ log.error('Search request threw an error:', err);
+ throw err;
+ }
+
+ // not indexed yet
+ if (resp.status === 202) {
+ let w = (await resp.json()).retry_after * 1000;
+ w = w || this.stats.searchDelay; // Fix retry_after 0
+ this.stats.throttledCount++;
+ this.stats.throttledTotalTime += w;
+ log.warn(`This channel isn't indexed yet. Waiting ${w}ms for discord to index it...`);
+ await wait(w);
+ return await this.search();
+ }
+
+ if (!resp.ok) {
+ // searching messages too fast
+ if (resp.status === 429) {
+ let w = (await resp.json()).retry_after * 1000;
+ w = w || this.stats.searchDelay; // Fix retry_after 0
+
+ this.stats.throttledCount++;
+ this.stats.throttledTotalTime += w;
+ this.stats.searchDelay += w; // increase delay
+ w = this.stats.searchDelay;
+ log.warn(`Being rate limited by the API for ${w}ms! Increasing search delay...`);
+ this.printStats();
+ log.verb(`Cooling down for ${w * 2}ms before retrying...`);
+
+ await wait(w * 2);
+ return await this.search();
+ }
+ else {
+ this.state.running = false;
+ log.error(`Error searching messages, API responded with status ${resp.status}!\n`, await resp.json());
+ throw resp;
+ }
+ }
+ const data = await resp.json();
+ this.state._seachResponse = data;
+ console.log(PREFIX$1, 'search', data);
+ return data;
+ }
+
+ async filterResponse() {
+ const data = this.state._seachResponse;
+
+ // the search total will decrease as we delete stuff
+ const total = data.total_results;
+ if (total > this.state.grandTotal) this.state.grandTotal = total;
+
+ // search returns messages near the the actual message, only get the messages we searched for.
+ const discoveredMessages = data.messages.map(convo => convo.find(message => message.hit === true));
+
+ // we can only delete some types of messages, system messages are not deletable.
+ let messagesToDelete = discoveredMessages;
+ messagesToDelete = messagesToDelete.filter(msg => msg.type === 0 || (msg.type >= 6 && msg.type <= 21));
+ messagesToDelete = messagesToDelete.filter(msg => msg.pinned ? this.options.includePinned : true);
+
+ // custom filter of messages
+ try {
+ const regex = new RegExp(this.options.pattern, 'i');
+ messagesToDelete = messagesToDelete.filter(msg => regex.test(msg.content));
+ } catch (e) {
+ log.warn('Ignoring RegExp because pattern is malformed!', e);
+ }
+
+ // create an array containing everything we skipped. (used to calculate offset for next searches)
+ const skippedMessages = discoveredMessages.filter(msg => !messagesToDelete.find(m => m.id === msg.id));
+
+ this.state._messagesToDelete = messagesToDelete;
+ this.state._skippedMessages = skippedMessages;
+
+ console.log(PREFIX$1, 'filterResponse', this.state);
+ }
+
+ async deleteMessagesFromList() {
+ for (let i = 0; i < this.state._messagesToDelete.length; i++) {
+ const message = this.state._messagesToDelete[i];
+ if (!this.state.running) return log.error('Stopped by you!');
+
+ log.debug(
+ // `${((this.state.delCount + 1) / this.state.grandTotal * 100).toFixed(2)}%`,
+ `[${this.state.delCount + 1}/${this.state.grandTotal}] ` +
+ `${new Date(message.timestamp).toLocaleString()} ` +
+ `${redact(message.author.username + '#' + message.author.discriminator)} ` +
+ `: ${redact(message.content).replace(/\n/g, '↵')} ` +
+ (message.attachments.length ? redact(JSON.stringify(message.attachments)) : ''),
+ `{ID:${redact(message.id)}} `
+ );
+
+ // Delete a single message (with retry)
+ let attempt = 0;
+ while (attempt < this.options.maxAttempt) {
+ const result = await this.deleteMessage(message);
+
+ if (result === 'RETRY') {
+ attempt++;
+ log.verb(`Retrying in ${this.options.deleteDelay}ms... (${attempt}/${this.options.maxAttempt})`);
+ await wait(this.options.deleteDelay);
+ }
+ else break;
+ }
+
+ this.calcEtr();
+ if (this.onProgress) this.onProgress(this.state, this.stats);
+
+ await wait(this.options.deleteDelay);
+ }
+ }
+
+ async deleteMessage(message) {
+ const API_DELETE_URL = `https://discord.com/api/v9/channels/${message.channel_id}/messages/${message.id}`;
+ let resp;
+ try {
+ this.beforeRequest();
+ resp = await fetch(API_DELETE_URL, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': this.options.authToken,
+ },
+ });
+ this.afterRequest();
+ } catch (err) {
+ // no response error (e.g. network error)
+ log.error('Delete request throwed an error:', err);
+ log.verb('Related object:', redact(JSON.stringify(message)));
+ this.state.failCount++;
+ return 'FAILED';
+ }
+
+ if (!resp.ok) {
+ if (resp.status === 429) {
+ // deleting messages too fast
+ const w = (await resp.json()).retry_after * 1000;
+ this.stats.throttledCount++;
+ this.stats.throttledTotalTime += w;
+ this.options.deleteDelay = w; // increase delay
+ log.warn(`Being rate limited by the API for ${w}ms! Adjusted delete delay to ${this.options.deleteDelay}ms.`);
+ this.printStats();
+ log.verb(`Cooling down for ${w * 2}ms before retrying...`);
+ await wait(w * 2);
+ return 'RETRY';
+ } else {
+ const body = await resp.text();
+
+ try {
+ const r = JSON.parse(body);
+
+ if (resp.status === 400 && r.code === 50083) {
+ // 400 can happen if the thread is archived (code=50083)
+ // in this case we need to "skip" this message from the next search
+ // otherwise it will come up again in the next page (and fail to delete again)
+ log.warn('Error deleting message (Thread is archived). Will increment offset so we don\'t search this in the next page...');
+ this.state.offset++;
+ this.state.failCount++;
+ return 'FAIL_SKIP'; // Failed but we will skip it next time
+ }
+
+ log.error(`Error deleting message, API responded with status ${resp.status}!`, r);
+ log.verb('Related object:', redact(JSON.stringify(message)));
+ this.state.failCount++;
+ return 'FAILED';
+ } catch (e) {
+ log.error(`Fail to parse JSON. API responded with status ${resp.status}!`, body);
+ }
+ }
+ }
+
+ this.state.delCount++;
+ return 'OK';
+ }
+
+ #beforeTs = 0; // used to calculate latency
+ beforeRequest() {
+ this.#beforeTs = Date.now();
+ }
+ afterRequest() {
+ this.stats.lastPing = (Date.now() - this.#beforeTs);
+ this.stats.avgPing = this.stats.avgPing > 0 ? (this.stats.avgPing * 0.9) + (this.stats.lastPing * 0.1) : this.stats.lastPing;
+ }
+
+ printStats() {
+ log.verb(
+ `Delete delay: ${this.options.deleteDelay}ms, Search delay: ${this.options.searchDelay}ms`,
+ `Last Ping: ${this.stats.lastPing}ms, Average Ping: ${this.stats.avgPing | 0}ms`,
+ );
+ log.verb(
+ `Rate Limited: ${this.stats.throttledCount} times.`,
+ `Total time throttled: ${msToHMS(this.stats.throttledTotalTime)}.`
+ );
+ }
}
- const MOVE = 0;
- const RESIZE_T = 1;
- const RESIZE_B = 2;
- const RESIZE_L = 4;
- const RESIZE_R = 8;
- const RESIZE_TL = RESIZE_T + RESIZE_L;
- const RESIZE_TR = RESIZE_T + RESIZE_R;
- const RESIZE_BL = RESIZE_B + RESIZE_L;
- const RESIZE_BR = RESIZE_B + RESIZE_R;
-
- /**
- * Make an element draggable/resizable
- * @author Victor N. wwww.vitim.us
- */
- class DragResize {
- constructor({ elm, moveHandle, options }) {
- this.options = defaultArgs({
- enabledDrag: true,
- enabledResize: true,
- minWidth: 200,
- maxWidth: Infinity,
- minHeight: 100,
- maxHeight: Infinity,
- dragAllowX: true,
- dragAllowY: true,
- resizeAllowX: true,
- resizeAllowY: true,
- draggingClass: 'drag',
- useMouseEvents: true,
- useTouchEvents: true,
- createHandlers: true,
- }, options);
- Object.assign(this, options);
- options = undefined;
-
- elm.style.position = 'fixed';
-
- this.drag_m = new Draggable(elm, moveHandle, MOVE, this.options);
-
- if (this.options.createHandlers) {
- this.el_t = createElement('div', { name: 'grab-t' }, elm);
- this.drag_t = new Draggable(elm, this.el_t, RESIZE_T, this.options);
- this.el_r = createElement('div', { name: 'grab-r' }, elm);
- this.drag_r = new Draggable(elm, this.el_r, RESIZE_R, this.options);
- this.el_b = createElement('div', { name: 'grab-b' }, elm);
- this.drag_b = new Draggable(elm, this.el_b, RESIZE_B, this.options);
- this.el_l = createElement('div', { name: 'grab-l' }, elm);
- this.drag_l = new Draggable(elm, this.el_l, RESIZE_L, this.options);
- this.el_tl = createElement('div', { name: 'grab-tl' }, elm);
- this.drag_tl = new Draggable(elm, this.el_tl, RESIZE_TL, this.options);
- this.el_tr = createElement('div', { name: 'grab-tr' }, elm);
- this.drag_tr = new Draggable(elm, this.el_tr, RESIZE_TR, this.options);
- this.el_br = createElement('div', { name: 'grab-br' }, elm);
- this.drag_br = new Draggable(elm, this.el_br, RESIZE_BR, this.options);
- this.el_bl = createElement('div', { name: 'grab-bl' }, elm);
- this.drag_bl = new Draggable(elm, this.el_bl, RESIZE_BL, this.options);
- }
- }
+ const MOVE = 0;
+ const RESIZE_T = 1;
+ const RESIZE_B = 2;
+ const RESIZE_L = 4;
+ const RESIZE_R = 8;
+ const RESIZE_TL = RESIZE_T + RESIZE_L;
+ const RESIZE_TR = RESIZE_T + RESIZE_R;
+ const RESIZE_BL = RESIZE_B + RESIZE_L;
+ const RESIZE_BR = RESIZE_B + RESIZE_R;
+
+ /**
+ * Make an element draggable/resizable
+ * @author Victor N. wwww.vitim.us
+ */
+ class DragResize {
+ constructor({ elm, moveHandle, options }) {
+ this.options = defaultArgs({
+ enabledDrag: true,
+ enabledResize: true,
+ minWidth: 200,
+ maxWidth: Infinity,
+ minHeight: 100,
+ maxHeight: Infinity,
+ dragAllowX: true,
+ dragAllowY: true,
+ resizeAllowX: true,
+ resizeAllowY: true,
+ draggingClass: 'drag',
+ useMouseEvents: true,
+ useTouchEvents: true,
+ createHandlers: true,
+ }, options);
+ Object.assign(this, options);
+ options = undefined;
+
+ elm.style.position = 'fixed';
+
+ this.drag_m = new Draggable(elm, moveHandle, MOVE, this.options);
+
+ if (this.options.createHandlers) {
+ this.el_t = createElement('div', { name: 'grab-t' }, elm);
+ this.drag_t = new Draggable(elm, this.el_t, RESIZE_T, this.options);
+ this.el_r = createElement('div', { name: 'grab-r' }, elm);
+ this.drag_r = new Draggable(elm, this.el_r, RESIZE_R, this.options);
+ this.el_b = createElement('div', { name: 'grab-b' }, elm);
+ this.drag_b = new Draggable(elm, this.el_b, RESIZE_B, this.options);
+ this.el_l = createElement('div', { name: 'grab-l' }, elm);
+ this.drag_l = new Draggable(elm, this.el_l, RESIZE_L, this.options);
+ this.el_tl = createElement('div', { name: 'grab-tl' }, elm);
+ this.drag_tl = new Draggable(elm, this.el_tl, RESIZE_TL, this.options);
+ this.el_tr = createElement('div', { name: 'grab-tr' }, elm);
+ this.drag_tr = new Draggable(elm, this.el_tr, RESIZE_TR, this.options);
+ this.el_br = createElement('div', { name: 'grab-br' }, elm);
+ this.drag_br = new Draggable(elm, this.el_br, RESIZE_BR, this.options);
+ this.el_bl = createElement('div', { name: 'grab-bl' }, elm);
+ this.drag_bl = new Draggable(elm, this.el_bl, RESIZE_BL, this.options);
+ }
+ }
+ }
+
+ class Draggable {
+ constructor(targetElm, handleElm, op, options) {
+ Object.assign(this, options);
+ options = undefined;
+
+ this._targetElm = targetElm;
+ this._handleElm = handleElm;
+
+ let vw = window.innerWidth;
+ let vh = window.innerHeight;
+ let initialX, initialY, initialT, initialL, initialW, initialH;
+
+ const clamp = (value, min, max) => value < min ? min : value > max ? max : value;
+
+ const moveOp = (x, y) => {
+ const deltaX = (x - initialX);
+ const deltaY = (y - initialY);
+ const t = clamp(initialT + deltaY, 0, vh - initialH);
+ const l = clamp(initialL + deltaX, 0, vw - initialW);
+ this._targetElm.style.top = t + 'px';
+ this._targetElm.style.left = l + 'px';
+ };
+
+ const resizeOp = (x, y) => {
+ x = clamp(x, 0, vw);
+ y = clamp(y, 0, vh);
+ const deltaX = (x - initialX);
+ const deltaY = (y - initialY);
+ const resizeDirX = (op & RESIZE_L) ? -1 : 1;
+ const resizeDirY = (op & RESIZE_T) ? -1 : 1;
+ const deltaXMax = (this.maxWidth - initialW);
+ const deltaXMin = (this.minWidth - initialW);
+ const deltaYMax = (this.maxHeight - initialH);
+ const deltaYMin = (this.minHeight - initialH);
+ const t = initialT + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax) * resizeDirY;
+ const l = initialL + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax) * resizeDirX;
+ const w = initialW + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax);
+ const h = initialH + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax);
+ if (op & RESIZE_T) { // resize ↑
+ this._targetElm.style.top = t + 'px';
+ this._targetElm.style.height = h + 'px';
+ }
+ if (op & RESIZE_B) { // resize ↓
+ this._targetElm.style.height = h + 'px';
+ }
+ if (op & RESIZE_L) { // resize ←
+ this._targetElm.style.left = l + 'px';
+ this._targetElm.style.width = w + 'px';
+ }
+ if (op & RESIZE_R) { // resize →
+ this._targetElm.style.width = w + 'px';
+ }
+ };
+
+ let operation = op === MOVE ? moveOp : resizeOp;
+
+ 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;
+ initialX = x;
+ initialY = y;
+ vw = window.innerWidth;
+ vh = window.innerHeight;
+ initialT = this._targetElm.offsetTop;
+ initialL = this._targetElm.offsetLeft;
+ initialW = this._targetElm.clientWidth;
+ initialH = this._targetElm.clientHeight;
+ if (this.useMouseEvents) {
+ document.addEventListener('mousemove', this._dragMoveHandler);
+ document.addEventListener('mouseup', this._dragEndHandler);
+ }
+ if (this.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;
+ }
+ // perform drag / resize operation
+ operation(x, y);
+ }
+
+ function dragEndHandler(e) {
+ if (this.useMouseEvents) {
+ document.removeEventListener('mousemove', this._dragMoveHandler);
+ document.removeEventListener('mouseup', this._dragEndHandler);
+ }
+ if (this.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
+ this._dragStartHandler = dragStartHandler.bind(this);
+ this._dragMoveHandler = dragMoveHandler.bind(this);
+ this._dragEndHandler = dragEndHandler.bind(this);
+
+ this.enable();
+ }
+
+ /** Turn on the drag and drop of the instance */
+ enable() {
+ this.destroy(); // prevent events from getting binded twice
+ if (this.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler);
+ if (this.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() */
+ destroy() {
+ this._targetElm.classList.remove(this.draggingClass);
+ if (this.useMouseEvents) {
+ this._handleElm.removeEventListener('mousedown', this._dragStartHandler);
+ document.removeEventListener('mousemove', this._dragMoveHandler);
+ document.removeEventListener('mouseup', this._dragEndHandler);
+ }
+ if (this.useTouchEvents) {
+ this._handleElm.removeEventListener('touchstart', this._dragStartHandler);
+ document.removeEventListener('touchmove', this._dragMoveHandler);
+ document.removeEventListener('touchend', this._dragEndHandler);
+ }
+ }
+ }
+
+ function createElement(tag='div', attrs, parent) {
+ const elm = document.createElement(tag);
+ if (attrs) Object.entries(attrs).forEach(([k, v]) => elm.setAttribute(k, v));
+ if (parent) parent.appendChild(elm);
+ return elm;
+ }
+
+ function defaultArgs(defaults, options) {
+ function isObj(x) { return x !== null && typeof x === 'object'; }
+ function hasOwn(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
+ if (isObj(options)) for (let prop in defaults) {
+ if (hasOwn(defaults, prop) && hasOwn(options, prop) && options[prop] !== undefined) {
+ if (isObj(defaults[prop])) defaultArgs(defaults[prop], options[prop]);
+ else defaults[prop] = options[prop];
+ }
+ }
+ return defaults;
}
- class Draggable {
- constructor(targetElm, handleElm, op, options) {
- Object.assign(this, options);
- options = undefined;
-
- this._targetElm = targetElm;
- this._handleElm = handleElm;
-
- let vw = window.innerWidth;
- let vh = window.innerHeight;
- let initialX, initialY, initialT, initialL, initialW, initialH;
-
- const clamp = (value, min, max) => value < min ? min : value > max ? max : value;
-
- const moveOp = (x, y) => {
- const deltaX = (x - initialX);
- const deltaY = (y - initialY);
- const t = clamp(initialT + deltaY, 0, vh - initialH);
- const l = clamp(initialL + deltaX, 0, vw - initialW);
- this._targetElm.style.top = t + 'px';
- this._targetElm.style.left = l + 'px';
- };
-
- const resizeOp = (x, y) => {
- x = clamp(x, 0, vw);
- y = clamp(y, 0, vh);
- const deltaX = (x - initialX);
- const deltaY = (y - initialY);
- const resizeDirX = (op & RESIZE_L) ? -1 : 1;
- const resizeDirY = (op & RESIZE_T) ? -1 : 1;
- const deltaXMax = (this.maxWidth - initialW);
- const deltaXMin = (this.minWidth - initialW);
- const deltaYMax = (this.maxHeight - initialH);
- const deltaYMin = (this.minHeight - initialH);
- const t = initialT + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax) * resizeDirY;
- const l = initialL + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax) * resizeDirX;
- const w = initialW + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax);
- const h = initialH + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax);
- if (op & RESIZE_T) { // resize ↑
- this._targetElm.style.top = t + 'px';
- this._targetElm.style.height = h + 'px';
- }
- if (op & RESIZE_B) { // resize ↓
- this._targetElm.style.height = h + 'px';
- }
- if (op & RESIZE_L) { // resize ←
- this._targetElm.style.left = l + 'px';
- this._targetElm.style.width = w + 'px';
- }
- if (op & RESIZE_R) { // resize →
- this._targetElm.style.width = w + 'px';
- }
- };
-
- let operation = op === MOVE ? moveOp : resizeOp;
-
- 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;
- initialX = x;
- initialY = y;
- vw = window.innerWidth;
- vh = window.innerHeight;
- initialT = this._targetElm.offsetTop;
- initialL = this._targetElm.offsetLeft;
- initialW = this._targetElm.clientWidth;
- initialH = this._targetElm.clientHeight;
- if (this.useMouseEvents) {
- document.addEventListener('mousemove', this._dragMoveHandler);
- document.addEventListener('mouseup', this._dragEndHandler);
- }
- if (this.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;
- }
- // perform drag / resize operation
- operation(x, y);
- }
-
- function dragEndHandler(e) {
- if (this.useMouseEvents) {
- document.removeEventListener('mousemove', this._dragMoveHandler);
- document.removeEventListener('mouseup', this._dragEndHandler);
- }
- if (this.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
- this._dragStartHandler = dragStartHandler.bind(this);
- this._dragMoveHandler = dragMoveHandler.bind(this);
- this._dragEndHandler = dragEndHandler.bind(this);
-
- this.enable();
- }
-
- /** Turn on the drag and drop of the instance */
- enable() {
- this.destroy(); // prevent events from getting binded twice
- if (this.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler);
- if (this.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() */
- destroy() {
- this._targetElm.classList.remove(this.draggingClass);
- if (this.useMouseEvents) {
- this._handleElm.removeEventListener('mousedown', this._dragStartHandler);
- document.removeEventListener('mousemove', this._dragMoveHandler);
- document.removeEventListener('mouseup', this._dragEndHandler);
- }
- if (this.useTouchEvents) {
- this._handleElm.removeEventListener('touchstart', this._dragStartHandler);
- document.removeEventListener('touchmove', this._dragMoveHandler);
- document.removeEventListener('touchend', this._dragEndHandler);
- }
- }
- }
-
- function createElement(tag='div', attrs, parent) {
- const elm = document.createElement(tag);
- if (attrs) Object.entries(attrs).forEach(([k, v]) => elm.setAttribute(k, v));
- if (parent) parent.appendChild(elm);
- return elm;
- }
-
- function defaultArgs(defaults, options) {
- function isObj(x) { return x !== null && typeof x === 'object'; }
- function hasOwn(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
- if (isObj(options)) for (let prop in defaults) {
- if (hasOwn(defaults, prop) && hasOwn(options, prop) && options[prop] !== undefined) {
- if (isObj(defaults[prop])) defaultArgs(defaults[prop], options[prop]);
- else defaults[prop] = options[prop];
- }
- }
- return defaults;
- }
-
- function createElm(html) {
- const temp = document.createElement('div');
- temp.innerHTML = html;
- return temp.removeChild(temp.firstElementChild);
+ 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;
+ 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);
- });
- }
- };
+ 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);
+ });
+ }
+ };
window.messagePicker = messagePicker;
- function getToken() {
- window.dispatchEvent(new Event('beforeunload'));
- const LS = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
- try {
- return JSON.parse(LS.token);
- } catch {
- log.info('Could not automatically detect Authorization Token in local storage!');
- log.info('Attempting to grab token using webpack');
- return (window.webpackChunkdiscord_app.push([[''], {}, e => { window.m = []; for (let c in e.c) window.m.push(e.c[c]); }]), window.m).find(m => m?.exports?.default?.getToken !== void 0).exports.default.getToken();
- }
- }
-
- 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 find 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 find the Channel ID!\nPlease make sure you are on a Channel or DM.');
- }
-
- function fillToken() {
- try {
- return getToken();
- } catch (err) {
- log.verb(err);
- log.error('Could not automatically detect Authorization Token!');
- log.info('Please make sure Undiscord is up to date');
- log.debug('Alternatively, you can try entering a Token manually in the "Advanced Settings" section.');
- }
- return '';
- }
-
- const PREFIX = '[UNDISCORD]';
-
- // -------------------------- User interface ------------------------------- //
-
- // links
- const HOME = 'https://github.com/victornpb/undiscord';
- const WIKI = 'https://github.com/victornpb/undiscord/wiki';
-
- const undiscordCore = new UndiscordCore();
- messagePicker.init();
-
- const ui = {
- undiscordWindow: null,
- undiscordBtn: null,
- logArea: null,
- autoScroll: null,
-
- // progress handler
- progressMain: null,
- progressIcon: null,
- percent: null,
- };
- const $ = s => ui.undiscordWindow.querySelector(s);
-
- function initUI() {
-
- insertCss(themeCss);
- insertCss(mainCss);
- insertCss(dragCss);
-
- // create undiscord window
- const undiscordUI = replaceInterpolations(undiscordTemplate, {
- VERSION,
- HOME,
- WIKI,
- });
- ui.undiscordWindow = createElm(undiscordUI);
- document.body.appendChild(ui.undiscordWindow);
-
- // enable drag and resize on undiscord window
- new DragResize({ elm: ui.undiscordWindow, moveHandle: $('.header') });
-
- // create undiscord Trash icon
- ui.undiscordBtn = createElm(buttonHtml);
- ui.undiscordBtn.onclick = toggleWindow;
- function mountBtn() {
- const toolbar = document.querySelector('#app-mount [class*="-toolbar"]');
- if (toolbar) toolbar.appendChild(ui.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(ui.undiscordBtn)) mountBtn(); // re-mount the button to the toolbar
- }, 3000);
- });
- observer.observe(discordElm, { attributes: false, childList: true, subtree: true });
-
- function toggleWindow() {
- if (ui.undiscordWindow.style.display !== 'none') {
- ui.undiscordWindow.style.display = 'none';
- ui.undiscordBtn.style.color = 'var(--interactive-normal)';
- }
- else {
- ui.undiscordWindow.style.display = '';
- ui.undiscordBtn.style.color = 'var(--interactive-active)';
- }
- }
-
- // cached elements
- ui.logArea = $('#logArea');
- ui.autoScroll = $('#autoScroll');
- ui.progressMain = $('#progressBar');
- ui.progressIcon = ui.undiscordBtn.querySelector('progress');
- ui.percent = $('#progressPercent');
-
- // register event listeners
- $('#hide').onclick = toggleWindow;
- $('#toggleSidebar').onclick = ()=> ui.undiscordWindow.classList.toggle('hide-sidebar');
- $('button#start').onclick = startAction;
- $('button#stop').onclick = stopAction;
- $('button#clear').onclick = () => ui.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 = ui.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.');
- toggleWindow();
- const id = await messagePicker.grab('after');
- if (id) $('input#minId').value = id;
- toggleWindow();
- };
- $('#pickMessageBefore').onclick = async () => {
- alert('Select a message on the chat.\nThe message above it will be deleted.');
- toggleWindow();
- const id = await messagePicker.grab('before');
- if (id) $('input#maxId').value = id;
- toggleWindow();
- };
- $('button#getToken').onclick = () => $('input#token').value = fillToken();
-
- // sync delays
- $('input#searchDelay').onchange = (e) => {
- const v = parseInt(e.target.value);
- if (v) undiscordCore.options.searchDelay = v;
- };
- $('input#deleteDelay').onchange = (e) => {
- const v = parseInt(e.target.value);
- if (v) undiscordCore.options.deleteDelay = v;
- };
-
- $('input#searchDelay').addEventListener('input', (event) => {
- $('div#searchDelayValue').textContent = event.target.value + 'ms';
- });
- $('input#deleteDelay').addEventListener('input', (event) => {
- $('div#deleteDelayValue').textContent = event.target.value + 'ms';
- });
-
- // import json
- const fileSelection = $('input#importJsonInput');
- fileSelection.onchange = async () => {
- const files = fileSelection.files;
-
- // No files added
- if (files.length === 0) return log.warn('No file selected.');
-
- // Get channel id field to set it later
- const channelIdField = $('input#channelId');
-
- // Force the guild id to be ourself (@me)
- const guildIdField = $('input#guildId');
- guildIdField.value = '@me';
-
- // Set author id in case its not set already
- $('input#authorId').value = getAuthorId();
- try {
- const file = files[0];
- const text = await file.text();
- const json = JSON.parse(text);
- const channelIds = Object.keys(json);
- channelIdField.value = channelIds.join(',');
- log.info(`Loaded ${channelIds.length} channels.`);
- } catch(err) {
- log.error('Error parsing file!', err);
- }
- };
-
- // redirect console logs to inside the window after setting up the UI
- setLogFn(printLog);
-
- setupUndiscordCore();
- }
-
- function printLog(type = '', args) {
- ui.logArea.insertAdjacentHTML('beforeend', `${Array.from(args).map(o => typeof o === 'object' ? JSON.stringify(o, o instanceof Error && Object.getOwnPropertyNames(o)) : o).join('\t')}
`);
- if (ui.autoScroll.checked) ui.logArea.querySelector('div:last-child').scrollIntoView(false);
- if (type==='error') console.error(PREFIX, ...Array.from(args));
- }
-
- function setupUndiscordCore() {
-
- undiscordCore.onStart = (state, stats) => {
- console.log(PREFIX, 'onStart', state, stats);
- $('#start').disabled = true;
- $('#stop').disabled = false;
-
- ui.undiscordBtn.classList.add('running');
- ui.progressMain.style.display = 'block';
- ui.percent.style.display = 'block';
- };
-
- undiscordCore.onProgress = (state, stats) => {
- // console.log(PREFIX, 'onProgress', state, stats);
- let max = state.grandTotal;
- const value = state.delCount + state.failCount;
- max = Math.max(max, value, 0); // clamp max
-
- // status bar
- const percent = value >= 0 && max ? Math.round(value / max * 100) + '%' : '';
- const elapsed = msToHMS(Date.now() - stats.startTime.getTime());
- const remaining = msToHMS(stats.etr);
- ui.percent.innerHTML = `${percent} (${value}/${max}) Elapsed: ${elapsed} Remaining: ${remaining}`;
-
- ui.progressIcon.value = value;
- ui.progressMain.value = value;
-
- // indeterminate progress bar
- if (max) {
- ui.progressIcon.setAttribute('max', max);
- ui.progressMain.setAttribute('max', max);
- } else {
- ui.progressIcon.removeAttribute('value');
- ui.progressMain.removeAttribute('value');
- ui.percent.innerHTML = '...';
- }
-
- // update delays
- const searchDelayInput = $('input#searchDelay');
- searchDelayInput.value = undiscordCore.options.searchDelay;
- $('div#searchDelayValue').textContent = undiscordCore.options.searchDelay+'ms';
-
- const deleteDelayInput = $('input#deleteDelay');
- deleteDelayInput.value = undiscordCore.options.deleteDelay;
- $('div#deleteDelayValue').textContent = undiscordCore.options.deleteDelay+'ms';
- };
-
- undiscordCore.onStop = (state, stats) => {
- console.log(PREFIX, 'onStop', state, stats);
- $('#start').disabled = false;
- $('#stop').disabled = true;
- ui.undiscordBtn.classList.remove('running');
- ui.progressMain.style.display = 'none';
- ui.percent.style.display = 'none';
- };
- }
-
- async function startAction() {
- console.log(PREFIX, 'startAction');
- // general
- 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());
-
- // token
- const authToken = $('input#token').value.trim() || fillToken();
- if (!authToken) return; // get token already logs an error.
-
- // validate input
- if (!guildId) return log.error('You must fill the "Server ID" field!');
-
- // clear logArea
- ui.logArea.innerHTML = '';
-
- undiscordCore.resetState();
- undiscordCore.options = {
- ...undiscordCore.options,
- authToken,
- authorId,
- guildId,
- channelId: channelIds.length === 1 ? channelIds[0] : undefined, // single or multiple channel
- minId: minId || minDate,
- maxId: maxId || maxDate,
- content,
- hasLink,
- hasFile,
- includeNsfw,
- includePinned,
- pattern,
- searchDelay,
- deleteDelay,
- // maxAttempt: 2,
- };
- if (channelIds.length > 1) {
- const jobs = channelIds.map(ch => ({
- guildId: guildId,
- channelId: ch,
- }));
-
- try {
- await undiscordCore.runBatch(jobs);
- } catch (err) {
- log.error('CoreException', err);
- }
- }
- // single channel
- else {
- try {
- await undiscordCore.run();
- } catch (err) {
- log.error('CoreException', err);
- undiscordCore.stop();
- }
- }
- }
-
- function stopAction() {
- console.log(PREFIX, 'stopAction');
- undiscordCore.stop();
+ function getToken() {
+ window.dispatchEvent(new Event('beforeunload'));
+ const LS = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
+ try {
+ return JSON.parse(LS.token);
+ } catch {
+ log.info('Could not automatically detect Authorization Token in local storage!');
+ log.info('Attempting to grab token using webpack');
+ return (window.webpackChunkdiscord_app.push([[''], {}, e => { window.m = []; for (let c in e.c) window.m.push(e.c[c]); }]), window.m).find(m => m?.exports?.default?.getToken !== void 0).exports.default.getToken();
+ }
+ }
+
+ 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 find 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 find the Channel ID!\nPlease make sure you are on a Channel or DM.');
+ }
+
+ function fillToken() {
+ try {
+ return getToken();
+ } catch (err) {
+ log.verb(err);
+ log.error('Could not automatically detect Authorization Token!');
+ log.info('Please make sure Undiscord is up to date');
+ log.debug('Alternatively, you can try entering a Token manually in the "Advanced Settings" section.');
+ }
+ return '';
}
+ const PREFIX = '[UNDISCORD]';
+
+ // -------------------------- User interface ------------------------------- //
+
+ // links
+ const HOME = 'https://github.com/victornpb/undiscord';
+ const WIKI = 'https://github.com/victornpb/undiscord/wiki';
+
+ const undiscordCore = new UndiscordCore();
+ messagePicker.init();
+
+ const ui = {
+ undiscordWindow: null,
+ undiscordBtn: null,
+ logArea: null,
+ autoScroll: null,
+
+ // progress handler
+ progressMain: null,
+ progressIcon: null,
+ percent: null,
+ };
+ const $ = s => ui.undiscordWindow.querySelector(s);
+
+ function initUI() {
+
+ insertCss(themeCss);
+ insertCss(mainCss);
+ insertCss(dragCss);
+
+ // create undiscord window
+ const undiscordUI = replaceInterpolations(undiscordTemplate, {
+ VERSION,
+ HOME,
+ WIKI,
+ });
+ ui.undiscordWindow = createElm(undiscordUI);
+ document.body.appendChild(ui.undiscordWindow);
+
+ // enable drag and resize on undiscord window
+ new DragResize({ elm: ui.undiscordWindow, moveHandle: $('.header') });
+
+ // create undiscord Trash icon
+ ui.undiscordBtn = createElm(buttonHtml);
+ ui.undiscordBtn.onclick = toggleWindow;
+ function mountBtn() {
+ const toolbar = document.querySelector('#app-mount [class*="toolbar__"]');
+ if (toolbar) toolbar.appendChild(ui.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(ui.undiscordBtn)) mountBtn(); // re-mount the button to the toolbar
+ }, 3000);
+ });
+ observer.observe(discordElm, { attributes: false, childList: true, subtree: true });
+
+ function toggleWindow() {
+ if (ui.undiscordWindow.style.display !== 'none') {
+ ui.undiscordWindow.style.display = 'none';
+ ui.undiscordBtn.style.color = 'var(--scrollbar-thin-thumb)';
+ }
+ else {
+ ui.undiscordWindow.style.display = '';
+ ui.undiscordBtn.style.color = 'var(--interactive-text-hover)';
+ }
+ }
+
+ // cached elements
+ ui.logArea = $('#logArea');
+ ui.autoScroll = $('#autoScroll');
+ ui.progressMain = $('#progressBar');
+ ui.progressIcon = ui.undiscordBtn.querySelector('progress');
+ ui.percent = $('#progressPercent');
+
+ // register event listeners
+ $('#hide').onclick = toggleWindow;
+ $('#toggleSidebar').onclick = ()=> ui.undiscordWindow.classList.toggle('hide-sidebar');
+ $('button#start').onclick = startAction;
+ $('button#stop').onclick = stopAction;
+ $('button#clear').onclick = () => ui.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 = ui.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.');
+ toggleWindow();
+ const id = await messagePicker.grab('after');
+ if (id) $('input#minId').value = id;
+ toggleWindow();
+ };
+ $('#pickMessageBefore').onclick = async () => {
+ alert('Select a message on the chat.\nThe message above it will be deleted.');
+ toggleWindow();
+ const id = await messagePicker.grab('before');
+ if (id) $('input#maxId').value = id;
+ toggleWindow();
+ };
+ $('button#getToken').onclick = () => $('input#token').value = fillToken();
+
+ // sync delays
+ $('input#searchDelay').onchange = (e) => {
+ const v = parseInt(e.target.value);
+ if (v) undiscordCore.options.searchDelay = v;
+ };
+ $('input#deleteDelay').onchange = (e) => {
+ const v = parseInt(e.target.value);
+ if (v) undiscordCore.options.deleteDelay = v;
+ };
+
+ $('input#searchDelay').addEventListener('input', (event) => {
+ $('div#searchDelayValue').textContent = event.target.value + 'ms';
+ });
+ $('input#deleteDelay').addEventListener('input', (event) => {
+ $('div#deleteDelayValue').textContent = event.target.value + 'ms';
+ });
+
+ // import json
+ const fileSelection = $('input#importJsonInput');
+ fileSelection.onchange = async () => {
+ const files = fileSelection.files;
+
+ // No files added
+ if (files.length === 0) return log.warn('No file selected.');
+
+ // Get channel id field to set it later
+ const channelIdField = $('input#channelId');
+
+ // Force the guild id to be ourself (@me)
+ const guildIdField = $('input#guildId');
+ guildIdField.value = '@me';
+
+ // Set author id in case its not set already
+ $('input#authorId').value = getAuthorId();
+ try {
+ const file = files[0];
+ const text = await file.text();
+ const json = JSON.parse(text);
+ const channelIds = Object.keys(json);
+ channelIdField.value = channelIds.join(',');
+ log.info(`Loaded ${channelIds.length} channels.`);
+ } catch(err) {
+ log.error('Error parsing file!', err);
+ }
+ };
+
+ // redirect console logs to inside the window after setting up the UI
+ setLogFn(printLog);
+
+ setupUndiscordCore();
+ }
+
+ function printLog(type = '', args) {
+ ui.logArea.insertAdjacentHTML('beforeend', `${Array.from(args).map(o => typeof o === 'object' ? JSON.stringify(o, o instanceof Error && Object.getOwnPropertyNames(o)) : o).join('\t')}
`);
+ if (ui.autoScroll.checked) ui.logArea.querySelector('div:last-child').scrollIntoView(false);
+ if (type==='error') console.error(PREFIX, ...Array.from(args));
+ }
+
+ function setupUndiscordCore() {
+
+ undiscordCore.onStart = (state, stats) => {
+ console.log(PREFIX, 'onStart', state, stats);
+ $('#start').disabled = true;
+ $('#stop').disabled = false;
+
+ ui.undiscordBtn.classList.add('running');
+ ui.progressMain.style.display = 'block';
+ ui.percent.style.display = 'block';
+ };
+
+ undiscordCore.onProgress = (state, stats) => {
+ // console.log(PREFIX, 'onProgress', state, stats);
+ let max = state.grandTotal;
+ const value = state.delCount + state.failCount;
+ max = Math.max(max, value, 0); // clamp max
+
+ // status bar
+ const percent = value >= 0 && max ? Math.round(value / max * 100) + '%' : '';
+ const elapsed = msToHMS(Date.now() - stats.startTime.getTime());
+ const remaining = msToHMS(stats.etr);
+ ui.percent.innerHTML = `${percent} (${value}/${max}) Elapsed: ${elapsed} Remaining: ${remaining}`;
+
+ ui.progressIcon.value = value;
+ ui.progressMain.value = value;
+
+ // indeterminate progress bar
+ if (max) {
+ ui.progressIcon.setAttribute('max', max);
+ ui.progressMain.setAttribute('max', max);
+ } else {
+ ui.progressIcon.removeAttribute('value');
+ ui.progressMain.removeAttribute('value');
+ ui.percent.innerHTML = '...';
+ }
+
+ // update delays
+ const searchDelayInput = $('input#searchDelay');
+ searchDelayInput.value = undiscordCore.options.searchDelay;
+ $('div#searchDelayValue').textContent = undiscordCore.options.searchDelay+'ms';
+
+ const deleteDelayInput = $('input#deleteDelay');
+ deleteDelayInput.value = undiscordCore.options.deleteDelay;
+ $('div#deleteDelayValue').textContent = undiscordCore.options.deleteDelay+'ms';
+ };
+
+ undiscordCore.onStop = (state, stats) => {
+ console.log(PREFIX, 'onStop', state, stats);
+ $('#start').disabled = false;
+ $('#stop').disabled = true;
+ ui.undiscordBtn.classList.remove('running');
+ ui.progressMain.style.display = 'none';
+ ui.percent.style.display = 'none';
+ };
+ }
+
+ async function startAction() {
+ console.log(PREFIX, 'startAction');
+ // general
+ 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());
+
+ // token
+ const authToken = $('input#token').value.trim() || fillToken();
+ if (!authToken) return; // get token already logs an error.
+
+ // validate input
+ if (!guildId) return log.error('You must fill the "Server ID" field!');
+
+ // clear logArea
+ ui.logArea.innerHTML = '';
+
+ undiscordCore.resetState();
+ undiscordCore.options = {
+ ...undiscordCore.options,
+ authToken,
+ authorId,
+ guildId,
+ channelId: channelIds.length === 1 ? channelIds[0] : undefined, // single or multiple channel
+ minId: minId || minDate,
+ maxId: maxId || maxDate,
+ content,
+ hasLink,
+ hasFile,
+ includeNsfw,
+ includePinned,
+ pattern,
+ searchDelay,
+ deleteDelay,
+ // maxAttempt: 2,
+ };
+ if (channelIds.length > 1) {
+ const jobs = channelIds.map(ch => ({
+ guildId: guildId,
+ channelId: ch,
+ }));
+
+ try {
+ await undiscordCore.runBatch(jobs);
+ } catch (err) {
+ log.error('CoreException', err);
+ }
+ }
+ // single channel
+ else {
+ try {
+ await undiscordCore.run();
+ } catch (err) {
+ log.error('CoreException', err);
+ undiscordCore.stop();
+ }
+ }
+ }
+
+ function stopAction() {
+ console.log(PREFIX, 'stopAction');
+ undiscordCore.stop();
+ }
+
// ---- END Undiscord ----
initUI();
diff --git a/package-lock.json b/package-lock.json
index 5ae00a05..38b71dcc 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -248,10 +248,11 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -336,22 +337,24 @@
"dev": true
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -482,10 +485,11 @@
}
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -761,10 +765,11 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -817,10 +822,11 @@
}
},
"node_modules/flatted": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
- "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
- "dev": true
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/fs.realpath": {
"version": "1.0.0",
@@ -1073,6 +1079,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -1124,10 +1131,11 @@
}
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -1421,12 +1429,13 @@
"dev": true
},
"node_modules/micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "braces": "^3.0.2",
+ "braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -1458,10 +1467,11 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -1692,10 +1702,11 @@
}
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8.6"
},
@@ -1851,10 +1862,11 @@
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -1878,12 +1890,13 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
- "version": "7.4.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.3.tgz",
- "integrity": "sha512-5UB4yYusDtkRPbRiy1cqZ1IpGNcJCGlEMG17RKzPddpyiPKoCdwohbED8g4QXT0ewCt8LTkQXuljsUfQ3FKM4A==",
+ "version": "7.4.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz",
+ "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==",
"dev": true,
+ "license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=10"
@@ -1893,10 +1906,11 @@
}
},
"node_modules/rollup": {
- "version": "3.20.2",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz",
- "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==",
+ "version": "3.30.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz",
+ "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==",
"dev": true,
+ "license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -2233,6 +2247,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -2295,10 +2310,11 @@
}
},
"node_modules/word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
- "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -2356,12 +2372,19 @@
"dev": true
},
"node_modules/yaml": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
- "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"dev": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
"engines": {
- "node": ">= 14"
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yocto-queue": {
diff --git a/package.json b/package.json
index 503d7941..60ca0a91 100755
--- a/package.json
+++ b/package.json
@@ -29,6 +29,9 @@
},
"homepage": "https://github.com/victornpb/undiscord",
"author": "victornpb",
+ "contributors": [
+ "dekooah"
+ ],
"main": "deleteDiscordMessages.user.js",
"scripts": {
"start": "npm run watch",
diff --git a/src/ui/main.css b/src/ui/main.css
index 0b19168d..865e47e1 100644
--- a/src/ui/main.css
+++ b/src/ui/main.css
@@ -5,7 +5,7 @@
height: 24px;
margin: 0 8px;
cursor: pointer;
- color: var(--interactive-normal);
+ color: var(--scrollbar-thin-thumb);
flex: 0 0 auto;
}
@@ -19,7 +19,7 @@
}
#undicord-btn.running {
- color: var(--button-danger-background) !important;
+ color: var(--control-critical-primary-background-default) !important;
}
#undicord-btn.running progress {
@@ -40,7 +40,7 @@
max-width: 100vw;
min-height: 448px;
max-height: 100vh;
- color: var(--text-normal);
+ color: var(--text-default);
border-radius: 4px;
background-color: var(--background-secondary);
box-shadow: var(--elevation-stroke), var(--elevation-high);
@@ -143,9 +143,15 @@
cursor: pointer;
white-space: nowrap;
text-overflow: ellipsis;
- color: var(--interactive-normal);
+ color: var(--scrollbar-thin-thumb);
border-radius: 4px;
flex-shrink: 0;
+
+ transition: color .3s ease;
+}
+
+#undiscord summary:hover {
+ color: var(--interactive-text-hover);
}
#undiscord fieldset {
diff --git a/src/ui/theme.css b/src/ui/theme.css
index 281e0163..4589715b 100644
--- a/src/ui/theme.css
+++ b/src/ui/theme.css
@@ -21,20 +21,30 @@
min-height: 48px;
padding: 0 16px;
display: flex;
- color: var(--header-secondary);
+ color: var(--text-default);
cursor: grab;
}
+#undiscord.drag .header {
+ cursor: grabbing;
+}
+
#undiscord .header .icon {
- color: var(--interactive-normal);
+ color: var(--scrollbar-thin-thumb);
margin-right: 8px;
flex-shrink: 0;
width: 24;
height: 24;
+
+ transition: color .3s ease;
}
-#undiscord .header .icon:hover {
- color: var(--interactive-hover);
+#undiscord .header .logo {
+ color: var(--text-default);
+}
+
+#undiscord .header .disbtn:hover {
+ color: var(--interactive-text-hover);
}
#undiscord .header h3 {
@@ -42,7 +52,7 @@
line-height: 20px;
font-weight: 500;
font-family: var(--font-display);
- color: var(--header-primary);
+ color: var(--text-default);
flex-shrink: 0;
margin-right: 16px;
}
@@ -54,14 +64,14 @@
#undiscord .header .vert-divider {
width: 1px;
height: 24px;
- background-color: var(--background-modifier-accent);
+ background-color: var(--background-base-lower);
margin-right: 16px;
flex-shrink: 0;
}
#undiscord legend,
#undiscord label {
- color: var(--header-secondary);
+ color: var(--scrollbar-thin-thumb);
font-size: 12px;
line-height: 16px;
font-weight: 500;
@@ -69,6 +79,12 @@
cursor: default;
font-family: var(--font-display);
margin-bottom: 8px;
+
+ transition: color .3s ease;
+}
+#undiscord legend:hover,
+#undiscord label:hover{
+ color: var(--interactive-text-hover);
}
#undiscord .multiInput {
@@ -79,7 +95,7 @@
width: 100%;
border-radius: 3px;
color: var(--text-default);
- background-color: var(--input-background);
+ background-color: var(--input-background-default);
border: none;
transition: border-color 0.2s ease-in-out 0s;
}
@@ -99,8 +115,8 @@
padding: 10px;
height: 44px;
- background-color: var(--input-background);
- border: 1px solid var(--input-border);
+ background-color: var(--input-background-default);
+ border: 1px solid var(--input-border-default);
border-radius: 8px;
box-sizing: border-box;
color: var(--text-default);
@@ -118,9 +134,9 @@
width: 100%;
border-radius: 3px;
color: var(--text-default);
- background-color: var(--input-background);
border: none;
transition: border-color 0.2s ease-in-out 0s;
+ padding-right: 3px;
}
#undiscord input[type="text"],
@@ -129,8 +145,8 @@
#undiscord input[type="datetime-local"],
#undiscord input[type="number"],
#undiscord input[type="range"] {
- background-color: var(--input-background);
- border: 1px solid var(--input-border);
+ background-color: var(--input-background-default);
+ border: 1px solid var(--input-border-default);
border-radius: 8px;
box-sizing: border-box;
color: var(--text-default);
@@ -151,10 +167,16 @@
#undiscord .sectionDescription {
margin-bottom: 16px;
- color: var(--header-secondary);
+ color: var(--scrollbar-thin-thumb);
font-size: 14px;
line-height: 20px;
font-weight: 400;
+
+ transition: color .3s ease;
+}
+
+#undiscord .sectionDescription:hover {
+ color: var(--interactive-text-hover);
}
#undiscord a {
@@ -187,8 +209,13 @@
min-height: 32px;
/* lookFilled colorPrimary */
- color: rgb(255, 255, 255);
- background-color: var(--button-secondary-background);
+ color: var(--text-default);
+ background-color: var(--control-secondary-background-default);
+ transition: background-color var(--custom-button-transition-duration) ease,color var(--custom-button-transition-duration) ease;
+}
+
+#undiscord button:hover {
+ background-color: var(--control-secondary-background-hover);
}
#undiscord .sizeMedium {
@@ -207,13 +234,13 @@
vertical-align: top;
}
-/* lookFilled colorPrimary */
-#undiscord .accent {
- background-color: var(--brand-experiment);
+#undiscord .danger {
+ background-color: var(--control-critical-primary-background-default);
+ color: var(--white);
}
-#undiscord .danger {
- background-color: var(--button-danger-background);
+#undiscord .danger:hover {
+ background-color: var(--control-critical-primary-background-hover);
}
#undiscord .positive {
diff --git a/src/ui/undiscord.html b/src/ui/undiscord.html
index 0b247b96..38de20b4 100644
--- a/src/ui/undiscord.html
+++ b/src/ui/undiscord.html
@@ -1,6 +1,6 @@