v5.3.1: Modular architecture, 14 upstream PRs, security fixes, bug fixes#796
Open
TheCellMaster wants to merge 13 commits intovictornpb:masterfrom
Open
v5.3.1: Modular architecture, 14 upstream PRs, security fixes, bug fixes#796TheCellMaster wants to merge 13 commits intovictornpb:masterfrom
TheCellMaster wants to merge 13 commits intovictornpb:masterfrom
Conversation
Major refactor: modular architecture + upstream PR features + security fixes. === ARCHITECTURE (complete rewrite) === - Split monolithic undiscord-core.js (584 lines) into 5 focused modules: - src/core/undiscord-core.js: Orchestrator (run, runBatch, stop, confirm) - src/core/search.js: Search with iterative retry (202/429 handling) - src/core/filter.js: Pure message filtering (types, pinned, bots, threads, regex) - src/core/delete.js: Delete with retry loop + rate limit adaptation - src/core/unarchive.js: Thread unarchive before delete - Split monolithic undiscord-ui.js (378 lines) into 4 modules: - src/ui/init.js: DOM mount, CSS injection, toolbar button + MutationObserver - src/ui/handlers.js: All event handlers (start, stop, getChannel, pick, etc) - src/ui/progress.js: onStart/onProgress/onStop callbacks - src/ui/logger.js: XSS-safe log rendering with ring buffer - Created src/api/discord-api.js: Pure fetch layer with AbortSignal.timeout(30s) - Split helpers.js (8 functions in 8 lines) into semantic modules: - src/utils/time.js: wait(), msToHMS() - src/utils/html.js: escapeHTML(), redact(), replaceInterpolations() - src/utils/discord.js: queryString(), ask(), toSnowflake() - Merged createElm.js + insertCss.js into src/utils/dom.js - Split CSS (theme.css 355 lines + main.css 172 lines) into 6 modules: - layout.css, components.css, scrollbar.css, redact.css, log.css, drag.css - Moved HTML templates to src/ui/html/ - Renamed utils to kebab-case (getIds -> get-ids, messagePicker -> message-picker) - Removed 12 legacy files replaced by modular architecture - Created src/utils/constants.js with shared constants === NEW FEATURES (from upstream PRs) === - victornpb#741: Poll messages (type 46) can now be deleted - victornpb#741: Bot slash command responses (type 20) excluded from deletion - victornpb#742: HTTP 403 on delete returns FAIL_SKIP instead of infinite retry loop - victornpb#743: Retry logic refactored: FAILED/FAIL_SKIP properly handled, failCount centralized - victornpb#740: HTTP 403 on search gracefully skips channel instead of canceling batch - victornpb#739: 30s delay between batch jobs to prevent API spam - victornpb#737: Thread unarchiving: attempts PATCH to unarchive before skipping - victornpb#729: Empty page retries (configurable, default 2) before stopping - victornpb#629: "Include bot/application messages" checkbox in Filter section - victornpb#610: Thread auto-detection via API when clicking "current" channel button - victornpb#603: Graceful handling of API errors 50024 (channel not found) and 50001 (missing access) - victornpb#643: Date filter warning: "Make sure you enter both date AND time" - victornpb#527/victornpb#519: Rate limit delay adds on top (never decreases) with caps === SECURITY FIXES === - S1 XSS fix: printLog now escapes all external data via escapeHTML(), preserving <x> redact tags via split pattern for streamer mode - escapeHTML() now also escapes > character - Log type validated against whitelist before becoming CSS class name - AbortSignal.timeout(30s) on ALL fetch calls (search, delete, unarchive, getChannel) replacing leaky setTimeout-based AbortController - retry_after clamped with Math.max(w, 0) to prevent negative values causing tight loops === BUG FIXES === - Fixed onStop called twice (stop() + end of run()) via guard check - Fixed missing return DELETE_RESULT.FAILED in JSON.parse catch path - Fixed filterResponse was async unnecessarily (now sync) - Fixed _searchResponse null crash with guard in filterResponse - Fixed .filter(Boolean) after map().find() to prevent undefined entries - Fixed replaceInterpolations treating falsy values (0, false, "") as missing (|| -> ??) === QUALITY IMPROVEMENTS === - DELETE_RESULT enum (Object.freeze) replaces magic strings - DELETABLE_MSG_TYPES Set replaces compound conditional - Set-based lookup for skipped messages (O(n) vs O(n^2)) - search() converted from recursive to iterative with MAX_SEARCH_RETRIES=20 - searchDelay capped at MAX_SEARCH_DELAY_MS (60s) - deleteDelay capped at MAX_DELETE_DELAY_MS (30s) - Log ring buffer: MAX_LOG_ENTRIES=5000 prevents unbounded DOM growth - Confirm preview limited to 10 messages - messagePicker timeout (30s) with automatic cleanup - drag.css selectors scoped with #undiscord - .resize-handle scoped with #undiscord - Orphan .logarea CSS class removed - MutationObserver throttle extracted to OBSERVER_THROTTLE_MS constant - All CSS variables use fallback pattern var(--new, var(--old)) - JSDoc on all public methods === BUILD === - metadata.mjs supports contributors array from package.json - @author now shows both victornpb and TheCellMaster - CLAUDE.md created with project documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- filters.md: document includeApplications toggle, poll/bot message types - delay.md: document searchDelay/deleteDelay caps, empty page retries, batch job delay - channelId.md: document thread auto-detection, archive wipe skip behavior - authToken.md: document 3 automatic detection methods, streamer mode mention - dateRange.md: new page - date AND time requirement, snowflake conversion - pattern.md: new page - regex usage, examples, ReDoS warning - importJson.md: new page - archive wipe workflow, batch resilience, skip behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The estimated time remaining was calculated using grandTotal (total messages found) instead of remaining (grandTotal - delCount - failCount). This caused the ETR to stay constant or increase when deleteDelay grew from rate limits, instead of decreasing as messages were deleted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- avgPing initialized to 0 instead of null (prevented NaN in ETR calc) - Math.round -> Math.ceil in ETR page count (round under-counted pages when remaining < half a page, e.g. 12 msgs = 0 pages instead of 1) - HTTP 202 "not indexed" no longer counted as rate limit throttle (inflated "Rate Limited: N times" statistic incorrectly) - Progress bar sets max before value to avoid brief 100% glitch on first update (HTML progress default max is 1.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…guard - C-02: runBatch now continues to next job after failure instead of silently stopping. Uses _userStopped flag to distinguish user-initiated stop (respects break) from internal errors (skips to next job). - C-07: Display counter [N/Total] now shows delCount+failCount+1 to reflect actual processing position, not just successful deletes. - C-03: throttledTotalTime now tracks actual cooldown time (w*2) instead of just the retry_after value (w), so "Total time throttled" is accurate. - C-06: onStop at end of run() guarded by !_userStopped to prevent double callback when user clicks Stop during execution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: after setting state.running=false (end condition), the code still fell through to `await wait(searchDelay)` before the while loop checked the condition. This caused: 1. 30 seconds of unnecessary waiting after every termination 2. During the wait, stop() was a no-op (running already false) so the Stop button never changed back to Delete 3. Potential for duplicate "Ended at" messages from timing issues Fixes: - Skip searchDelay wait when state.running is false (guard before wait) - Initialize _userStopped=false as class field (was undefined for single runs) - Reset _userStopped=false at start of run() (not just in runBatch) - stop() now guards on _userStopped instead of state.running, so it works even when called after the run loop has already set running=false Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When delCount+failCount >= grandTotal, there's no point retrying empty pages — we already processed everything. Previously, the script would wait 30s × emptyPageRetries (default 60s) after completing all deletions before finally stopping. Now it checks if all messages have been processed and exits immediately with "All messages have been processed." instead of retrying. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, after deleteMessagesFromList() finished processing all messages (delCount >= grandTotal), the code still waited searchDelay (14.5s) and did one more search request before the allProcessed check in the empty-page branch could trigger exit. Now checks allProcessed right after deleteMessagesFromList() returns, skipping the unnecessary wait + search cycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 tasks
This was referenced May 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete refactor of the Undiscord codebase with modular architecture, implementation of 14 open community PRs, security fixes, numerical bug fixes, and quality-of-life improvements.
Version: 5.3.1 (7 commits: v5.3.0 initial + 6 follow-up fixes)
Architecture Overhaul
undiscord-core.js(584 lines) into 5 focused modules undersrc/core/(orchestrator, search, filter, delete, unarchive)undiscord-ui.js(378 lines) into 4 modules undersrc/ui/(init, handlers, progress, logger)src/api/discord-api.jsas a pure fetch layer withAbortSignal.timeout(30s)on all requestshelpers.js(8 unrelated functions) into semantic modules:time.js,html.js,discord.jscreateElm.js+insertCss.jsintodom.jssrc/ui/html/src/utils/constants.jswith shared constants (API_VERSION,DELETE_RESULT,DELETABLE_MSG_TYPES)drag.cssscoped with#undiscordto prevent DOM conflictsCLAUDE.mdproject documentationCommunity PRs Implemented
FAIL_SKIPinstead of infinite retry loopFAILED/FAIL_SKIPproperly handled,failCountcentralizedPATCHto unarchive before skipping archived threadsSecurity Fixes
printLognow escapes all external data viaescapeHTML(), preserving<x>redact tags for streamer mode via split patternescapeHTML()now escapes>character (was missing)AbortSignal.timeout(30s)on ALL 4 fetch endpoints (search, delete, unarchive, getChannel) -- native browser API, no timer leaksretry_afterclamped withMath.max(w, 0)to prevent negative values causing tight request loopsBug Fixes (v5.3.1)
grandTotal - delCount - failCount) instead of total, so the estimate actually decreases as messages are deletedMath.ceilinstead ofMath.round(round under-counted pages when remaining < half a page)0instead ofnull(prevented accidental NaN in ETR arithmetic)runBatchnow continues to next job after failure using_userStoppedflag (previously silently stopped all remaining jobs with misleading "skipping to next" log)[N/Total]now showsdelCount+failCount+1to reflect actual processing position, not just successful deletesw*2) instead of justretry_aftervaluemaxbeforevalueto avoid brief 100% glitch on first update (HTML progress default max is 1.0)_userStoppedflag prevents doubleonStopcallback when user clicks Stop during executionrunning=false, the code no longer falls through toawait wait(searchDelay)-- exits immediately via guardstop()now guards on_userStoppedinstead ofrunning, so it works even after the run loop has already setrunning=false_userStoppedinitialized as class field: Wasundefinedfor single runs, causing stop() to be a no-opreturn DELETE_RESULT.FAILEDin JSON.parse catch path_searchResponse||changed to??to preserve falsy values (0,false,"")Quality Improvements
DELETE_RESULTenum (Object.freeze) replaces magic strings across 7 return pathsDELETABLE_MSG_TYPESSet replaces compound conditional for message type filteringO(n)vsO(n^2))search()converted from recursive to iterative withMAX_SEARCH_RETRIES=20(prevents stack overflow)searchDelaycapped atMAX_SEARCH_DELAY_MS(60s)deleteDelaycapped atMAX_DELETE_DELAY_MS(30s)MAX_LOG_ENTRIES=5000prevents unbounded DOM growthwindow.confirm)messagePickertimeout (30s) with automatic cleanup of event listenersvar(--new, var(--old))contributorsfield added topackage.jsonStats
Test plan
Generated with Claude Code