Scrape all messages and timestamps in a Discord channel or DM and save to a JSON file (Latest to First)
// ==UserScript==
// @name DiscordScraper
// @description Scrape all messages and timestamps in a Discord channel or DM and save to a JSON file (Latest to First)
// @version 1.3
// @author Ghosty-Tongue
// @match https://*.discord.com/app
// @match https://*.discord.com/channels/*
// @match https://*.discord.com/login
// @license MIT
// @grant none
// @namespace https://greasyfork.org/users/1305224
// ==/UserScript==
(function () {
'use strict';
/* rollup-plugin-baked-env */
const VERSION = "1.0.0 (Scraper)";
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; }
`);
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-positive-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; }
`);
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; }
`);
var buttonHtml = (`
<div id="undicord-btn" tabindex="0" role="button" aria-label="Scrape Messages" title="Scrape Messages with DiscordScraper">
<svg aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
<path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"></path>
</svg>
<progress></progress>
</div>
`);
var undiscordTemplate = (`
<div id="undiscord" class="browser container redact" style="display:none;">
<div class="header">
<svg class="icon" aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
<path fill="currentColor"
d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z">
</path>
</svg>
<h3>DiscordScraper</h3>
<div class="vert-divider"></div>
<span> Scrape messages</span>
<div class="spacer"></div>
<div id="hide" class="icon" aria-label="Close" role="button" tabindex="0">
<svg aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z">
</path>
</svg>
</div>
</div>
<div class="window-body" style="display: flex; flex-direction: row;">
<div class="sidebar scroll">
<details open>
<summary>General</summary>
<fieldset>
<legend>
Server ID
<a href="{{WIKI}}/guildId" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="multiInput">
<div class="input-wrapper">
<input class="input" id="guildId" type="text" priv>
</div>
<button id="getGuild">current</button>
</div>
</fieldset>
<fieldset>
<legend>
Channel ID
<a href="{{WIKI}}/channelId" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="multiInput mb1">
<div class="input-wrapper">
<input class="input" id="channelId" type="text" priv>
</div>
<button id="getChannel">current</button>
</div>
<div class="sectionDescription">
<label class="row"><input id="includeNsfw" type="checkbox">This is a NSFW channel</label>
</div>
</fieldset>
</details>
<hr>
<details>
<summary>Filter</summary>
<fieldset>
<legend>
Search
<a href="{{WIKI}}/filters" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="input-wrapper">
<input id="search" type="text" placeholder="Containing text" priv>
</div>
<div class="sectionDescription">
Only scrape messages that contain the text
</div>
<div class="sectionDescription">
<label><input id="hasLink" type="checkbox">has: link</label>
</div>
<div class="sectionDescription">
<label><input id="hasFile" type="checkbox">has: file</label>
</div>
<div class="sectionDescription">
<label><input id="includePinned" type="checkbox">Include pinned</label>
</div>
</fieldset>
<hr>
<fieldset>
<legend>
Pattern
<a href="{{WIKI}}/pattern" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="sectionDescription">
Scrape messages that match the regular expression
</div>
<div class="input-wrapper">
<span class="info">/</span>
<input id="pattern" type="text" placeholder="regular expression" priv>
<span class="info">/</span>
</div>
</fieldset>
</details>
<details>
<summary>Messages interval</summary>
<fieldset>
<legend>
Interval of messages
<a href="{{WIKI}}/messageId" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="multiInput mb1">
<div class="input-wrapper">
<input id="minId" type="text" placeholder="After a message" priv>
</div>
<button id="pickMessageAfter">Pick</button>
</div>
<div class="multiInput">
<div class="input-wrapper">
<input id="maxId" type="text" placeholder="Before a message" priv>
</div>
<button id="pickMessageBefore">Pick</button>
</div>
<div class="sectionDescription">
Specify an interval to scrape messages.
</div>
</fieldset>
</details>
<details>
<summary>Date interval</summary>
<fieldset>
<legend>
After date
<a href="{{WIKI}}/dateRange" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="input-wrapper mb1">
<input id="minDate" type="datetime-local" title="Messages posted AFTER this date">
</div>
<legend>
Before date
<a href="{{WIKI}}/dateRange" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="input-wrapper">
<input id="maxDate" type="datetime-local" title="Messages posted BEFORE this date">
</div>
<div class="sectionDescription">
Scrape messages that were posted between the two dates.
</div>
<div class="sectionDescription">
* Filtering by date doesn't work if you use the "Messages interval".
</div>
</fieldset>
</details>
<hr>
<details>
<summary>Advanced settings</summary>
<fieldset>
<legend>
Search delay
<a href="{{WIKI}}/delay" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="input-wrapper">
<input id="searchDelay" type="range" value="30000" step="100" min="100" max="60000">
<div id="searchDelayValue"></div>
</div>
</fieldset>
<hr>
<fieldset>
<legend>
Authorization Token
<a href="{{WIKI}}/authToken" title="Help" target="_blank" rel="noopener noreferrer">help</a>
</legend>
<div class="multiInput">
<div class="input-wrapper">
<input class="input" id="token" type="password" autocomplete="dont" priv>
</div>
<button id="getToken">fill</button>
</div>
</fieldset>
</details>
<hr>
<div></div>
<div class="info">
DiscordScraper {{VERSION}}
<br> AI Assistant
</div>
</div>
<div class="main col">
<div class="tbar col">
<div class="row">
<button id="toggleSidebar" class="sizeMedium icon">☰</button>
<button id="start" class="sizeMedium positive" style="width: 150px;" title="Start the scraping process">▶︎ Scrape</button>
<button id="stop" class="sizeMedium" title="Stop the scraping process" disabled>🛑 Stop</button>
<button id="clear" class="sizeMedium">Clear log</button>
<label class="row" title="Hide sensitive information on your screen for taking screenshots">
<input id="redact" type="checkbox" checked> Streamer mode
</label>
</div>
<div class="row">
<progress id="progressBar" style="display:none;"></progress>
</div>
</div>
<pre id="logArea" class="logarea scroll">
<center>
<div>Star <a href="{{HOME}}" target="_blank" rel="noopener noreferrer">the original project</a> on GitHub!</div>
<div><a href="{{HOME}}/discussions" target="_blank" rel="noopener noreferrer">Issues or help</a></div>
</center>
</pre>
<div class="tbar footer row">
<div id="progressPercent"></div>
<span class="spacer"></span>
<label>
<input id="autoScroll" type="checkbox" checked> Auto scroll
</label>
<div class="resize-handle"></div>
</div>
</div>
</div>
</div>
`);
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 => `<x>${escapeHTML(str)}</x>`;
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 = '[SCRAPER]';
/**
* Scrape all messages in a Discord channel or DM
* @author AI Assistant (based on Undiscord by Victornpb)
* @see https://github.com/AI-Assistant/DiscordScraper
*/
class DiscordScraperCore {
options = {
authToken: null, // Your authorization token
guildId: null, // Server were the messages are located
channelId: null, // Channel were the messages are located
minId: null, // Only scrape messages after this, leave blank to scrape all
maxId: null, // Only scrape messages before this, leave blank to scrape 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, // Scrape messages that are pinned
pattern: null, // Only scrape messages that match the regex (insensitive)
searchDelay: null, // Delay each time we fetch for more messages
askForConfirmation: true,
};
state = {
running: false,
scrapedCount: 0,
grandTotal: 0,
offset: 0,
iterations: 0,
allMessages: [], // Array to store scraped messages
_seachResponse: null,
_scrapedMessagesOnPage: [],
_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,
scrapedCount: 0,
grandTotal: 0,
offset: 0,
iterations: 0,
allMessages: [],
_seachResponse: null,
_scrapedMessagesOnPage: [],
_skippedMessages: [],
};
this.options.askForConfirmation = true;
}
/** Start the scraping process */
async run() {
if (this.state.running) return log.error('Already running!');
this.state.running = true;
this.stats.startTime = new Date();
log.success(`\nStarted scraping at ${this.stats.startTime.toLocaleString()}`);
log.debug(
`guildId = "${redact(this.options.guildId)}"`,
`channelId = "${redact(this.options.channelId)}"`,
);
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 scraped
this.filterResponse();
log.verb(
`Total messages found: ${this.state.grandTotal}`,
`(Messages on current page: ${this.state._seachResponse.messages.length}`,
`Scraped this page: ${this.state._scrapedMessagesOnPage.length}`,
`Total scraped: ${this.state.scrapedCount})`,
`offset: ${this.state.offset}`
);
this.printStats();
// Calculate estimated time
this.calcEtr();
log.verb(`Estimated time remaining: ${msToHMS(this.stats.etr)}`);
// Add messages to allMessages array
if (this.state._scrapedMessagesOnPage.length > 0) {
this.state.allMessages.push(...this.state._scrapedMessagesOnPage);
this.state.scrapedCount += this.state._scrapedMessagesOnPage.length;
// Update progress
if (this.onProgress) this.onProgress(this.state, this.stats);
// Adjust offset for the next search
this.state.offset = this.state.scrapedCount;
}
else if (this.state._skippedMessages.length > 0) {
// There are stuff, but nothing to scrape (e.g., a page full of system messages)
// Check the 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('Nothing to scrape on this page, checking next page...');
log.verb(`Skipped ${this.state._skippedMessages.length} out of ${this.state._seachResponse.messages.length} on this page.`, `(Offset was ${oldOffset}, adjusted to ${this.state.offset})`);
}
else {
log.verb('Ended because API returned an empty page.');
this.state.running = false;
}
// wait before next page (fix search page not updating fast enough)
if (this.state.running) {
log.verb(`Waiting ${(this.options.searchDelay / 1000).toFixed(2)}s before next page...`);
await wait(this.options.searchDelay);
}
} while (this.state.running);
// Finalize: Save to JSON
if (this.state.scrapedCount > 0) {
this.saveToJson();
} else {
log.warn("No messages were scraped to save.");
}
this.stats.endTime = new Date();
log.success(`Scraping ended at ${this.stats.endTime.toLocaleString()}! Total time: ${msToHMS(this.stats.endTime.getTime() - this.stats.startTime.getTime())}`);
this.printStats();
log.debug(`Scraped ${this.state.scrapedCount} messages.\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() {
// The search endpoint returns up to 25 results, so we estimate the total pages.
this.stats.etr = (this.options.searchDelay * Math.round(this.state.grandTotal / 25));
}
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([
['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'], // Latest to first
['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.options.searchDelay += w; // increase delay
w = this.options.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;
}
filterResponse() {
const data = this.state._seachResponse;
// the search total will decrease as we scroll
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));
// Filter out messages we don't want to scrape (e.g. system messages)
let messagesToScrape = discoveredMessages;
// Filter out message types we can't 'delete' (which often means system messages, but we'll scrape all for now, keeping the filter minimal for scraping)
// messagesToScrape = messagesToScrape.filter(msg => msg.type === 0 || (msg.type >= 6 && msg.type <= 21));
messagesToScrape = messagesToScrape.filter(msg => msg.pinned ? this.options.includePinned : true);
// custom filter of messages
try {
const regex = new RegExp(this.options.pattern, 'i');
messagesToScrape = messagesToScrape.filter(msg => regex.test(msg.content));
} catch (e) {
log.warn('Ignoring RegExp because pattern is malformed!', e);
}
// transform to desired output format (message content and timestamp)
const formattedMessages = messagesToScrape.map(msg => ({
id: msg.id,
timestamp: msg.timestamp,
author: `${msg.author.username}#${msg.author.discriminator}`,
content: msg.content,
attachments_count: msg.attachments.length,
attachments_urls: msg.attachments.map(a => a.url),
}));
// create an array containing everything we skipped. (used to calculate offset for next searches)
const skippedMessages = discoveredMessages.filter(msg => !messagesToScrape.find(m => m.id === msg.id));
this.state._scrapedMessagesOnPage = formattedMessages;
this.state._skippedMessages = skippedMessages;
console.log(PREFIX$1, 'filterResponse', this.state);
}
saveToJson() {
log.info('Scraping complete. Preparing JSON file...');
const dataStr = JSON.stringify(this.state.allMessages, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const filename = `discord_messages_${this.options.channelId}_${new Date().toISOString().slice(0, 10)}.json`;
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
log.success(`Successfully scraped ${this.state.scrapedCount} messages and saved to ${filename}!`);
}
#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(
`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);
}
}
}
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 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);
});
}
};
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 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 the script is up to date');
log.debug('Alternatively, you can try entering a Token manually in the "Advanced Settings" section.');
}
return '';
}
const PREFIX = '[SCRAPER]';
// -------------------------- User interface ------------------------------- //
// links (using original links for reference)
const HOME = 'https://github.com/victornpb/undiscord';
const WIKI = 'https://github.com/victornpb/undiscord/wiki';
const discordScraperCore = new DiscordScraperCore();
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#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 messages after (newer) it will be scraped.');
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 messages before (older) it will be scraped.');
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) discordScraperCore.options.searchDelay = v;
};
$('input#searchDelay').addEventListener('input', (event) => {
$('div#searchDelayValue').textContent = event.target.value + 'ms';
});
// Removed import json logic as it's not relevant for scraping from Discord.
// redirect console logs to inside the window after setting up the UI
setLogFn(printLog);
setupScraperCore();
}
function printLog(type = '', args) {
ui.logArea.insertAdjacentHTML('beforeend', `<div class="log log-${type}">${Array.from(args).map(o => typeof o === 'object' ? JSON.stringify(o, o instanceof Error && Object.getOwnPropertyNames(o)) : o).join('\t')}</div>`);
if (ui.autoScroll.checked) ui.logArea.querySelector('div:last-child').scrollIntoView(false);
if (type==='error') console.error(PREFIX, ...Array.from(args));
}
function setupScraperCore() {
discordScraperCore.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';
};
discordScraperCore.onProgress = (state, stats) => {
// console.log(PREFIX, 'onProgress', state, stats);
let max = state.grandTotal;
const value = state.scrapedCount;
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}) Scraped. 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 = discordScraperCore.options.searchDelay;
$('div#searchDelayValue').textContent = discordScraperCore.options.searchDelay+'ms';
};
discordScraperCore.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 guildId = $('input#guildId').value.trim();
const channelId = $('input#channelId').value.trim();
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());
// 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!');
if (!channelId) return log.error('You must fill the "Channel ID" field!');
if (!authToken) return log.error('You must have an Authorization Token!');
// clear logArea
ui.logArea.innerHTML = '';
discordScraperCore.resetState();
discordScraperCore.options = {
...discordScraperCore.options,
authToken,
guildId,
channelId,
minId: minId || minDate,
maxId: maxId || maxDate,
content,
hasLink,
hasFile,
includeNsfw,
includePinned,
pattern,
searchDelay,
};
try {
// No batch processing implemented in this scraper version, only single channel
await discordScraperCore.run();
} catch (err) {
log.error('CoreException', err);
discordScraperCore.stop();
}
}
function stopAction() {
console.log(PREFIX, 'stopAction');
discordScraperCore.stop();
}
// ---- END DiscordScraper ----
initUI();
})();