Loading this cog called bot.remove_command("help") with no replacement
command ever defined and no restore on unload — simply loading it broke
[p]help bot-wide, permanently, until process restart. Traced through Red's
own source (redbot/core/commands/help.py) and confirmed the default help
command already routes through bot.send_help_for(), which is exactly what
this cog hooks to render CV2 output — so the fix is to just not remove the
command at all; the CV2 conversion now actually works as intended too,
since its hook point is reachable again.
_active_views leaked one permanent dict entry (holding the whole rendered
page/embed list) per help/menu invocation — every on_timeout and "close"
button already called view.stop(), but nothing was popping the matching
entry except one specific code path. Added a single _track_view() helper
that wraps stop() for automatic eviction and applied it at all 5 insertion
points instead of just the one.
Parallelized the per-command can_run() permission checks in
gather_bot_help_data/gather_cog_help_data (previously one await at a time
for every bot/cog command on every single [p]help call). Also added the
admin_or_permissions check to `cv2 version`, the one subcommand missing it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- _serve_spa built file paths directly from the URL with no containment
check, on a route reachable with zero authentication. Verified the exact
exploit (percent-encoded traversal) reproduces path escape, then fixed by
resolving the path and requiring it stay under WEB_DIR before ever
touching the filesystem.
- OAuth state was generated and handed to the client but never bound
server-side or checked on callback — classic login CSRF/session fixation.
Now bound to a short-lived httponly cookie set during login and verified
(constant-time) on callback.
- Default bind changed from 0.0.0.0 to 127.0.0.1 — loading this cog with
zero config shouldn't expose a web server on every interface before the
owner has even run setup. `[p]evedash host 0.0.0.0` still opts back in.
- _check_guild_access and the callback's guild-membership check relied
solely on the member cache, which can wrongly deny a real admin if the
Members intent hasn't warmed for that guild yet. Both now fall back to a
live fetch on cache miss (per-guild for the single-guild endpoints,
bot-wide only in the rare one-time login path — deliberately not done in
the bulk guild-list endpoint, where that fallback would mean an API call
per guild on every page load).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Re-enabled the "Report to Wormhole", "Delete from Network", and "Wormhole
Profile" context menus (previously disabled entirely). Removed "Bookmark
(Wormhole)" per request — the prefix `wh bm list`/`clear` commands are
untouched, but note this was the only way to *add* a bookmark, so that
feature is now list/clear-only for existing entries.
Wormhole Profile is now considerably richer: account creation date, total
cross-network message count, global-ban status, and per-network karma/
server-count/muted/banned status, sorted busiest-network-first.
Fixed both report-creation paths (the "Report to Wormhole" context menu's
modal, and `wh report msg`): report_counter was read before the config lock
and written after, so concurrent reports could collide on the same id with
the counter never advancing. Now computed and saved atomically inside the
lock. Also gave both paths full parity — message id, jump link, and a real
content preview (the context-menu path previously stored `hash()` of the
content, which is randomized per-process in Python 3 and useless for
anything; the prefix-command path never even recorded who wrote the
message). `wh report view` now surfaces all of it.
Also added the "Find Origin" context menu (right-click a message -> find
its true source server/channel/author across the network) as a separate,
complementary feature to Profile — not a replacement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Right-click any relayed message -> Apps -> Find Origin resolves it back to
the true source: origin server (name + ID), channel, author (name + ID),
message ID, and a jump link. Uses the existing message_map reverse lookup,
falling back to the same brute-force search across the network's member
channels that _ctx_delete_message/wh mod nuke already rely on when the
relay map doesn't have a direct answer. Mod-gated (deanonymizes real
identity across servers). Registered independently of the existing bulk
Report/Bookmark/Delete/Profile context-menu registration, which is
currently disabled — left that decision alone rather than reviving it as
a side effect.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_handle_message_delete_with_cache tried to stash cached attachment data as
message._cached_attachments, but discord.Message uses __slots__ and rejects
arbitrary attribute assignment — this crashed on_message_delete every time a
deleted message had cached attachments (42 times in 48h in production).
Fixed by looking the cache up by message id in self._attachment_cache
instead (same data, same spot _build_embed already checked, just accessed
legally), deferring eviction until after it's actually used.
Also removed two dead `accent = await config.accent_color()` calls —
accent_color was never a registered config key, so both crashed
unconditionally whenever the attachment-preservation feature actually ran,
and the fetched value was never even used afterward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
redbot.core.commands.Group has autohelp=True by default, and its invoke()
override already calls ctx.send_help() unconditionally whenever no
subcommand matches. Manually calling it too (the
`if ctx.invoked_subcommand is None: await ctx.send_help(ctx.command)`
pattern) caused help to be sent twice on every bare group invocation
across GhostWipe, ImageVault, and Affiliates. Verified against Red's
actual Group.invoke() source before fixing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POJ mentions new human members in configured channels then auto-deletes
the ping after a configurable delay. The affiliate board is a staff-managed,
persistent plain-text listing (modal to add, select+confirm to remove),
10 entries per message with a new message per additional page, always
kept in sync in real time and recreated if a page is ever deleted. DM
Affiliates mirrors the same management on a fully separate list sent to
new members via DM on join.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Messages within each channel now sort oldest-first (history()/purge()
return newest-first, which read backwards). Added a real mobile layout:
below 700px the sidebar stacks above the message view and its channel
list becomes a horizontally-scrolling chip strip instead of a cramped
vertical column. Report title/footer and the Discord log embeds now
carry Ruby branding alongside the GhostWipe module name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The HTML report was entirely JavaScript-driven — stats/sidebar/messages only
populated from an embedded JSON blob after load. Viewers that don't execute
scripts (or block network access for local files) showed a blank 0/0/0
skeleton even though the purge worked correctly. Now every stat, the channel
list, and every message are rendered directly into the HTML by Python; JS is
only a progressive enhancement for search. Avatar is downloaded and embedded
as a base64 data URI instead of a live CDN link, so it always renders. Also
parallelized archived-thread lookups, skip the rate-limit pause on channels
with nothing to delete, and post an immediate placeholder log message that
gets edited with the final results instead of the channel sitting silent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Watches configured channels for image attachments, reposts each image (with
attribution and message text) into a staff-only vault channel, then deletes
the original. Retrieval re-fetches the vault message fresh each time so
there's no stale-link problem, and spoiler tags round-trip intact. Access
to config is admin-only; retrieval defaults to open for everyone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deletes a member's messages server-wide the moment they leave, are kicked,
or are banned (all fire on_member_remove; audit log classifies which).
Generates a self-contained HTML report with channel-switching, search, and
attachment previews, alongside per-guild config for triggers, exemptions,
scan scope, dry-run mode, and report retention.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Added null check for network existence in all wh set commands
- All setting changes now log to the log channel with descriptive titles
- Prevents KeyError crashes on invalid network names
- Improves audit trail for administrator actions
- Add `allowed_mentions=discord.AllowedMentions(parse=[])` to all webhook and channel sends
- Prevents users from being pinged when replies are relayed across the network
- Fixes issue where reply context could trigger unintended mentions across all servers
- Applies to webhook sends (primary and fallback) and embed/compact relay modes
Remove Red's built-in contact and dm commands in setup() before the Support
cog loads to prevent command registration conflicts. Commands are stored and
restored in teardown() when the cog unloads.
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Root cause: _init() was blocking on wait_until_ready() for up to 30 seconds,
causing Red's package load to timeout. Red then reports 'Failed to load
package wormhole (timeout)' but the cog is actually partially loaded,
causing 'already loaded' errors on retry.
Timeline:
1. [p]load wormhole starts
2. setup() calls bot.add_cog(cog) ✓
3. setup() calls await cog._init()
4. _init() blocks on wait_until_ready() for 30s
5. Red's load timeout (30s) triggers
6. Red reports 'timeout' but cog is registered
7. [p]load wormhole again → 'already loaded' error
Solution: Remove wait_until_ready() from _init() since:
- Bot is already ready when cogs load on startup
- Background loops have their own wait_until_ready() calls
- _init() shouldn't block setup()
Background loops still wait_until_ready() independently (correct behavior).
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Problem: Red's internal state has stale 'Wormhole' cog registrations from
previous versions (both identifier 7730187301 and 928374650), causing
'Cog named Wormhole already loaded' errors even on fresh bot starts.
Root cause: Multiple config identifiers registered under same cog name:
- wormhole: {'wormhole': {}} (old lowercase)
- Wormhole: {'7730187301': {}, '928374650': {}} (two identifiers!)
Solution: Rename cog class Wormhole → WormholeV4 to bypass all stale
registration state. This gives a clean slate without requiring users to
manually purge config data.
Changes:
- class Wormhole → class WormholeV4 in core.py
- Updated __init__.py import and setup()
- Config identifier unchanged (928374650)
This completely sidesteps the phantom load bug without touching user data.
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Problem: If bot crashes or shuts down ungracefully, context menus stay
registered in Discord's command tree. On restart, add_command() fails
with 'already loaded' error.
Solution: Remove existing context menus before registering new ones.
This makes registration idempotent (safe to run multiple times).
Changes:
- _register_context_menus() now removes existing menus first
- Wrapped in try/except to handle 'not found' gracefully
- Ensures clean reload even after ungraceful shutdown
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Keeping the 4,782-line v3.4.6 monolith as wormhole.py.old for:
- Historical reference
- Rollback safety
- Migration verification
This file is NOT loaded by the cog - it's purely archival.
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
**The Problem:**
Discord CDN URLs (even proxy URLs) expire after message deletion, so image
previews in logs would show as broken after a few minutes.
**The Solution:**
- New "reupload" feature (enabled by default) downloads and re-uploads attachments
- Deleted images/GIFs are permanently preserved in the log channel
- Works with aiohttp to download attachments asynchronously
- Attaches files directly to log message (up to 10 per message)
**Features:**
- `[p]rlog attachments reupload` - Toggle re-uploading on/off
- Downloads images when message is deleted
- Re-uploads as Discord Files (permanent storage)
- Falls back to URL embedding if download fails
- Shows status in settings display
**Improvements:**
- Use proxy URLs throughout for better persistence
- Pass cached attachments through message object
- Better file extension detection (.png, .jpg, .jpeg, .gif, .webp)
- Track ephemeral attachments
- Exception handling for download failures
**Trade-offs:**
- Uses bandwidth to download/upload
- Files become permanent in log channel
- Worth it for permanent image preservation!
Images and GIFs now remain visible in logs forever! 🖼️
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
**Fixed Duplicate Logs:**
- Skip registering generic handlers for message_delete and bulk_delete
- These events are now handled only by specialized attachment handlers
- Eliminates 2x delete logs and 3x edit logs issue
**Added Image Previews:**
- Deleted messages now show image previews in the embed
- Cached attachments display the actual image/GIF
- Multiple images shown as separate embeds with numbering
- set_image() on main embed for first attachment
**Improved Message Edit Display:**
- No more duplicate "Argument 1" and "Argument 2"
- Clean before/after content comparison
- Shows context (author, channel, jump link) separately
- Better content truncation (200 chars vs 50)
- Pin status with emoji (📌 Pinned)
- Tracks attachment count changes
**Better Change Detection:**
- Message edits: old/new content, pin status, embeds, attachments
- Cleaner formatting with proper line breaks
- Longer content previews for readability
All logs are now single, beautiful embeds with image previews!
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Fixed issue where help was being sent twice by completely removing our
custom help command and instead hooking Red's bot.send_help_for method.
This allows Red's help command to process normally, then we intercept
the actual help rendering when CV2 is enabled.
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
User-install contexts (DMs, group DMs, non-member guilds) now only show
commands that can actually work outside a guild:
- _command_needs_guild() heuristic checks @guild_only, Red privilege
levels (MOD/ADMIN/GUILD_OWNER), and user permission requirements,
walking the parent chain for subcommands.
- Autocomplete pre-filters the candidate pool in user-install contexts
and shows available DM-compatible commands when nothing is typed.
- Command execution blocks guild-only commands early with a clear
ephemeral error instead of letting them fail cryptically.
Also:
- Slash command description changed to 'Shine bright like a Ruby'
- Added [p]userslash setdescription command to change it on the fly
(persisted in Config, applied on cog load)
The join hybrid_group has 34 subcommands which exceeds Discord's 25
subcommand limit for slash command groups. Since UserSlash already
bridges all text commands to slash, the hybrid registration is redundant.
Changed commands.hybrid_group → commands.group to avoid the ValueError.
The file userslash/commands.py shadowed redbot.core.commands when Python
registered the submodule on the parent package, causing:
AttributeError: module 'userslash.commands' has no attribute 'Cog'
Renaming the file to slash_bridge.py avoids the collision entirely.
Also adds a user-install whitelist feature:
- [p]userslash whitelist enable/disable
- [p]userslash whitelist add/remove @user
- [p]userslash whitelist list
- [p]userslash whitelist clear
When enabled, only whitelisted users may use the bot via user-install
(DMs, group DMs, non-member guilds). Guild-installed usage is unaffected.
Replaces OneTrueSlash with full user-install support:
- Single proxy slash command bridges all text commands
- Works in servers, DMs, and group DMs
- REST API patcher sets integration_types & contexts on all global commands
- Owner commands: [p]userslash sync/patch/status
- Fuzzy autocomplete (rapidfuzz)
- Enhanced InterContext/InterMessage for non-guild contexts