Skip to content

v5.3.1: Modular architecture, 14 upstream PRs, security fixes, bug fixes#796

Open
TheCellMaster wants to merge 13 commits intovictornpb:masterfrom
TheCellMaster:master
Open

v5.3.1: Modular architecture, 14 upstream PRs, security fixes, bug fixes#796
TheCellMaster wants to merge 13 commits intovictornpb:masterfrom
TheCellMaster:master

Conversation

@TheCellMaster
Copy link
Copy Markdown

@TheCellMaster TheCellMaster commented Apr 15, 2026

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

  • Split monolithic undiscord-core.js (584 lines) into 5 focused modules under src/core/ (orchestrator, search, filter, delete, unarchive)
  • Split monolithic undiscord-ui.js (378 lines) into 4 modules under src/ui/ (init, handlers, progress, logger)
  • Created src/api/discord-api.js as a pure fetch layer with AbortSignal.timeout(30s) on all requests
  • Split helpers.js (8 unrelated functions) into semantic modules: time.js, html.js, discord.js
  • Merged createElm.js + insertCss.js into dom.js
  • Split CSS (527 lines across 2 files) into 6 modular files: layout, components, scrollbar, redact, log, drag
  • Moved HTML templates to src/ui/html/
  • Created src/utils/constants.js with shared constants (API_VERSION, DELETE_RESULT, DELETABLE_MSG_TYPES)
  • All selectors in drag.css scoped with #undiscord to prevent DOM conflicts
  • Added CLAUDE.md project documentation

Community PRs Implemented

Security Fixes

  • XSS fix in log rendering: printLog now escapes all external data via escapeHTML(), preserving <x> redact tags for streamer mode via split pattern
  • escapeHTML() now escapes > character (was missing)
  • Log type validated against whitelist before becoming CSS class name
  • AbortSignal.timeout(30s) on ALL 4 fetch endpoints (search, delete, unarchive, getChannel) -- native browser API, no timer leaks
  • retry_after clamped with Math.max(w, 0) to prevent negative values causing tight request loops

Bug Fixes (v5.3.1)

  • ETR calculation: Now uses remaining messages (grandTotal - delCount - failCount) instead of total, so the estimate actually decreases as messages are deleted
  • ETR page count: Math.ceil instead of Math.round (round under-counted pages when remaining < half a page)
  • avgPing/lastPing: Initialized to 0 instead of null (prevented accidental NaN in ETR arithmetic)
  • Batch continuation: runBatch now continues to next job after failure using _userStopped flag (previously silently stopped all remaining jobs with misleading "skipping to next" log)
  • Display counter: [N/Total] now shows delCount+failCount+1 to reflect actual processing position, not just successful deletes
  • throttledTotalTime: Now tracks actual cooldown time (w*2) instead of just retry_after value
  • HTTP 202: No longer counted as rate limit throttle (was inflating "Rate Limited: N times" statistic)
  • Progress bar: Sets max before value to avoid brief 100% glitch on first update (HTML progress default max is 1.0)
  • onStop guard: _userStopped flag prevents double onStop callback when user clicks Stop during execution
  • Unnecessary 30s wait after completion: After setting running=false, the code no longer falls through to await wait(searchDelay) -- exits immediately via guard
  • Stop button not resetting: stop() now guards on _userStopped instead of running, so it works even after the run loop has already set running=false
  • _userStopped initialized as class field: Was undefined for single runs, causing stop() to be a no-op
  • Missing return: Added return DELETE_RESULT.FAILED in JSON.parse catch path
  • filterResponse: Converted from async to sync (no await inside), added guard for null _searchResponse
  • replaceInterpolations: || changed to ?? to preserve falsy values (0, false, "")

Quality Improvements

  • DELETE_RESULT enum (Object.freeze) replaces magic strings across 7 return paths
  • DELETABLE_MSG_TYPES Set replaces compound conditional for message type filtering
  • Set-based lookup for skipped messages (O(n) vs O(n^2))
  • search() converted from recursive to iterative with MAX_SEARCH_RETRIES=20 (prevents stack overflow)
  • 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 (was unbounded window.confirm)
  • messagePicker timeout (30s) with automatic cleanup of event listeners
  • All CSS variables use fallback pattern var(--new, var(--old))
  • JSDoc on all public methods
  • contributors field added to package.json
  • Help documentation updated for all new features (7 help files)

Stats

  • 40+ files changed, +3000, -1900
  • 28 focused source files replacing 12 monolithic ones
  • 0 lint errors, build passes cleanly
  • All numerical calculations verified by simulation (3 scenarios + edge cases each)

Test plan

  • Install userscript in Tampermonkey -- tested, works
  • Trash icon appears in Discord toolbar -- confirmed
  • Single channel deletion -- tested with 886 messages
  • Rate limit handling (delays increase, capped at 30s/60s) -- confirmed in production
  • Empty page retries -- triggered and recovered correctly
  • ETR decreases as messages are deleted -- confirmed after fix
  • Test batch deletion (multiple channels via comma-separated IDs)
  • Test archive wipe (import index.json)
  • Verify thread detection (click "current" on a thread)
  • Test "Include bot/application messages" toggle
  • Verify streamer mode (redact) hides sensitive data
  • Test stop button during deletion

Generated with Claude Code

TheCellMaster and others added 7 commits April 14, 2026 23:54
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@TheCellMaster TheCellMaster changed the title v5.3.0: Modular architecture, 14 upstream PRs, security fixes, quality improvements v5.3.1: Modular architecture, 14 upstream PRs, security fixes, bug fixes Apr 15, 2026
TheCellMaster and others added 6 commits April 15, 2026 01:26
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>
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>
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>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant