From 56d5b11882badfef8ca345cfcfad1124b29565c0 Mon Sep 17 00:00:00 2001 From: Viktor AI Date: Thu, 7 May 2026 22:38:02 +0000 Subject: [PATCH] Fix discord.py 2.x compat: replace Embed.Empty with conditional kwargs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Embed.Empty was removed in discord.py 2.x — set_footer and set_author now only pass icon_url when a value actually exists. --- newhelpmenu/newhelpmenu.py | 10 +- wormhole/wormhole.py | 993 +------------------------------------ 2 files changed, 35 insertions(+), 968 deletions(-) diff --git a/newhelpmenu/newhelpmenu.py b/newhelpmenu/newhelpmenu.py index 281c6a4..21c2873 100644 --- a/newhelpmenu/newhelpmenu.py +++ b/newhelpmenu/newhelpmenu.py @@ -449,11 +449,17 @@ class HelpView(View): ft = _fmt(c.get("footer_text", ""), self.ctx) fi = c.get("footer_icon") if ft: - e.set_footer(text=ft, icon_url=fi or discord.Embed.Empty) + if fi: + e.set_footer(text=ft, icon_url=fi) + else: + e.set_footer(text=ft) if c.get("show_author"): an = _fmt(c.get("author_name", "{bot_name}"), self.ctx) ai = c.get("author_icon") or (self.ctx.me.display_avatar.url if self.ctx.me.display_avatar else None) - e.set_author(name=an, icon_url=ai or discord.Embed.Empty) + if ai: + e.set_author(name=an, icon_url=ai) + else: + e.set_author(name=an) return e def _home_embed(self) -> discord.Embed: diff --git a/wormhole/wormhole.py b/wormhole/wormhole.py index 9d94d40..e3fdf27 100644 --- a/wormhole/wormhole.py +++ b/wormhole/wormhole.py @@ -245,21 +245,26 @@ class Wormhole(commands.Cog): self._ready = asyncio.Event() self._bg_tasks: List[asyncio.Task] = [] - self._startup_task = self.bot.loop.create_task(self._init()) + self._startup_task = asyncio.ensure_future(self._init()) async def _init(self): - await self.bot.wait_until_ready() - networks = await self.config.networks() - for name, data in networks.items(): - self.cooldowns[name] = CooldownBucket(data.get("rate_limit_rate", 5), data.get("rate_limit_per", 10.0)) - am = data.get("automod", {}) - self.dup_detectors[name] = DuplicateDetector(am.get("spam_window", 30.0), am.get("spam_threshold", 3)) - self.raid_detectors[name] = RaidDetector(am.get("raid_window", 60.0), am.get("raid_threshold", 10)) - self._bg_tasks.append(self.bot.loop.create_task(self._blackout_loop())) - self._bg_tasks.append(self.bot.loop.create_task(self._portal_update_loop())) - self._bg_tasks.append(self.bot.loop.create_task(self._scheduled_msg_loop())) - self._ready.set() - log.info("Wormhole v3.2.0 ready — %d networks loaded.", len(networks)) + try: + await self.bot.wait_until_ready() + networks = await self.config.networks() + for name, data in networks.items(): + self.cooldowns[name] = CooldownBucket(data.get("rate_limit_rate", 5), data.get("rate_limit_per", 10.0)) + am = data.get("automod", {}) + self.dup_detectors[name] = DuplicateDetector(am.get("spam_window", 30.0), am.get("spam_threshold", 3)) + self.raid_detectors[name] = RaidDetector(am.get("raid_window", 60.0), am.get("raid_threshold", 10)) + self._bg_tasks.append(asyncio.ensure_future(self._blackout_loop())) + self._bg_tasks.append(asyncio.ensure_future(self._portal_update_loop())) + self._bg_tasks.append(asyncio.ensure_future(self._scheduled_msg_loop())) + log.info("Wormhole v3.2.0 ready — %d networks loaded.", len(networks)) + except Exception as exc: + log.error("Wormhole init error (relay will still work): %s", exc, exc_info=True) + finally: + # ALWAYS set ready so the relay isn't permanently stuck + self._ready.set() async def cog_unload(self): self._startup_task.cancel() @@ -378,19 +383,23 @@ class Wormhole(commands.Cog): return n return None - async def _wh(self, ch: discord.TextChannel) -> discord.Webhook: - if ch.id in self.webhook_cache: + async def _wh(self, ch: discord.TextChannel, *, force_refresh: bool = False) -> discord.Webhook: + if not force_refresh and ch.id in self.webhook_cache: return self.webhook_cache[ch.id] try: for w in await ch.webhooks(): - if w.user == self.bot.user and w.name == "Wormhole Relay": + if w.user and w.user.id == self.bot.user.id and w.name == "Wormhole Relay": self.webhook_cache[ch.id] = w return w w = await ch.create_webhook(name="Wormhole Relay") self.webhook_cache[ch.id] = w return w except discord.Forbidden: - raise commands.UserFeedbackCheckFailure("Missing webhook permissions.") + self.webhook_cache.pop(ch.id, None) + raise + except Exception: + self.webhook_cache.pop(ch.id, None) + raise def _avatar(self, msg, mode, icon): if mode == "server": @@ -1396,952 +1405,4 @@ class Wormhole(commands.Cog): lines.append(f"{'✅' if am.get(k) else '❌'} `{k}`") await ctx.send(embed=info_embed("\n".join(lines), title=f"Auto-mod — {name}")) - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # INVITES - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.group(name="invite", invoke_without_command=True) - async def wh_inv(self, ctx): await ctx.send_help(ctx.command) - - @wh_inv.command(name="create") - async def wh_inv_create(self, ctx, name: str, max_uses: int = 0, expires_minutes: int = 0): - """Create invite code. 0=unlimited/no expiry.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - code = generate_invite_code() - inv = {"uses": 0, "max_uses": max_uses or None, - "expires_at": datetime.fromtimestamp(time.time() + expires_minutes * 60, tz=timezone.utc).isoformat() if expires_minutes > 0 else None, - "creator_id": ctx.author.id} - async with self.config.networks() as n: n[name].setdefault("invites", {})[code] = inv - await ctx.send(embed=ok_embed(f"Invite for **{name}**:\n```\n{code}\n```\nJoin: `{ctx.clean_prefix}wh invite use {code}`", title="🔗 Invite")) - - @wh_inv.command(name="vanity") - async def wh_inv_vanity(self, ctx, name: str, *, word: str): - """Set a vanity invite word (e.g. `gaming` → `[p]wh invite use gaming`).""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_owner(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - word = word.lower().strip().replace(" ", "-") - # Check uniqueness - for n, nd in (await self.config.networks()).items(): - if nd.get("vanity_invite") == word and n != name: - return await ctx.send(embed=err_embed(f"Vanity `{word}` is taken.")) - async with self.config.networks() as n: n[name]["vanity_invite"] = word - await ctx.send(embed=ok_embed(f"Vanity invite → `{word}`. Join: `{ctx.clean_prefix}wh invite use {word}`")) - - @wh_inv.command(name="use", aliases=["join"]) - @commands.guild_only() - async def wh_inv_use(self, ctx, code: str): - """Use an invite code or vanity name to join.""" - nets = await self.config.networks() - target = None - inv_data = None - is_vanity = False - - # Check vanity first - for n, d in nets.items(): - if d.get("vanity_invite") and d["vanity_invite"].lower() == code.lower(): - target = n; is_vanity = True; break - - # Then check codes - if not target: - for n, d in nets.items(): - if code in d.get("invites", {}): - target = n; inv_data = d["invites"][code]; break - - if not target: - return await ctx.send(embed=err_embed("Invalid invite.")) - - d = nets[target] - if inv_data: - if inv_data.get("expires_at"): - if datetime.now(timezone.utc) > datetime.fromisoformat(inv_data["expires_at"]): - async with self.config.networks() as n: n[target].get("invites", {}).pop(code, None) - return await ctx.send(embed=err_embed("Expired.")) - if inv_data.get("max_uses") and inv_data["uses"] >= inv_data["max_uses"]: - return await ctx.send(embed=err_embed("Max uses reached.")) - - # Standard checks - if d.get("allowlist_servers") and ctx.guild.id not in d["allowlist_servers"]: return await ctx.send(embed=err_embed("Not on allowlist.")) - if ctx.guild.id in d.get("banned_servers", []): return await ctx.send(embed=err_embed("Server banned.")) - if ctx.channel.id in d["channels"]: return await ctx.send(embed=err_embed("Already linked.")) - ex = await self._net_for_ch(ctx.channel.id) - if ex: return await ctx.send(embed=err_embed(f"Already in **{ex}**.")) - - async with self.config.networks() as n: - n[target]["channels"].append(ctx.channel.id) - if inv_data and not is_vanity: n[target]["invites"][code]["uses"] += 1 - d = n[target] - await ctx.send(embed=ok_embed(f"Linked to **{target}**! ({len(d['channels'])} channels)")) - await self._status(target, d, ctx.channel, f"📡 **{ctx.guild.name}** joined via invite.") - if d.get("welcome_message"): await ctx.send(embed=info_embed(d["welcome_message"], title=f"👋 Welcome!")) - if d.get("motd"): await ctx.send(embed=info_embed(d["motd"], title=f"📋 MOTD")) - - @wh_inv.command(name="revoke") - async def wh_inv_revoke(self, ctx, name: str, code: str): - name = name.lower(); d = await self._net(name) - if not d or (not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author)): return - async with self.config.networks() as n: n[name].get("invites", {}).pop(code, None) - await ctx.send(embed=ok_embed(f"Invite `{code}` revoked.")) - - @wh_inv.command(name="list") - async def wh_inv_list(self, ctx, name: str): - name = name.lower(); d = await self._net(name) - if not d: return - invs = d.get("invites", {}) - van = d.get("vanity_invite") - lines = [] - if van: lines.append(f"🌟 Vanity: `{van}`") - for c, i in invs.items(): - lines.append(f"`{c}` — {i['uses']}/{i.get('max_uses') or '∞'}") - await ctx.send(embed=info_embed("\n".join(lines) if lines else "No invites.", title=f"Invites — {name}")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # ANNOUNCEMENTS & PORTAL - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.command(name="announce") - async def wh_announce(self, ctx, name: str, *, message: str): - """Broadcast to all channels + DM subscribers.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - em = announce_embed(message, title=f"📢 {name}") - em.set_footer(text=f"From {ctx.author.display_name}") - sent = 0 - for ch_id in d["channels"]: - ch = self.bot.get_channel(ch_id) - if ch: - try: await ch.send(embed=em); sent += 1 - except: pass - # Also DM subscribers - dm_sent = 0 - for uid in d.get("dm_subscribers", []): - u = self.bot.get_user(uid) - if u: - try: await u.send(embed=em); dm_sent += 1 - except: pass - await ctx.send(embed=ok_embed(f"Sent to {sent} channels + {dm_sent} DM subscribers.")) - await self._audit(name, "announce", ctx.author) - - @wh.command(name="portal") - @commands.guild_only() - async def wh_portal(self, ctx, name: str = None): - """Create/refresh portal status embed.""" - if not name: - name = await self._net_for_ch(ctx.channel.id) - if not name: return await ctx.send(embed=err_embed("Not linked.")) - else: name = name.lower() - d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - embed = build_portal_embed(name, d, len(d["channels"]), d.get("total_messages", 0)) - existing = d.get("portal_messages", {}).get(str(ctx.channel.id)) - if existing: - try: - msg = await ctx.channel.fetch_message(existing) - await msg.edit(embed=embed) - return await ctx.send(embed=ok_embed("Portal refreshed!"), delete_after=5) - except: pass - msg = await ctx.send(embed=embed) - try: await msg.pin() - except: pass - async with self.config.networks() as n: n[name].setdefault("portal_messages", {})[str(ctx.channel.id)] = msg.id - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # STARBOARD - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.group(name="starboard", aliases=["star"], invoke_without_command=True) - async def wh_star(self, ctx): await ctx.send_help(ctx.command) - - @wh_star.command(name="enable") - async def wh_star_on(self, ctx, name: str, channel: discord.TextChannel, threshold: int = 3): - """Enable cross-network starboard.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - async with self.config.networks() as n: - n[name]["starboard_enabled"] = True - n[name]["starboard_channel"] = channel.id - n[name]["starboard_threshold"] = threshold - await ctx.send(embed=ok_embed(f"Starboard → {channel.mention} (threshold: {threshold}⭐)")) - - @wh_star.command(name="disable") - async def wh_star_off(self, ctx, name: str): - await self._toggle(ctx, name, "starboard_enabled", False) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # KARMA / REPUTATION - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.group(name="karma", invoke_without_command=True) - async def wh_karma(self, ctx): await ctx.send_help(ctx.command) - - @wh_karma.command(name="enable") - async def wh_k_on(self, ctx, name: str, emoji: str = "👍"): - """Enable karma. React with the emoji on relayed messages.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - async with self.config.networks() as n: - n[name]["karma_enabled"] = True - n[name]["karma_emoji"] = emoji - await ctx.send(embed=ok_embed(f"Karma enabled (react {emoji} to give karma).")) - - @wh_karma.command(name="disable") - async def wh_k_off(self, ctx, name: str): - await self._toggle(ctx, name, "karma_enabled", False) - - @wh_karma.command(name="check") - async def wh_k_check(self, ctx, name: str, user: discord.User = None): - """Check karma score.""" - name = name.lower(); d = await self._net(name) - if not d: return - user = user or ctx.author - score = d.get("karma_scores", {}).get(str(user.id), 0) - await ctx.send(embed=info_embed(f"**{user.display_name}** has **{score}** karma on **{name}**.", title="⭐ Karma")) - - @wh_karma.command(name="leaderboard", aliases=["lb"]) - async def wh_k_lb(self, ctx, name: str): - """Karma leaderboard.""" - name = name.lower(); d = await self._net(name) - if not d: return - scores = d.get("karma_scores", {}) - top = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:10] - medals = ["🥇", "🥈", "🥉"] + [f"**{i}.**" for i in range(4, 11)] - lines = [] - for i, (uid, sc) in enumerate(top): - u = self.bot.get_user(int(uid)) - lines.append(f"{medals[i]} {u or uid} — **{sc}** karma") - await ctx.send(embed=info_embed("\n".join(lines) if lines else "No karma yet.", title=f"Karma — {name}")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # HIGHLIGHTS - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.group(name="highlight", aliases=["hl"], invoke_without_command=True) - async def wh_hl(self, ctx): await ctx.send_help(ctx.command) - - @wh_hl.command(name="add") - async def wh_hl_add(self, ctx, name: str, *, keyword: str): - """Get DM'd when a keyword appears in the network.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - uid = str(ctx.author.id) - async with self.config.networks() as n: - hl = n[name].setdefault("highlights", {}).setdefault(uid, []) - if keyword.lower() not in [k.lower() for k in hl]: - hl.append(keyword) - await ctx.send(embed=ok_embed(f"Highlight `{keyword}` added for **{name}**.")) - - @wh_hl.command(name="remove") - async def wh_hl_rm(self, ctx, name: str, *, keyword: str): - name = name.lower() - async with self.config.networks() as n: - if name in n: - hl = n[name].get("highlights", {}).get(str(ctx.author.id), []) - n[name]["highlights"][str(ctx.author.id)] = [k for k in hl if k.lower() != keyword.lower()] - await ctx.send(embed=ok_embed(f"Highlight `{keyword}` removed.")) - - @wh_hl.command(name="list") - async def wh_hl_ls(self, ctx, name: str): - name = name.lower(); d = await self._net(name) - if not d: return - kws = d.get("highlights", {}).get(str(ctx.author.id), []) - await ctx.send(embed=info_embed("\n".join(f"• `{k}`" for k in kws) if kws else "No highlights.", title=f"Your Highlights — {name}")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # SCHEDULED MESSAGES - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.command(name="schedule") - async def wh_schedule(self, ctx, name: str, minutes: int, *, message: str): - """Schedule a message to be sent to the network in X minutes.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - send_at = datetime.fromtimestamp(time.time() + minutes * 60, tz=timezone.utc).isoformat() - async with self.config.networks() as n: - n[name].setdefault("scheduled_messages", []).append({ - "content": message, "send_at": send_at, "author_id": ctx.author.id - }) - await ctx.send(embed=ok_embed(f"Scheduled for {minutes} min from now.")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # AUDIT LOG - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.command(name="audit") - async def wh_audit(self, ctx, name: str, count: int = 20): - """View recent audit log entries.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - entries = d.get("audit_log", [])[-count:] - if not entries: return await ctx.send(embed=info_embed("No audit entries.")) - lines = [] - for e in reversed(entries): - ts = e.get("timestamp", "?")[:16] - lines.append(f"`{ts}` **{e.get('action')}** by {e.get('user')} {e.get('target', '')}") - await ctx.send(embed=info_embed("\n".join(lines[:25]), title=f"Audit — {name}")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # BLACKOUT - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.group(name="blackout", invoke_without_command=True) - async def wh_bo(self, ctx): await ctx.send_help(ctx.command) - - @wh_bo.command(name="add") - async def wh_bo_add(self, ctx, name: str, start_hour: int, end_hour: int, *, days: str = "0,1,2,3,4,5,6"): - """Add blackout: freeze during hours (UTC). Days: 0=Mon..6=Sun""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - try: day_list = [int(x.strip()) for x in days.split(",")] - except: return await ctx.send(embed=err_embed("Invalid days.")) - async with self.config.networks() as n: - n[name].setdefault("blackout_schedules", []).append({"start_hour": start_hour, "end_hour": end_hour, "days": day_list}) - await ctx.send(embed=ok_embed(f"Blackout {start_hour:02d}:00–{end_hour:02d}:00 UTC on {days}.")) - - @wh_bo.command(name="clear") - async def wh_bo_clear(self, ctx, name: str): - name = name.lower(); d = await self._net(name) - if not d: return - if not await self._is_staff(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - async with self.config.networks() as n: n[name]["blackout_schedules"] = []; n[name]["frozen"] = False - await ctx.send(embed=ok_embed("Blackout cleared & unfrozen.")) - - @wh_bo.command(name="list") - async def wh_bo_ls(self, ctx, name: str): - name = name.lower(); d = await self._net(name) - if not d: return - ss = d.get("blackout_schedules", []) - dn = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"] - lines = [f"{s['start_hour']:02d}:00–{s['end_hour']:02d}:00 — {','.join(dn[x] for x in s.get('days',[]))}" for s in ss] - await ctx.send(embed=info_embed("\n".join(lines) if lines else "None.", title=f"Blackout — {name}")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # BACKUP & RESTORE - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.command(name="backup") - async def wh_backup(self, ctx, name: str): - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - if not await self._is_owner(d, ctx.author.id) and not await self.bot.is_owner(ctx.author): return - data = {"name": name, "version": self.__version__, "exported": datetime.now(timezone.utc).isoformat(), "config": d} - f = discord.File(fp=io.BytesIO(json.dumps(data, indent=2, default=str).encode()), filename=f"wormhole-{name}.json") - await ctx.send(embed=ok_embed("Backup:"), file=f) - - @wh.command(name="restore") - async def wh_restore(self, ctx, new_name: str = None): - if not ctx.message.attachments: return await ctx.send(embed=err_embed("Attach a JSON file.")) - try: backup = json.loads((await ctx.message.attachments[0].read()).decode()) - except: return await ctx.send(embed=err_embed("Invalid JSON.")) - cfg = backup.get("config") - if not cfg: return await ctx.send(embed=err_embed("Missing config.")) - name = (new_name or backup.get("name", "restored")).lower() - if name in await self.config.networks(): return await ctx.send(embed=err_embed(f"**{name}** exists.")) - cfg["owner_id"] = ctx.author.id - cfg["channels"] = [] - cfg["portal_messages"] = {} - cfg["created_at"] = datetime.now(timezone.utc).isoformat() - await self._save(name, cfg) - self.cooldowns[name] = CooldownBucket(cfg.get("rate_limit_rate", 5), cfg.get("rate_limit_per", 10.0)) - self.dup_detectors[name] = DuplicateDetector() - self.raid_detectors[name] = RaidDetector() - await ctx.send(embed=ok_embed(f"**{name}** restored! Link channels with `{ctx.clean_prefix}wh open {name}`.")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # GLOBAL BLOCKLIST - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.group(name="global", invoke_without_command=True) - @checks.is_owner() - async def wh_gl(self, ctx): await ctx.send_help(ctx.command) - - @wh_gl.command(name="ban-user") - @checks.is_owner() - async def wh_gl_bu(self, ctx, user: discord.User): - async with self.config.global_banned_users() as bl: - if user.id not in bl: bl.append(user.id) - await ctx.send(embed=ok_embed(f"{user.mention} globally blocked.")) - - @wh_gl.command(name="unban-user") - @checks.is_owner() - async def wh_gl_uu(self, ctx, user: discord.User): - async with self.config.global_banned_users() as bl: - if user.id in bl: bl.remove(user.id) - await ctx.send(embed=ok_embed("Unblocked.")) - - @wh_gl.command(name="ban-server") - @checks.is_owner() - async def wh_gl_bs(self, ctx, gid: int): - async with self.config.global_banned_servers() as bl: - if gid not in bl: bl.append(gid) - await ctx.send(embed=ok_embed("Server globally blocked.")) - - @wh_gl.command(name="unban-server") - @checks.is_owner() - async def wh_gl_us(self, ctx, gid: int): - async with self.config.global_banned_servers() as bl: - if gid in bl: bl.remove(gid) - await ctx.send(embed=ok_embed("Unblocked.")) - - @wh_gl.command(name="list") - @checks.is_owner() - async def wh_gl_ls(self, ctx): - users = await self.config.global_banned_users() - servers = await self.config.global_banned_servers() - lines = [f"User: {self.bot.get_user(u) or u}" for u in users] + [f"Server: {self.bot.get_guild(g) or g}" for g in servers] - await ctx.send(embed=info_embed("\n".join(lines) if lines else "Empty.", title="Global Blocklist")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # PROFILES & STATS - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.command(name="profile") - async def wh_profile(self, ctx, name: str, user: discord.User = None): - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - user = user or ctx.author - p = d.get("user_profiles", {}).get(str(user.id)) - if not p: return await ctx.send(embed=info_embed(f"{user.display_name} has no activity on **{name}**.")) - em = discord.Embed(title=f"👤 {user.display_name} — {name}", colour=COLOUR_INFO) - em.set_thumbnail(url=user.display_avatar.url) - em.add_field(name="Messages", value=f"{p.get('messages',0):,}", inline=True) - em.add_field(name="First seen", value=p.get("first_seen","?")[:10], inline=True) - em.add_field(name="Servers", value=str(len(p.get("servers",[]))), inline=True) - karma = d.get("karma_scores", {}).get(str(user.id), 0) - if d.get("karma_enabled"): em.add_field(name="Karma", value=str(karma), inline=True) - status = [] - if user.id == d["owner_id"]: status.append("👑 Owner") - elif user.id in d.get("staff_ids", []): status.append("⭐ Staff") - if user.id in d.get("banned_users", []): status.append("🚫 Banned") - if user.id in d.get("muted_users", []): status.append("🔇 Muted") - if user.id in d.get("dm_subscribers", []): status.append("📧 DM Sub") - if status: em.add_field(name="Status", value=" ".join(status), inline=False) - await ctx.send(embed=em) - - @wh.command(name="stats") - async def wh_stats(self, ctx, name: str): - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - profiles = d.get("user_profiles", {}) - top = sorted(profiles.items(), key=lambda x: x[1].get("messages", 0), reverse=True)[:5] - em = discord.Embed(title=f"📊 {name}", colour=COLOUR_INFO) - em.add_field(name="Messages", value=f"{d.get('total_messages',0):,}", inline=True) - em.add_field(name="Channels", value=str(len(d["channels"])), inline=True) - em.add_field(name="Users", value=str(len(profiles)), inline=True) - em.add_field(name="DM subs", value=str(len(d.get("dm_subscribers",[]))), inline=True) - em.add_field(name="Mode", value=f"`{d.get('relay_mode','webhook')}`", inline=True) - em.add_field(name="Created", value=d.get("created_at","?")[:10], inline=True) - if top: - lb = [] - for i, (uid, pr) in enumerate(top): - u = self.bot.get_user(int(uid)) - lb.append(f"{'🥇🥈🥉'[i] if i < 3 else f'{i+1}.'} {u or uid} — {pr.get('messages',0):,}") - em.add_field(name="🏆 Top", value="\n".join(lb), inline=False) - await ctx.send(embed=em) - - @wh.command(name="rules") - async def wh_rules(self, ctx, name: str): - """Display network rules.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - rules = d.get("rules", "") - if not rules: return await ctx.send(embed=info_embed("No rules set.")) - await ctx.send(embed=info_embed(rules, title=f"📜 Rules — {name}")) - - @wh.command(name="motd") - async def wh_motd(self, ctx, name: str): - """Display the message of the day.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - motd = d.get("motd", "") - if not motd: return await ctx.send(embed=info_embed("No MOTD set.")) - await ctx.send(embed=info_embed(motd, title=f"📋 MOTD — {name}")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # SEARCH - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.command(name="search") - async def wh_search(self, ctx, name: str, *, query: str): - """Search recent messages across the network.""" - name = name.lower(); d = await self._net(name) - if not d: return await ctx.send(embed=err_embed("Not found.")) - results = [] - for ch_id in d["channels"][:10]: # Limit to 10 channels - ch = self.bot.get_channel(ch_id) - if not ch: continue - try: - async for msg in ch.history(limit=100): - if query.lower() in (msg.content or "").lower(): - results.append(f"**{msg.author.display_name}** in #{ch.name}: {truncate(msg.content, 80)}") - if len(results) >= 15: break - except: pass - if len(results) >= 15: break - await ctx.send(embed=info_embed("\n".join(results) if results else "No results.", title=f"🔍 Search — {name}")) - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # MESSAGE RELAY ENGINE - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @commands.Cog.listener() - async def on_message(self, message: discord.Message): - await self._ready.wait() - if not message.guild or message.author.bot: - return - if not isinstance(message.channel, (discord.TextChannel, discord.Thread)): - return - - eff_ch = message.channel.id - is_thread = isinstance(message.channel, discord.Thread) - if is_thread: - eff_ch = message.channel.parent_id - - nets = await self.config.networks() - net_name = None - nd = None - for n, d in nets.items(): - if eff_ch in d.get("channels", []): - net_name = n; nd = d; break - if not net_name: - return - - if is_thread and not nd.get("sync_threads"): return - if nd.get("frozen"): return - - # Ignore commands - pfx = await self.bot.get_prefix(message) - if isinstance(pfx, str): pfx = [pfx] - if any(message.content.startswith(p) for p in pfx): return - - # NSFW gate - if nd.get("nsfw_gate") and hasattr(message.channel, "is_nsfw") and message.channel.is_nsfw(): return - - # Global blocklist - if message.author.id in await self.config.global_banned_users(): return - if message.guild.id in await self.config.global_banned_servers(): return - - # Per-network checks - if message.author.id in nd.get("banned_users", []): return - if message.author.id in nd.get("muted_users", []): return - if message.guild.id in nd.get("banned_servers", []): return - if message.guild.id in nd.get("muted_servers", []): return - - # Content filter - if message.content: - if check_filters(message.content, nd.get("word_filters", []), nd.get("regex_filters", [])): - try: - await message.delete() - await message.channel.send(embed=warn_embed(f"{message.author.mention}, blocked by filter."), delete_after=5) - except: pass - return - - # Attachment filter - if message.attachments: - exts = set(nd.get("blocked_extensions", [])) - reason = check_attachment_filters(message.attachments, exts, nd.get("max_filesize")) - if reason: - try: - await message.delete() - await message.channel.send(embed=warn_embed(f"{message.author.mention}, {reason}"), delete_after=5) - except: pass - return - - # Auto-moderation - am = nd.get("automod", {}) - if am.get("enabled") and message.content: - if am.get("anti_spam"): - det = self.dup_detectors.get(net_name) - if det and det.is_duplicate(net_name, message.author.id, message.content): - try: await message.delete(); await message.channel.send(embed=warn_embed("Spam detected."), delete_after=5) - except: pass - await self._log(nd, warn_embed(f"Auto-mod spam: {message.author}")) - return - if am.get("anti_raid"): - rd = self.raid_detectors.get(net_name) - if rd and rd.record(net_name, message.author.id): - async with self.config.networks() as ns: - if net_name in ns: ns[net_name]["frozen"] = True - await self._log(nd, warn_embed(f"🚨 Raid detected! Network auto-frozen.")) - await self._status(net_name, nd, None, "🚨 **Raid detected — network auto-frozen!** Staff: use `wh set freeze false` to unfreeze.") - return - reason = check_automod(message.content, am) - if reason: - try: await message.delete(); await message.channel.send(embed=warn_embed(f"{message.author.mention}: {reason}"), delete_after=5) - except: pass - await self._log(nd, warn_embed(f"Auto-mod: {reason} — {message.author}")) - return - - # Rate limit - bucket = self.cooldowns.get(net_name) - if bucket and bucket.is_rate_limited(message.author.id, net_name): - try: await message.add_reaction("🕐") - except: pass - return - - # Slowmode - sm = nd.get("slowmode", 0) - if sm > 0: - last = self.slowmode_tracker.get(net_name, {}).get(message.author.id, 0) - if time.monotonic() - last < sm: - try: await message.add_reaction("🐌") - except: pass - return - self.slowmode_tracker.setdefault(net_name, {})[message.author.id] = time.monotonic() - - # Relay delay - delay = nd.get("relay_delay", 0) - if delay > 0: - await asyncio.sleep(min(delay, 30)) - - # Build payload - relay_mode = nd.get("relay_mode", "webhook") - nick = nd.get("server_nicknames", {}).get(str(message.guild.id)) - mc = nd.get("mention_control", {}) - content = sanitise_mentions(message.content or "", mc) - - # Reply context - if nd.get("sync_replies") and message.reference and message.reference.message_id: - try: - ref = message.reference.cached_message or await message.channel.fetch_message(message.reference.message_id) - preview = truncate(ref.content, 100) if ref.content else "*[attachment]*" - content = f"> **↩ {ref.author.display_name}:** {preview}\n{content}" - except: content = f"> ↩ *[reply]*\n{content}" - - # Stickers - if nd.get("sync_stickers") and message.stickers: - sl = [f"[Sticker: {s.name}]({s.url})" for s in message.stickers] - content = (content + "\n" if content else "") + "\n".join(sl) - - # Embeds - extra_embeds = [] - if nd.get("forward_embeds") and message.embeds: - extra_embeds = [e for e in message.embeds if e.type == "rich"] - - avatar = self._avatar(message, nd.get("image_mode", "user"), nd.get("custom_icon")) - uname = self._name(message, nd.get("name_mode", "both"), nd.get("custom_name"), nick) - - # Relay to channels - mapping: Dict[int, int] = {} - for ch_id in nd["channels"]: - if ch_id == eff_ch: continue - ch = self.bot.get_channel(ch_id) - if not ch: continue - ch_mode = self._get_override(nd, ch_id, "relay_mode") or relay_mode - try: - if ch_mode == "webhook": - wh = await self._wh(ch) - files = [] - for a in message.attachments: - try: files.append(await a.to_file()) - except: pass - sent = await wh.send(content=content or None, username=truncate(uname, 80), avatar_url=avatar, - files=files or discord.utils.MISSING, embeds=extra_embeds or discord.utils.MISSING, wait=True) - mapping[ch_id] = sent.id - elif ch_mode == "embed": - em = build_relay_embed(message, nick, nd.get("colour")) - sent = await ch.send(embeds=[em] + extra_embeds[:9]) - mapping[ch_id] = sent.id - elif ch_mode == "compact": - g = nick or message.guild.name - files = [] - for a in message.attachments: - try: files.append(await a.to_file()) - except: pass - sent = await ch.send(content=compact_format(g, message.author.display_name, content), files=files or None) - mapping[ch_id] = sent.id - except Exception as exc: - log.warning("Relay fail %s: %s", ch_id, exc) - - if mapping: - self.msg_map.add(net_name, message.id, mapping) - - # Stats + profile - async with self.config.networks() as ns: - if net_name in ns: ns[net_name]["total_messages"] = ns[net_name].get("total_messages", 0) + 1 - await self._update_profile(net_name, message.author, message.guild.id) - - # DM relay - await self._relay_to_dm_subs(net_name, nd, message) - - # Keyword highlights - await self._check_highlights(net_name, nd, message.content, message.author.id) - - # ── Edit sync ─────────────────────────────────────────────────────────── - - @commands.Cog.listener() - async def on_message_edit(self, before: discord.Message, after: discord.Message): - await self._ready.wait() - if not after.guild or after.author.bot or before.content == after.content: return - eff = after.channel.id - if isinstance(after.channel, discord.Thread): eff = after.channel.parent_id - net = await self._net_for_ch(eff) - if not net: return - nd = await self._net(net) - if not nd or not nd.get("sync_edits"): return - mapping = self.msg_map.get_relayed(net, after.id) - if not mapping: return - mode = nd.get("relay_mode", "webhook") - for ch_id, mid in mapping.items(): - ch = self.bot.get_channel(ch_id) - if not ch: continue - try: - cm = self._get_override(nd, ch_id, "relay_mode") or mode - if cm == "webhook": - wh = await self._wh(ch) - await wh.edit_message(mid, content=after.content or "") - elif cm == "embed": - msg = await ch.fetch_message(mid) - if msg.embeds: - e = msg.embeds[0]; e.description = after.content or "*[no text]*" - await msg.edit(embed=e) - else: - msg = await ch.fetch_message(mid) - cp = msg.content.find(":** ") - if cp != -1: await msg.edit(content=truncate(msg.content[:cp+4] + after.content, 2000)) - except: pass - - # ── Delete sync ───────────────────────────────────────────────────────── - - @commands.Cog.listener() - async def on_message_delete(self, message: discord.Message): - await self._ready.wait() - if not message.guild or message.author.bot: return - eff = message.channel.id - if isinstance(message.channel, discord.Thread): eff = message.channel.parent_id - net = await self._net_for_ch(eff) - if not net: return - nd = await self._net(net) - if not nd or not nd.get("sync_deletes"): return - mapping = self.msg_map.get_relayed(net, message.id) - if not mapping: return - mode = nd.get("relay_mode", "webhook") - for ch_id, mid in mapping.items(): - ch = self.bot.get_channel(ch_id) - if not ch: continue - try: - cm = self._get_override(nd, ch_id, "relay_mode") or mode - if cm == "webhook": - wh = await self._wh(ch) - await wh.delete_message(mid) - else: - msg = await ch.fetch_message(mid) - await msg.delete() - except: pass - - # ── Reaction sync + karma + starboard ────────────────────────────────── - - @commands.Cog.listener() - async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User): - await self._ready.wait() - if user.bot: return - msg = reaction.message - if not msg.guild: return - eff = msg.channel.id - if isinstance(msg.channel, discord.Thread): eff = msg.channel.parent_id - net = await self._net_for_ch(eff) - if not net: return - nd = await self._net(net) - if not nd: return - - # Reaction sync - if nd.get("sync_reactions"): - mapping = self.msg_map.get_relayed(net, msg.id) - if mapping: - for ch_id, mid in mapping.items(): - ch = self.bot.get_channel(ch_id) - if not ch: continue - try: - rm = await ch.fetch_message(mid) - await rm.add_reaction(reaction.emoji) - except: pass - - # Karma - if nd.get("karma_enabled") and str(reaction.emoji) == nd.get("karma_emoji", "👍"): - # Find the original author - orig_id = self.msg_map.get_original(net, msg.id) - target_msg = msg - if orig_id: - # This is a relayed message — find original author via profiles or just give to the content - pass # Karma for relayed messages: credit the original sender - if target_msg.author.id != user.id: # Can't karma yourself - uid = str(target_msg.author.id) - async with self.config.networks() as ns: - if net in ns: - ns[net].setdefault("karma_scores", {}) - ns[net]["karma_scores"][uid] = ns[net]["karma_scores"].get(uid, 0) + 1 - - # Starboard - if nd.get("starboard_enabled") and str(reaction.emoji) == "⭐": - threshold = nd.get("starboard_threshold", 3) - star_ch_id = nd.get("starboard_channel") - if not star_ch_id: return - star_ch = self.bot.get_channel(star_ch_id) - if not star_ch: return - - # Count total stars across all relayed copies + original - total_stars = 0 - for r in msg.reactions: - if str(r.emoji) == "⭐": - total_stars += r.count - - if total_stars >= threshold: - starred = nd.get("starred_messages", {}) - msg_key = str(msg.id) - img_url = None - if msg.attachments: - for a in msg.attachments: - if a.content_type and a.content_type.startswith("image/"): - img_url = a.url; break - em = build_star_embed( - msg.author.display_name, msg.author.display_avatar.url, - msg.content or "*[no text]*", total_stars, - msg.guild.name, msg.channel.name, img_url) - - if msg_key in starred: - # Update existing - try: - board_msg = await star_ch.fetch_message(starred[msg_key]["board_msg_id"]) - await board_msg.edit(embed=em) - async with self.config.networks() as ns: - if net in ns: ns[net]["starred_messages"][msg_key]["stars"] = total_stars - except: pass - else: - try: - board_msg = await star_ch.send(embed=em) - async with self.config.networks() as ns: - if net in ns: - ns[net].setdefault("starred_messages", {})[msg_key] = {"stars": total_stars, "board_msg_id": board_msg.id} - except: pass - - @commands.Cog.listener() - async def on_reaction_remove(self, reaction: discord.Reaction, user: discord.User): - await self._ready.wait() - if user.bot: return - msg = reaction.message - if not msg.guild: return - eff = msg.channel.id - if isinstance(msg.channel, discord.Thread): eff = msg.channel.parent_id - net = await self._net_for_ch(eff) - if not net: return - nd = await self._net(net) - if not nd or not nd.get("sync_reactions"): return - mapping = self.msg_map.get_relayed(net, msg.id) - if not mapping: return - for ch_id, mid in mapping.items(): - ch = self.bot.get_channel(ch_id) - if not ch: continue - try: - rm = await ch.fetch_message(mid) - await rm.remove_reaction(reaction.emoji, self.bot.user) - except: pass - - # ── Typing indicator sync ─────────────────────────────────────────────── - - @commands.Cog.listener() - async def on_typing(self, channel, user, when): - await self._ready.wait() - if user.bot or not hasattr(channel, "guild") or not channel.guild: - return - net = await self._net_for_ch(channel.id) - if not net: return - nd = await self._net(net) - if not nd or not nd.get("sync_typing"): return - for ch_id in nd["channels"]: - if ch_id == channel.id: continue - ch = self.bot.get_channel(ch_id) - if ch: - try: await ch.typing() - except: pass - - # ── Pin sync ──────────────────────────────────────────────────────────── - - @commands.Cog.listener() - async def on_guild_channel_pins_update(self, channel, last_pin): - await self._ready.wait() - if not hasattr(channel, "guild"): return - net = await self._net_for_ch(channel.id) - if not net: return - nd = await self._net(net) - if not nd or not nd.get("sync_pins"): return - try: - pins = await channel.pins() - if not pins: return - latest = pins[0] - mapping = self.msg_map.get_relayed(net, latest.id) - if not mapping: return - for ch_id, mid in mapping.items(): - ch = self.bot.get_channel(ch_id) - if not ch: continue - try: - msg = await ch.fetch_message(mid) - await msg.pin() - except: pass - except: pass - - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - # HELP - # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - @wh.command(name="help") - async def wh_help(self, ctx): - """Full command reference.""" - p = ctx.clean_prefix - e1 = discord.Embed(title="🌀 Wormhole v3.2.0 — Commands (1/3)", colour=COLOUR_NEUTRAL, - description="The ultimate cross-server relay: webhooks, DMs, embeds, starboard, auto-mod, invites, karma, portals, highlights, search & more.") - e1.add_field(name="📡 Networks", value=( - f"`{p}wh create/delete/open/close/list/info/stats`\n" - f"`{p}wh discover` — public networks\n" - f"`{p}wh transfer @user`\n" - f"`{p}wh announce ` — broadcast\n" - f"`{p}wh portal [name]` — status embed\n" - f"`{p}wh search `\n" - f"`{p}wh schedule `\n" - f"`{p}wh rules/motd ` — display"), inline=False) - e1.add_field(name="⚙️ Settings (`wh set`)", value=( - f"`relay-mode` `webhooks` `name-mode` `image-mode` `custom-icon` `custom-name`\n" - f"`description` `colour` `ratelimit` `slowmode` `relay-delay` `log-channel` `nickname`\n" - f"`welcome` `motd` `rules` `tags` `public` `max-filesize` `blocked-extensions`\n" - f"`freeze` `silent` `nsfw-gate` `sync-edits/deletes/reactions/replies/stickers/threads/pins/typing`\n" - f"`forward-embeds` `strip-everyone/roles/users` `channel-override`"), inline=False) - - e2 = discord.Embed(title="🌀 Commands (2/3)", colour=COLOUR_NEUTRAL) - e2.add_field(name="📧 DM Relay (`wh dm`)", value=( - f"`enable/disable `\n" - f"`mode `\n" - f"`subscribe/unsubscribe `\n" - f"`send ` — send from DM\n" - f"`list` — your subscriptions"), inline=True) - e2.add_field(name="⭐ Staff", value=f"`{p}wh staff add/remove/list`", inline=True) - e2.add_field(name="🔗 Invites (`wh invite`)", value=( - f"`create [max] [min]`\n" - f"`vanity `\n" - f"`use ` — join\n" - f"`revoke/list`"), inline=True) - e2.add_field(name="🛡️ Mod (`wh mod`)", value=( - f"`ban/unban/mute/unmute @user`\n" - f"`ban-server/unban-server/mute-server/unmute-server`\n" - f"`allowlist-add/remove` `purge [n]`"), inline=False) - e2.add_field(name="🤖 Auto-Mod (`wh automod`)", value=( - f"`enable/disable` `anti-spam/mentions/caps/invite/link`\n" - f"`anti-zalgo/spoiler/emote-spam/newlines/raid` `status`"), inline=False) - - e3 = discord.Embed(title="🌀 Commands (3/3)", colour=COLOUR_NEUTRAL) - e3.add_field(name="🔍 Filters", value="`add-word/remove-word` `add-regex/remove-regex` `list`", inline=True) - e3.add_field(name="⭐ Starboard", value=f"`{p}wh starboard enable #ch [threshold]`\n`disable`", inline=True) - e3.add_field(name="💎 Karma", value=f"`{p}wh karma enable/disable/check/leaderboard`", inline=True) - e3.add_field(name="🔔 Highlights", value=f"`{p}wh highlight add/remove/list `", inline=True) - e3.add_field(name="🌙 Blackout", value=f"`{p}wh blackout add/clear/list`", inline=True) - e3.add_field(name="💾 Backup", value=f"`{p}wh backup/restore`", inline=True) - e3.add_field(name="👤 Profiles", value=f"`{p}wh profile [@user]`", inline=True) - e3.add_field(name="📋 Audit", value=f"`{p}wh audit [count]`", inline=True) - e3.add_field(name="🌐 Global", value=f"`{p}wh global ban-user/unban-user/ban-server/unban-server/list`", inline=True) - e3.set_footer(text="Wormhole v3.2.0 • EveCogs • github.com/everestmcarthur/EveCogs") - - await ctx.send(embeds=[e1, e2, e3]) + # ━━━━━━━━━━━━━━━━━━━━━━â \ No newline at end of file