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>
- 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
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>
- Mirror channels now receive relayed messages (was blocked from sending but never sent to)
- Server allowlist enforced in relay (allowlist_servers gate check)
- Silent mode suppresses status broadcasts (_status respects 'silent' flag)
- Welcome message sent when channel joins via 'wh open'
Commands now show as 'wh create', 'wh staff add', 'wh set relaymode'
instead of 'wh-create', 'wh-staff add', 'wh-set relaymode'.
- Added commands/_base.py with root 'wh' group and all 20 subgroup stubs
- All 13 command mixins inherit from WormholeBase
- Decorators changed: @commands.hybrid_command(name='wh-X') → @WormholeBase.wh.command(name='X')
- Subgroup commands: @wh_X.command() → @WormholeBase.wh_X.command()
- Removed duplicate 'wh' group from core.py (now in _base.py)
- Fixed alias: 'wh-color' → 'color' for colour command
The hybrid_command decorator on mixin classes already auto-registers
slash commands. The manual registrations in _register_context_menus
clashed, causing 'wh-list already registered' on load. Removed all
manual slash commands — only context menus remain.
Embed mode was missing foreign emoji support. Now all three relay
modes (webhook, embed, compact) show unknown emojis as embed
thumbnails via attachment:// scheme.
File attachments render as massive images in Discord regardless of
source size. Switch to embed thumbnails via the attachment:// scheme:
1. Download emoji from CDN at 48px
2. Attach as a File to the message
3. Reference via attachment://name.png in a minimal embed's thumbnail
4. Thumbnails render at ~80×80 px — much closer to emoji size
Fresh File/Embed objects are built per channel (consumed on send).
Bumps to v3.4.6.
File attachments render as massive images in Discord. Replace the
download-and-attach approach with clickable [:name:](cdn_url) inline
links — keeps foreign emojis compact and inline with the message text.
Removes aiohttp dependency and simplifies the helper to synchronous.
Bumps to v3.4.5.
Custom emojis the bot can't see (bot.get_emoji returns None) are now:
1. Replaced with :name: in message text
2. Downloaded from Discord CDN at 64px
3. Attached as small image files to the relay message
Known emojis (from any server the bot is in) keep the <:name:id>
syntax and render normally. Up to 5 emoji images per message.
Works in webhook and compact relay modes. Bumps to v3.4.4.
The forward map only stores relay targets — the source channel (where
the original message was typed) was never included. When someone in
Server B replied to a relayed message, the button for Server A fell
back to the fallback URL pointing at Server B instead of Server A.
Fix: after building the relay-target URLs, identify the source channel
by finding network channels not present in the forward map, and map
them to the original message ID.
Passing view=None to wh.send() on every message caused all relays
to fail. Now uses **_view_kw dict unpacking so the view parameter
is only included when there is actually a reply-jump button to show.
When a reply is relayed across servers, each copy now includes a URL
button underneath the quoted reply text:
- If the referenced message has a relayed copy in the target channel,
the button links to that local copy (so users stay in their own server).
- Otherwise, it falls back to the original message URL.
Works across all relay modes (webhook, embed, compact) and leverages
the existing msg_map to resolve per-channel message IDs.
Bumps version to 3.4.3.
- Migration now uses a _mention_migrated flag so it only forces mentions
off once. After that, admins can freely toggle per-network settings
with 'wh mentions set <network> <type> <true/false>'
- New mention_optout_users list per network: users can opt out of being
@pinged through the relay even when user mentions are enabled
network-wide. Their <@ID> mentions are stripped; everyone else's pass
through. Default: opted in.
- New commands:
• wh mentions optout <network> — stop receiving relay pings
• wh mentions optin <network> — start receiving relay pings again
• wh mentions mystatus <network> — check your current ping status
- apply_mention_policy() now accepts optout_users set and selectively
strips only opted-out users' mentions when user mentions are globally
allowed
- Updated 'wh mentions status' to show opt-out count and correct
defaults
- On cog load, all existing networks are migrated to disable every
mention type (user, role, everyone, here) — changing the default
only affected new networks, not existing ones
- Reverted the v3.4.2 'and ctx.prefix' guard in the command filter —
the real relay bug was the regex crash, not the empty prefix.
ctx.valid alone correctly identifies registered commands even with
an empty-string prefix
- Default mention_policy now disables all mention types including user
mentions (previously allow_user_mentions defaulted to True)
- Blocked mentions are now removed entirely instead of replaced with
ZWSP placeholders — '@mention HI!' becomes 'HI!' on the other side
- Trailing whitespace from removed mentions is cleaned up
- Updated both apply_mention_policy() and sanitise_mentions()
Root cause: apply_mention_policy() in utils.py used raw strings for
re.sub replacement arguments:
re.sub(r'<@&(\d+)>', r'@\u200brole', content)
The r-prefix makes \u200b literal backslash-u instead of Unicode ZWSP.
Python's re.sub always validates the replacement template, so \u at
position 1 raises re.error on EVERY message — even without matches.
Since _DEFAULT_NETWORK sets mention_policy.allow_role_mentions=False,
this code path runs for all networks, causing _do_relay to throw on
every single message. The exception was caught by on_message's
try/except, silently killing relay with total_messages stuck at 0.
Fix: remove r-prefix from replacement strings so \u200b is correctly
interpreted as the Unicode zero-width space character.
Added three diagnostic tools to help debug relay issues:
- wh relaydebug: static check of all on_message gates against the
current channel — reports which checks pass/fail
- wh testsend [text]: bypasses on_message entirely and sends a test
message directly through the webhook to verify connectivity
- wh tracemode: toggles live tracing for the channel — when enabled,
every message produces a diagnostic embed showing exactly which
gate-check blocked it or that the relay succeeded
Also added trace checkpoints at every early-return in on_message so
tracemode can pinpoint the exact check that blocks a message.
The v3.4.1 command filter used bot.get_context() to detect commands,
but an empty-string prefix ('') matched EVERY message, causing
ctx.valid to return True for any message whose first word happened
to be a registered command. With enough built-in commands (help,
ping, info, set, etc.) this silently dropped most or all relay
traffic (total_messages stayed at 0).
Fix: require ctx.prefix to be truthy (non-empty) before skipping
a message as a command. Empty-prefix matches are now ignored,
so only messages with a real prefix (!, @bot, etc.) are filtered.
Also re-added the empty-prefix warning to 'wh debug' output so
users know to clean up their bot prefix configuration.
Discord enforces a max of 25 subcommands per slash command group.
Wormhole has 50 children on the main group and 39 on wh set, making
hybrid_group impossible.
Fix: wh is now a regular prefix group, with 7 standalone slash commands
registered for the most common user-facing operations:
/wormhole-list, /wormhole-info, /wormhole-discover,
/wormhole-accept, /wormhole-agree, /wormhole-report,
/wormhole-profile
All 4 context menus still work (Report, Bookmark, Delete, Profile).
All 85+ prefix commands still work via [p]wh.
get_relayed() now checks reverse map as fallback, so passing any
relayed copy's message ID resolves to the full mapping.
mod edit and mod nuke both call get_original() first, so staff can
right-click or copy ANY copy of a message and it deletes/edits the
entire chain across all servers.
Phase 5 — Security, Privacy & Power Update:
HYBRID COMMANDS
- Main command group converted to hybrid_group (slash + prefix)
- All commands work as both /wh and [p]wh
CONTEXT MENU ACTIONS (right-click)
- Report to Wormhole — opens modal for report reason
- Wormhole Bookmark — saves message to DM
- Wormhole Delete — staff can delete from entire network
- Wormhole Profile — view user stats across networks
GRANULAR MENTION POLICY
- Per-network control: allow/block @user, @role, @everyone, @here
- Per-server overrides (different rules per guild)
- Per-user exemptions (trusted users bypass policy)
- Replaces legacy strip-* with proper policy engine
- wh mentions set/server-set/exempt/unexempt/status
TERMS OF SERVICE / RULES ACCEPTANCE GATE
- wh tos enable — require acceptance before messaging
- Built-in legal template (6 sections, prosecution-ready)
- Consent tracking with timestamps
- wh tos set (custom text), template (reset), accepted (view), reset (force re-accept)
- wh accept / wh agree — user-facing acceptance flow
- Blocks relay for non-accepted users with helpful reminder
MOD EDIT/DELETE ACROSS NETWORK
- wh mod edit <name> <msg_id> <new_content> — edits all relayed copies
- wh mod nuke <name> <msg_id> — deletes from every server + original
- Both audit-logged
REPORT SYSTEM
- wh report message — report via reply or message ID
- Context menu report with modal (reason input)
- Anti-abuse cooldown (60s between reports)
- Content hashing for audit trails
- Reports sent to log channel + network owner DM
- wh report list/resolve/action (ban/mute/warn/dismiss)
- Capped at 200 reports per network
PRIVACY & SECURITY
- _ReportModal class for context menu reporting
- Content hash stored instead of plaintext in report records
- Report cooldowns prevent abuse
- ToS acceptance with ISO timestamps for legal compliance
4,196 lines in wormhole.py, 419 in utils.py — 85+ commands total