v3.2.0 — Phase 3: DM relay, starboard, karma, auto-mod, highlights, search & more
Complete ground-up rewrite (2,762 lines across 3 files): Phase 3 features: - Bidirectional DM relay (subscribe, send, receive, 3 formats) - Cross-network starboard with configurable threshold - Reputation/karma system with leaderboard - Keyword highlight notifications (DM alerts) - Anti-raid detection with auto-freeze - Attachment filters (blocked extensions, max filesize) - Pin sync across channels - Typing indicator relay - Network discovery (public flag + tags) - Message search across network - Scheduled messages (timed broadcasts) - Network-wide slowmode - Relay delay (configurable) - Vanity invite codes - Per-channel setting overrides - Purge command (mass delete relayed) - Audit log (500 entries, full trail) - MOTD and rules - Comprehensive README with full command reference Phase 2 features (carried forward): - Auto-moderation engine (spam, caps, invites, links, zalgo, spoiler, emote/newline spam) - Invite system with expiry and max uses - Staff announcements and portal messages - Welcome messages and mention control - User profiles and network stats - Backup/restore (JSON export/import) - Blackout scheduling - Global blocklist (bot owner) - Embed and compact relay modes Phase 1 features (carried forward): - Named networks with webhook relay - Edit/delete/reply/reaction/sticker sync - Staff system with ownership transfer - Word and regex content filters - Rate limiting and NSFW gate - Server nicknames, custom identity - Logging channel
This commit is contained in:
parent
6ca37b9ba1
commit
680b87db39
6 changed files with 2528 additions and 1203 deletions
250
README.md
250
README.md
|
|
@ -1,78 +1,222 @@
|
|||
# EveCogs
|
||||
# 🌀 EveCogs
|
||||
|
||||
> A curated collection of advanced, professional-grade cogs for [Red-DiscordBot](https://github.com/Cog-Creators/Red-DiscordBot).
|
||||
A premium cog repository for [Red-DiscordBot](https://github.com/Cog-Creators/Red-DiscordBot).
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
## Installation
|
||||
|
||||
```
|
||||
[p]repo add EveCogs https://github.com/everestmcarthur/EveCogs
|
||||
[p]cog install EveCogs <cog_name>
|
||||
[p]load <cog_name>
|
||||
[p]cog install EveCogs wormhole
|
||||
[p]load wormhole
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌀 Wormhole
|
||||
# 🌀 Wormhole v3.2.0
|
||||
|
||||
**The most advanced cross-server relay system for Red-DiscordBot.**
|
||||
**The ultimate cross-server message relay for Discord.**
|
||||
|
||||
Connect channels across unlimited servers into named networks. Every message, edit, delete, reaction, reply, sticker, embed, and file is relayed in real-time via webhooks — with full moderation, staff roles, filters, rate-limiting, and per-network customisation.
|
||||
Connect channels across multiple servers into named networks. Messages, edits, deletes, reactions, replies, stickers, pins, typing indicators, and files are relayed in real-time. Features DM relay, starboard, karma, auto-moderation, invites, portals, and 50+ commands.
|
||||
|
||||
### Highlights
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---|---|
|
||||
| **Named Networks** | Create unlimited named networks — each with its own config, staff, and channels |
|
||||
| **Webhook Relay** | Messages appear under the sender's name and avatar (or server icon, or custom) |
|
||||
| **Edit & Delete Sync** | Edits and deletes propagate across every linked channel instantly |
|
||||
| **Reply Context** | Replies include a preview of the referenced message |
|
||||
| **Reaction Sync** | Reactions mirror across channels |
|
||||
| **Sticker & Embed Forwarding** | Stickers, rich embeds, and file attachments are all relayed |
|
||||
| **Staff System** | Network owner + unlimited staff with moderation access |
|
||||
| **User Ban / Mute** | Ban or mute individual users from the network |
|
||||
| **Server Ban / Mute** | Block or silence entire servers |
|
||||
| **Server Allowlist** | Restrict which servers may join a network |
|
||||
| **Word & Regex Filters** | Auto-delete messages that match filters before relay |
|
||||
| **Rate Limiting** | Token-bucket per-user per-network spam protection |
|
||||
| **Logging Channel** | Designate a channel to receive moderation logs |
|
||||
| **Custom Identity** | Name mode (user / server / both / custom template), image mode, custom icon |
|
||||
| **Server Nicknames** | Override how your server's name appears on the network |
|
||||
| **Freeze / Pause** | Temporarily halt all relay on a network |
|
||||
| **Silent Mode** | Suppress join/leave announcements |
|
||||
| **NSFW Gating** | Block relay from NSFW channels |
|
||||
| **Thread Support** | Optionally relay messages from threads |
|
||||
| **Ownership Transfer** | Hand a network to another user |
|
||||
| **Statistics** | Track total messages relayed, channels linked, and more |
|
||||
### 📡 Core Relay
|
||||
- **Named networks** — create unlimited named relay networks
|
||||
- **Three relay modes** — `webhook` (rich identity), `embed` (styled cards), `compact` (minimal text)
|
||||
- **Full sync** — edits, deletes, reactions, replies, stickers, threads, pins, typing indicators
|
||||
- **File forwarding** — images, videos, documents all relayed automatically
|
||||
- **Embed forwarding** — rich embeds from bots/links passed through
|
||||
- **Reply context** — quoted reply previews carried across servers
|
||||
|
||||
### Quick Start
|
||||
### 📧 DM Relay
|
||||
- **Subscribe** to receive network messages directly in your DMs
|
||||
- **Send from DMs** — participate in networks without being in a server channel
|
||||
- **Three DM formats** — embed, compact, or plain text
|
||||
- **Bidirectional** — full two-way communication via DMs
|
||||
|
||||
### 🛡️ Moderation
|
||||
- **Ban / mute** individual users or entire servers
|
||||
- **Word filters** — block specific words
|
||||
- **Regex filters** — pattern-based content blocking
|
||||
- **Allowlist** — restrict which servers can join
|
||||
- **Purge** — mass delete relayed messages across all channels
|
||||
- **Audit log** — full trail of all admin/mod actions (last 500 entries)
|
||||
|
||||
### 🤖 Auto-Moderation Engine
|
||||
- **Anti-spam** — duplicate message detection with configurable window/threshold
|
||||
- **Anti-mention spam** — block mass mentions
|
||||
- **Anti-caps** — configurable caps percentage threshold
|
||||
- **Anti-invite** — block Discord invite links
|
||||
- **Anti-link** — block all URLs
|
||||
- **Anti-zalgo** — block combining character abuse
|
||||
- **Anti-spoiler spam** — block excessive spoiler tags
|
||||
- **Anti-emote spam** — limit custom emoji per message
|
||||
- **Anti-newline spam** — limit vertical space abuse
|
||||
- **Anti-raid** — auto-freeze network on flood detection
|
||||
|
||||
### ⭐ Starboard
|
||||
- Cross-network starboard — star reactions collected across all channels
|
||||
- Configurable threshold and dedicated starboard channel
|
||||
- Auto-updating star count on the board embed
|
||||
|
||||
### 💎 Karma / Reputation
|
||||
- React with a configurable emoji to give karma
|
||||
- Per-user karma scores tracked across the network
|
||||
- Leaderboard command
|
||||
|
||||
### 🔔 Keyword Highlights
|
||||
- Subscribe to keywords — get DM'd when they're mentioned
|
||||
- Per-user, per-network keyword lists
|
||||
|
||||
### 🔗 Invites
|
||||
- **Invite codes** — shareable codes with optional max uses and expiry
|
||||
- **Vanity invites** — custom words (e.g. `gaming` instead of `xK4mR2pQ`)
|
||||
- Join networks from any server with just a code
|
||||
|
||||
### 📢 Communication
|
||||
- **Staff announcements** — broadcast to all channels + DM subscribers
|
||||
- **Scheduled messages** — send timed announcements
|
||||
- **MOTD** — message of the day shown on join
|
||||
- **Rules** — network rules displayed on demand
|
||||
- **Welcome messages** — greet new servers automatically
|
||||
|
||||
### 🌀 Portal Messages
|
||||
- Persistent, auto-updating status embeds pinned in channels
|
||||
- Shows network status, channel count, features, MOTD
|
||||
- Refreshes every 5 minutes automatically
|
||||
|
||||
### 🔍 Search & Discovery
|
||||
- **Search** — search recent messages across the entire network
|
||||
- **Network discovery** — list public networks with tags
|
||||
- **Tags** — categorize networks for easy discovery
|
||||
|
||||
### ⚙️ Advanced Configuration
|
||||
- **Per-channel overrides** — different relay mode per channel
|
||||
- **Server nicknames** — custom display names per server
|
||||
- **Custom identity** — custom icons, names, colour accents
|
||||
- **Name modes** — show user, server, both, or custom template
|
||||
- **Mention control** — strip @everyone, @here, @role, @user
|
||||
- **Attachment filters** — block file types and enforce size limits
|
||||
- **Relay delay** — configurable seconds before relay (for review)
|
||||
- **Slowmode** — network-wide per-user cooldown
|
||||
- **Rate limiting** — configurable messages per time window
|
||||
- **NSFW gate** — block relay from NSFW channels
|
||||
- **Freeze** — pause a network instantly
|
||||
- **Silent mode** — suppress join/leave notifications
|
||||
|
||||
### 🌙 Blackout Scheduling
|
||||
- Auto-freeze/unfreeze on schedule
|
||||
- Per-day-of-week and hour range (UTC)
|
||||
- Multiple schedules per network
|
||||
|
||||
### 💾 Backup & Restore
|
||||
- Export entire network config as JSON
|
||||
- Restore to same or different name
|
||||
- Preserves all settings, filters, profiles
|
||||
|
||||
### 👤 User Profiles & Stats
|
||||
- Per-user message counts, first-seen date, server list
|
||||
- Network-wide statistics with top-5 leaderboard
|
||||
- Karma integration in profiles
|
||||
|
||||
### 🌐 Global Blocklist (Bot Owner)
|
||||
- Block users/servers from ALL wormhole networks
|
||||
- Independent of per-network moderation
|
||||
|
||||
### ⭐ Staff System
|
||||
- **Owner** — full control, can add/remove staff
|
||||
- **Staff** — moderation, settings, filters, announcements
|
||||
- **Ownership transfer** — hand off networks to other users
|
||||
|
||||
## 📋 Quick Start
|
||||
|
||||
```
|
||||
[p]wh create my-network A cool global chat
|
||||
[p]wh open my-network (run in each channel you want linked)
|
||||
[p]wh set webhooks my-network true
|
||||
[p]wh set name-mode my-network both
|
||||
[p]wh info my-network
|
||||
[p]wh help
|
||||
[p]wh create mynetwork A cool cross-server chat!
|
||||
[p]wh open mynetwork # in each channel you want linked
|
||||
[p]wh invite create mynetwork # share the code
|
||||
[p]wh dm enable mynetwork # enable DM relay
|
||||
[p]wh set relay-mode mynetwork webhook
|
||||
[p]wh automod enable mynetwork
|
||||
[p]wh starboard enable mynetwork #starboard 3
|
||||
[p]wh karma enable mynetwork 👍
|
||||
```
|
||||
|
||||
### Command Overview
|
||||
## 📖 Command Reference
|
||||
|
||||
| Group | Commands |
|
||||
|---|---|
|
||||
| `[p]wh` | `create`, `delete`, `open`, `close`, `list`, `info`, `stats`, `transfer`, `help` |
|
||||
| `[p]wh set` | `webhooks`, `name-mode`, `image-mode`, `custom-icon`, `custom-name`, `description`, `colour`, `ratelimit`, `log-channel`, `nickname`, `freeze`, `silent`, `nsfw-gate`, `sync-edits`, `sync-deletes`, `sync-reactions`, `sync-replies`, `sync-stickers`, `sync-threads`, `forward-embeds` |
|
||||
| `[p]wh staff` | `add`, `remove`, `list` |
|
||||
| `[p]wh mod` | `ban`, `unban`, `mute`, `unmute`, `ban-server`, `unban-server`, `mute-server`, `unmute-server`, `allowlist-add`, `allowlist-remove` |
|
||||
| `[p]wh filter` | `add-word`, `remove-word`, `add-regex`, `remove-regex`, `list` |
|
||||
Run `[p]wh help` in Discord for the full interactive reference (3-page embed).
|
||||
|
||||
---
|
||||
### Network Management
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `wh create <name> [desc]` | Create a new network |
|
||||
| `wh delete <name>` | Delete a network (with confirmation) |
|
||||
| `wh open <name>` | Link current channel |
|
||||
| `wh close [name]` | Unlink current channel |
|
||||
| `wh list` | List all networks |
|
||||
| `wh discover` | List public networks |
|
||||
| `wh info <name>` | Detailed network info |
|
||||
| `wh stats <name>` | Statistics & leaderboard |
|
||||
| `wh transfer <name> @user` | Transfer ownership |
|
||||
| `wh announce <name> <msg>` | Staff broadcast |
|
||||
| `wh portal [name]` | Create/refresh portal |
|
||||
| `wh search <name> <query>` | Search messages |
|
||||
| `wh schedule <name> <min> <msg>` | Scheduled message |
|
||||
| `wh rules <name>` | Show rules |
|
||||
| `wh motd <name>` | Show MOTD |
|
||||
| `wh audit <name> [n]` | View audit log |
|
||||
| `wh backup <name>` | Export JSON backup |
|
||||
| `wh restore [name]` | Import from JSON |
|
||||
|
||||
### DM Relay
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `wh dm enable/disable <name>` | Toggle DM relay |
|
||||
| `wh dm mode <name> <format>` | Set DM format |
|
||||
| `wh dm subscribe <name>` | Subscribe to DMs |
|
||||
| `wh dm unsubscribe <name>` | Unsubscribe |
|
||||
| `wh dm send <name> <msg>` | Send via DM |
|
||||
| `wh dm list` | Your subscriptions |
|
||||
|
||||
### Settings (`wh set`)
|
||||
`relay-mode` · `webhooks` · `name-mode` · `image-mode` · `custom-icon` · `custom-name` · `description` · `colour` · `ratelimit` · `slowmode` · `relay-delay` · `log-channel` · `nickname` · `welcome` · `motd` · `rules` · `tags` · `public` · `max-filesize` · `blocked-extensions` · `freeze` · `silent` · `nsfw-gate` · `sync-edits` · `sync-deletes` · `sync-reactions` · `sync-replies` · `sync-stickers` · `sync-threads` · `sync-pins` · `sync-typing` · `forward-embeds` · `strip-everyone` · `strip-roles` · `strip-users` · `channel-override`
|
||||
|
||||
### Moderation (`wh mod`)
|
||||
`ban/unban` · `mute/unmute` · `ban-server/unban-server` · `mute-server/unmute-server` · `allowlist-add/remove` · `purge`
|
||||
|
||||
### Auto-Moderation (`wh automod`)
|
||||
`enable/disable` · `anti-spam` · `anti-mentions` · `anti-caps` · `anti-invite` · `anti-link` · `anti-zalgo` · `anti-spoiler` · `anti-emote-spam` · `anti-newlines` · `anti-raid` · `status`
|
||||
|
||||
### More
|
||||
| Category | Commands |
|
||||
|----------|----------|
|
||||
| **Staff** | `wh staff add/remove/list` |
|
||||
| **Invites** | `wh invite create/vanity/use/revoke/list` |
|
||||
| **Filters** | `wh filter add-word/remove-word/add-regex/remove-regex/list` |
|
||||
| **Starboard** | `wh starboard enable/disable` |
|
||||
| **Karma** | `wh karma enable/disable/check/leaderboard` |
|
||||
| **Highlights** | `wh highlight add/remove/list` |
|
||||
| **Blackout** | `wh blackout add/clear/list` |
|
||||
| **Profiles** | `wh profile <name> [@user]` |
|
||||
| **Global** | `wh global ban-user/unban-user/ban-server/unban-server/list` |
|
||||
|
||||
## 📊 Architecture
|
||||
|
||||
```
|
||||
EveCogs/
|
||||
├── README.md
|
||||
├── LICENSE (MIT)
|
||||
├── info.json (repo metadata)
|
||||
└── wormhole/
|
||||
├── __init__.py (cog loader + data statement)
|
||||
├── info.json (cog metadata)
|
||||
├── utils.py (helpers, embeds, detectors, filters)
|
||||
└── wormhole.py (main cog — 2,200+ lines, 50+ commands)
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT — do whatever you want.
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
|
||||
---
|
||||
## 🤝 Credits
|
||||
|
||||
*Made with ❤️ by everestmcarthur*
|
||||
Built by [everestmcarthur](https://github.com/everestmcarthur) with assistance from Viktor AI.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
"author": ["everestmcarthur"],
|
||||
"description": "EveCogs — a curated collection of advanced, professional-grade cogs for Red-DiscordBot.",
|
||||
"install_msg": "Thank you for installing **EveCogs**! Browse available cogs with `[p]cog list` and install what you need.",
|
||||
"short": "Advanced cogs for Red-DiscordBot"
|
||||
"install_msg": "Welcome to **EveCogs**! Use `[p]cog list` to see available cogs.",
|
||||
"name": "EveCogs",
|
||||
"short": "A collection of advanced cogs for Red-DiscordBot.",
|
||||
"description": "EveCogs is a premium cog repository featuring Wormhole — the ultimate cross-server relay system with DM relay, starboard, karma, auto-moderation, invites, portals, and 50+ commands.",
|
||||
"type": "REPO"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
"""Wormhole — the ultimate cross-server relay cog for Red-DiscordBot."""
|
||||
|
||||
from .wormhole import Wormhole
|
||||
|
||||
__red_end_user_data_statement__ = (
|
||||
"This cog stores Discord user IDs, guild IDs, and channel IDs for relay functionality. "
|
||||
"Per-user data includes message counts, karma scores, keyword highlights, and DM relay subscriptions. "
|
||||
"Users may request deletion of their data via the bot owner."
|
||||
)
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
cog = Wormhole(bot)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,28 @@
|
|||
{
|
||||
"author": ["everestmcarthur"],
|
||||
"description": "Wormhole — an advanced cross-server/cross-channel communication relay with named networks, moderation, staff roles, message sync (edits/deletes/replies/reactions), per-network customisation, logging, statistics, filters, rate-limiting, and much more.",
|
||||
"install_msg": "Thanks for installing **Wormhole**! Run `[p]wh help` to get started.",
|
||||
"short": "Advanced cross-server relay with networks, moderation & full sync",
|
||||
"end_user_data_statement": "This cog stores user IDs, guild IDs, channel IDs, and message-mapping data required for relay functionality. No message content is stored persistently.",
|
||||
"install_msg": "Thanks for installing **Wormhole v3.2.0** — the ultimate cross-server relay!\n\nGet started: `[p]wh create <network-name>` then `[p]wh open <network-name>` in channels you want to link.\n\nFull help: `[p]wh help`",
|
||||
"name": "Wormhole",
|
||||
"disabled": false,
|
||||
"short": "The ultimate cross-server message relay with DM relay, starboard, karma, auto-mod, invites & more.",
|
||||
"description": "Wormhole v3.2.0 connects Discord channels across multiple servers into named networks. Messages, edits, deletes, reactions, replies, stickers, pins, typing indicators, and files are all relayed in real-time via webhooks, embeds, or compact text. Features include: bidirectional DM relay, cross-network starboard, reputation/karma system, keyword highlight notifications, auto-moderation engine (spam/caps/invites/links/zalgo/raid detection), network invite codes with vanity URLs, staff announcements, persistent portal embeds, user profiles and stats, backup/restore, blackout scheduling, message search, scheduled messages, per-channel overrides, network discovery, relay delay, slowmode, purge, audit log, attachment filters, and a global blocklist.",
|
||||
"tags": [
|
||||
"relay",
|
||||
"crossserver",
|
||||
"wormhole",
|
||||
"chat",
|
||||
"bridge",
|
||||
"network",
|
||||
"webhook",
|
||||
"moderation",
|
||||
"automod",
|
||||
"starboard",
|
||||
"karma",
|
||||
"dm",
|
||||
"global"
|
||||
],
|
||||
"type": "COG",
|
||||
"end_user_data_statement": "This cog stores Discord user IDs, guild IDs, and channel IDs for relay functionality. Per-user data includes message counts, karma scores, keyword highlights, and DM relay subscriptions.",
|
||||
"min_bot_version": "3.5.0",
|
||||
"min_python_version": [3, 9, 0],
|
||||
"tags": [
|
||||
"wormhole",
|
||||
"relay",
|
||||
"bridge",
|
||||
"cross-server",
|
||||
"network",
|
||||
"moderation",
|
||||
"webhook"
|
||||
]
|
||||
"requirements": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,50 @@
|
|||
"""Utility helpers for Wormhole cog."""
|
||||
"""
|
||||
Utility helpers for Wormhole v3.2.0 — the ultimate cross-server relay cog.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Optional
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
import discord
|
||||
|
||||
# ── Colour palette ──────────────────────────────────────────────────────────
|
||||
|
||||
COLOUR_OK = discord.Colour.from_rgb(87, 242, 135) # green
|
||||
COLOUR_WARN = discord.Colour.from_rgb(254, 231, 92) # yellow
|
||||
COLOUR_ERR = discord.Colour.from_rgb(237, 66, 69) # red
|
||||
COLOUR_INFO = discord.Colour.from_rgb(88, 101, 242) # blurple
|
||||
COLOUR_NEUTRAL = discord.Colour.from_rgb(114, 137, 218) # light blurple
|
||||
COLOUR_OK = discord.Colour.from_rgb(87, 242, 135)
|
||||
COLOUR_WARN = discord.Colour.from_rgb(254, 231, 92)
|
||||
COLOUR_ERR = discord.Colour.from_rgb(237, 66, 69)
|
||||
COLOUR_INFO = discord.Colour.from_rgb(88, 101, 242)
|
||||
COLOUR_NEUTRAL = discord.Colour.from_rgb(114, 137, 218)
|
||||
COLOUR_ANNOUNCE = discord.Colour.from_rgb(255, 163, 26)
|
||||
COLOUR_STAR = discord.Colour.from_rgb(255, 172, 51)
|
||||
COLOUR_DM = discord.Colour.from_rgb(87, 165, 242)
|
||||
|
||||
_SERVER_COLOURS = [
|
||||
discord.Colour.from_rgb(231, 76, 60),
|
||||
discord.Colour.from_rgb(46, 204, 113),
|
||||
discord.Colour.from_rgb(52, 152, 219),
|
||||
discord.Colour.from_rgb(155, 89, 182),
|
||||
discord.Colour.from_rgb(241, 196, 15),
|
||||
discord.Colour.from_rgb(26, 188, 156),
|
||||
discord.Colour.from_rgb(230, 126, 34),
|
||||
discord.Colour.from_rgb(233, 30, 99),
|
||||
discord.Colour.from_rgb(0, 188, 212),
|
||||
discord.Colour.from_rgb(139, 195, 74),
|
||||
discord.Colour.from_rgb(255, 87, 34),
|
||||
discord.Colour.from_rgb(121, 85, 72),
|
||||
]
|
||||
|
||||
|
||||
def server_colour(guild_id: int) -> discord.Colour:
|
||||
return _SERVER_COLOURS[guild_id % len(_SERVER_COLOURS)]
|
||||
|
||||
|
||||
# ── Cooldown bucket ────────────────────────────────────────────────────────
|
||||
|
|
@ -31,9 +60,7 @@ class CooldownBucket:
|
|||
def is_rate_limited(self, user_id: int, network: str) -> bool:
|
||||
key = f"{network}:{user_id}"
|
||||
now = time.monotonic()
|
||||
bucket = self._buckets[key]
|
||||
# Purge old timestamps
|
||||
self._buckets[key] = [t for t in bucket if now - t < self.per]
|
||||
self._buckets[key] = [t for t in self._buckets[key] if now - t < self.per]
|
||||
if len(self._buckets[key]) >= self.rate:
|
||||
return True
|
||||
self._buckets[key].append(now)
|
||||
|
|
@ -47,7 +74,6 @@ class CooldownBucket:
|
|||
# ── Filter helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
def check_filters(content: str, word_filters: list[str], regex_filters: list[str]) -> Optional[str]:
|
||||
"""Return the first matched filter pattern, or None if clean."""
|
||||
lowered = content.lower()
|
||||
for word in word_filters:
|
||||
if word.lower() in lowered:
|
||||
|
|
@ -61,32 +87,286 @@ def check_filters(content: str, word_filters: list[str], regex_filters: list[str
|
|||
return None
|
||||
|
||||
|
||||
# ── Auto-moderation helpers ─────────────────────────────────────────────────
|
||||
|
||||
INVITE_PATTERN = re.compile(
|
||||
r"(discord\.gg|discord\.com/invite|discordapp\.com/invite|dsc\.gg)/[\w-]+",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
LINK_PATTERN = re.compile(r"https?://\S+", re.IGNORECASE)
|
||||
MENTION_PATTERN = re.compile(r"<@!?\d+>|<@&\d+>|@everyone|@here")
|
||||
|
||||
BLOCKED_EXTENSIONS = {
|
||||
".exe", ".bat", ".cmd", ".scr", ".pif", ".msi", ".com",
|
||||
".vbs", ".vbe", ".js", ".jse", ".wsf", ".wsh", ".ps1",
|
||||
}
|
||||
|
||||
|
||||
def check_automod(content: str, automod_config: dict) -> Optional[str]:
|
||||
if not automod_config:
|
||||
return None
|
||||
if automod_config.get("anti_invite") and INVITE_PATTERN.search(content):
|
||||
return "Discord invite link detected"
|
||||
if automod_config.get("anti_link") and LINK_PATTERN.search(content):
|
||||
return "Link detected"
|
||||
if automod_config.get("anti_mention_spam"):
|
||||
max_mentions = automod_config.get("max_mentions", 5)
|
||||
if len(MENTION_PATTERN.findall(content)) > max_mentions:
|
||||
return f"Too many mentions"
|
||||
if automod_config.get("anti_caps"):
|
||||
threshold = automod_config.get("caps_threshold", 0.7)
|
||||
alpha = [c for c in content if c.isalpha()]
|
||||
if len(alpha) >= 10:
|
||||
ratio = sum(1 for c in alpha if c.isupper()) / len(alpha)
|
||||
if ratio >= threshold:
|
||||
return f"Excessive caps ({ratio:.0%})"
|
||||
if automod_config.get("anti_zalgo"):
|
||||
# Detect zalgo text (combining characters)
|
||||
combining = sum(1 for c in content if '\u0300' <= c <= '\u036f' or '\u0489' <= c <= '\u0489')
|
||||
if combining > 10:
|
||||
return "Zalgo text detected"
|
||||
if automod_config.get("anti_spoiler"):
|
||||
spoiler_count = content.count("||")
|
||||
if spoiler_count >= 6: # 3+ spoiler blocks
|
||||
return "Excessive spoiler tags"
|
||||
if automod_config.get("anti_emote_spam"):
|
||||
max_emotes = automod_config.get("max_emotes", 10)
|
||||
emote_count = len(re.findall(r"<a?:\w+:\d+>", content))
|
||||
if emote_count > max_emotes:
|
||||
return f"Too many custom emotes ({emote_count})"
|
||||
if automod_config.get("anti_newline_spam"):
|
||||
max_newlines = automod_config.get("max_newlines", 15)
|
||||
if content.count("\n") > max_newlines:
|
||||
return "Too many newlines"
|
||||
return None
|
||||
|
||||
|
||||
def check_attachment_filters(
|
||||
attachments: list,
|
||||
blocked_exts: Optional[set] = None,
|
||||
max_filesize: Optional[int] = None,
|
||||
) -> Optional[str]:
|
||||
"""Check attachments against extension and size filters."""
|
||||
exts = blocked_exts or set()
|
||||
for att in attachments:
|
||||
name_lower = att.filename.lower()
|
||||
for ext in exts:
|
||||
if name_lower.endswith(ext):
|
||||
return f"Blocked file type: {ext}"
|
||||
if max_filesize and att.size > max_filesize:
|
||||
mb = max_filesize / (1024 * 1024)
|
||||
return f"File too large (max {mb:.1f} MB)"
|
||||
return None
|
||||
|
||||
|
||||
class DuplicateDetector:
|
||||
"""Detects duplicate/spam messages per user per network."""
|
||||
|
||||
def __init__(self, window: float = 30.0, threshold: int = 3):
|
||||
self.window = window
|
||||
self.threshold = threshold
|
||||
self._history: Dict[Tuple[str, int], List[Tuple[str, float]]] = defaultdict(list)
|
||||
|
||||
def is_duplicate(self, network: str, user_id: int, content: str) -> bool:
|
||||
key = (network, user_id)
|
||||
now = time.monotonic()
|
||||
content_hash = hashlib.md5(content.lower().strip().encode()).hexdigest()
|
||||
self._history[key] = [(h, t) for h, t in self._history[key] if now - t < self.window]
|
||||
dup_count = sum(1 for h, _ in self._history[key] if h == content_hash)
|
||||
self._history[key].append((content_hash, now))
|
||||
if len(self._history[key]) > 50:
|
||||
self._history[key] = self._history[key][-25:]
|
||||
return dup_count >= self.threshold
|
||||
|
||||
def update(self, window: float, threshold: int):
|
||||
self.window = window
|
||||
self.threshold = threshold
|
||||
|
||||
|
||||
class RaidDetector:
|
||||
"""Detects raid patterns: many new unique users posting in short window."""
|
||||
|
||||
def __init__(self, window: float = 60.0, user_threshold: int = 10):
|
||||
self.window = window
|
||||
self.user_threshold = user_threshold
|
||||
# {network: [(user_id, timestamp), ...]}
|
||||
self._activity: Dict[str, List[Tuple[int, float]]] = defaultdict(list)
|
||||
|
||||
def record(self, network: str, user_id: int) -> bool:
|
||||
"""Record activity. Returns True if raid detected."""
|
||||
now = time.monotonic()
|
||||
self._activity[network] = [
|
||||
(uid, t) for uid, t in self._activity[network] if now - t < self.window
|
||||
]
|
||||
self._activity[network].append((user_id, now))
|
||||
unique = len(set(uid for uid, _ in self._activity[network]))
|
||||
return unique >= self.user_threshold
|
||||
|
||||
|
||||
# ── Invite code generation ──────────────────────────────────────────────────
|
||||
|
||||
def generate_invite_code(length: int = 8) -> str:
|
||||
chars = string.ascii_letters + string.digits
|
||||
return "".join(random.choices(chars, k=length))
|
||||
|
||||
|
||||
# ── Mention sanitisation ───────────────────────────────────────────────────
|
||||
|
||||
def sanitise_mentions(content: str, config: dict) -> str:
|
||||
if config.get("strip_everyone"):
|
||||
content = content.replace("@everyone", "@\u200beveryone").replace("@here", "@\u200bhere")
|
||||
if config.get("strip_role_mentions"):
|
||||
content = re.sub(r"<@&(\d+)>", r"@role", content)
|
||||
if config.get("strip_user_mentions"):
|
||||
content = re.sub(r"<@!?(\d+)>", r"@user", content)
|
||||
return content
|
||||
|
||||
|
||||
# ── Embed builders ──────────────────────────────────────────────────────────
|
||||
|
||||
def make_embed(
|
||||
title: str = "",
|
||||
description: str = "",
|
||||
colour: discord.Colour = COLOUR_INFO,
|
||||
**kwargs,
|
||||
def make_embed(title: str = "", description: str = "", colour: discord.Colour = COLOUR_INFO, **kw) -> discord.Embed:
|
||||
return discord.Embed(title=title, description=description, colour=colour, **kw)
|
||||
|
||||
def ok_embed(desc: str, title: str = "✅ Success") -> discord.Embed:
|
||||
return make_embed(title=title, description=desc, colour=COLOUR_OK)
|
||||
|
||||
def err_embed(desc: str, title: str = "❌ Error") -> discord.Embed:
|
||||
return make_embed(title=title, description=desc, colour=COLOUR_ERR)
|
||||
|
||||
def warn_embed(desc: str, title: str = "⚠️ Warning") -> discord.Embed:
|
||||
return make_embed(title=title, description=desc, colour=COLOUR_WARN)
|
||||
|
||||
def info_embed(desc: str, title: str = "ℹ️ Info") -> discord.Embed:
|
||||
return make_embed(title=title, description=desc, colour=COLOUR_INFO)
|
||||
|
||||
def announce_embed(desc: str, title: str = "📢 Announcement") -> discord.Embed:
|
||||
return make_embed(title=title, description=desc, colour=COLOUR_ANNOUNCE)
|
||||
|
||||
def star_embed(desc: str, title: str = "⭐ Starred") -> discord.Embed:
|
||||
return make_embed(title=title, description=desc, colour=COLOUR_STAR)
|
||||
|
||||
def dm_embed(desc: str, title: str = "✉️ DM Relay") -> discord.Embed:
|
||||
return make_embed(title=title, description=desc, colour=COLOUR_DM)
|
||||
|
||||
|
||||
# ── Embed relay builder ────────────────────────────────────────────────────
|
||||
|
||||
def build_relay_embed(
|
||||
message: discord.Message,
|
||||
server_nick: Optional[str] = None,
|
||||
net_colour: Optional[int] = None,
|
||||
) -> discord.Embed:
|
||||
em = discord.Embed(title=title, description=description, colour=colour, **kwargs)
|
||||
guild_label = server_nick or message.guild.name
|
||||
colour = discord.Colour(net_colour) if net_colour else server_colour(message.guild.id)
|
||||
|
||||
em = discord.Embed(description=message.content or "*[no text]*", colour=colour, timestamp=message.created_at)
|
||||
em.set_author(name=message.author.display_name, icon_url=message.author.display_avatar.url)
|
||||
footer_icon = message.guild.icon.url if message.guild.icon else discord.Embed.Empty
|
||||
em.set_footer(text=f"🌐 {guild_label} • #{message.channel.name}", icon_url=footer_icon)
|
||||
|
||||
if message.attachments:
|
||||
img_set = False
|
||||
file_lines = []
|
||||
for att in message.attachments:
|
||||
if att.content_type and att.content_type.startswith("image/") and not img_set:
|
||||
em.set_image(url=att.url)
|
||||
img_set = True
|
||||
else:
|
||||
file_lines.append(f"[{att.filename}]({att.url})")
|
||||
if file_lines:
|
||||
em.add_field(name="📎 Attachments", value="\n".join(file_lines), inline=False)
|
||||
|
||||
if message.stickers:
|
||||
em.add_field(name="🎨 Stickers", value=", ".join(f"[{s.name}]({s.url})" for s in message.stickers), inline=False)
|
||||
|
||||
return em
|
||||
|
||||
|
||||
def ok_embed(description: str, title: str = "✅ Success") -> discord.Embed:
|
||||
return make_embed(title=title, description=description, colour=COLOUR_OK)
|
||||
def build_dm_relay_embed(
|
||||
user: discord.User,
|
||||
content: str,
|
||||
network_name: str,
|
||||
) -> discord.Embed:
|
||||
"""Build an embed for messages sent from DM to the network."""
|
||||
em = discord.Embed(description=content, colour=COLOUR_DM, timestamp=datetime.now(timezone.utc))
|
||||
em.set_author(name=f"{user.display_name} (via DM)", icon_url=user.display_avatar.url)
|
||||
em.set_footer(text=f"✉️ DM → {network_name}")
|
||||
return em
|
||||
|
||||
|
||||
def err_embed(description: str, title: str = "❌ Error") -> discord.Embed:
|
||||
return make_embed(title=title, description=description, colour=COLOUR_ERR)
|
||||
def build_dm_incoming_embed(
|
||||
author_name: str,
|
||||
author_avatar: str,
|
||||
server_name: str,
|
||||
channel_name: str,
|
||||
content: str,
|
||||
network_name: str,
|
||||
net_colour: Optional[int] = None,
|
||||
) -> discord.Embed:
|
||||
"""Build an embed for network messages forwarded to DM subscribers."""
|
||||
colour = discord.Colour(net_colour) if net_colour else COLOUR_INFO
|
||||
em = discord.Embed(description=content or "*[no text]*", colour=colour, timestamp=datetime.now(timezone.utc))
|
||||
em.set_author(name=author_name, icon_url=author_avatar)
|
||||
em.set_footer(text=f"🌐 {server_name} • #{channel_name} • {network_name}")
|
||||
return em
|
||||
|
||||
|
||||
def warn_embed(description: str, title: str = "⚠️ Warning") -> discord.Embed:
|
||||
return make_embed(title=title, description=description, colour=COLOUR_WARN)
|
||||
# ── Portal embed builder ───────────────────────────────────────────────────
|
||||
|
||||
def build_portal_embed(
|
||||
network_name: str, data: dict, channel_count: int, total_messages: int,
|
||||
) -> discord.Embed:
|
||||
colour = discord.Colour(data["colour"]) if data.get("colour") else COLOUR_INFO
|
||||
desc = data.get("description") or "*A wormhole network.*"
|
||||
status = "❄️ Frozen" if data.get("frozen") else "🟢 Active"
|
||||
|
||||
em = discord.Embed(title=f"🌀 Wormhole — {network_name}", description=desc, colour=colour)
|
||||
em.add_field(name="Status", value=status, inline=True)
|
||||
em.add_field(name="Channels", value=str(channel_count), inline=True)
|
||||
em.add_field(name="Messages", value=f"{total_messages:,}", inline=True)
|
||||
|
||||
dm_count = len(data.get("dm_subscribers", []))
|
||||
if dm_count:
|
||||
em.add_field(name="DM Subscribers", value=str(dm_count), inline=True)
|
||||
|
||||
features = []
|
||||
for key, label in [
|
||||
("use_webhooks", "Webhooks"), ("sync_edits", "Edit sync"),
|
||||
("sync_deletes", "Delete sync"), ("sync_reactions", "Reaction sync"),
|
||||
("sync_replies", "Reply sync"), ("dm_enabled", "DM relay"),
|
||||
]:
|
||||
if data.get(key):
|
||||
features.append(f"✅ {label}")
|
||||
if features:
|
||||
em.add_field(name="Features", value=" • ".join(features), inline=False)
|
||||
|
||||
if data.get("motd"):
|
||||
em.add_field(name="📋 MOTD", value=data["motd"][:256], inline=False)
|
||||
if data.get("custom_icon"):
|
||||
em.set_thumbnail(url=data["custom_icon"])
|
||||
em.set_footer(text="Portal auto-updates • Wormhole v3.2.0")
|
||||
em.timestamp = datetime.now(timezone.utc)
|
||||
return em
|
||||
|
||||
|
||||
def info_embed(description: str, title: str = "ℹ️ Info") -> discord.Embed:
|
||||
return make_embed(title=title, description=description, colour=COLOUR_INFO)
|
||||
# ── Starboard embed ────────────────────────────────────────────────────────
|
||||
|
||||
def build_star_embed(
|
||||
author_name: str,
|
||||
author_avatar: str,
|
||||
content: str,
|
||||
stars: int,
|
||||
server_name: str,
|
||||
channel_name: str,
|
||||
image_url: Optional[str] = None,
|
||||
) -> discord.Embed:
|
||||
em = discord.Embed(description=content, colour=COLOUR_STAR, timestamp=datetime.now(timezone.utc))
|
||||
em.set_author(name=author_name, icon_url=author_avatar)
|
||||
em.add_field(name="⭐ Stars", value=str(stars), inline=True)
|
||||
em.set_footer(text=f"{server_name} • #{channel_name}")
|
||||
if image_url:
|
||||
em.set_image(url=image_url)
|
||||
return em
|
||||
|
||||
|
||||
# ── Formatting helpers ──────────────────────────────────────────────────────
|
||||
|
|
@ -96,18 +376,26 @@ def truncate(text: str, length: int = 2000) -> str:
|
|||
return text
|
||||
return text[: length - 3] + "..."
|
||||
|
||||
|
||||
def human_timedelta(seconds: float) -> str:
|
||||
m, s = divmod(int(seconds), 60)
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
parts = []
|
||||
if d:
|
||||
parts.append(f"{d}d")
|
||||
if h:
|
||||
parts.append(f"{h}h")
|
||||
if m:
|
||||
parts.append(f"{m}m")
|
||||
if s or not parts:
|
||||
parts.append(f"{s}s")
|
||||
if d: parts.append(f"{d}d")
|
||||
if h: parts.append(f"{h}h")
|
||||
if m: parts.append(f"{m}m")
|
||||
if s or not parts: parts.append(f"{s}s")
|
||||
return " ".join(parts)
|
||||
|
||||
def compact_format(server_name: str, user_name: str, content: str) -> str:
|
||||
return truncate(f"**[{server_name}] {user_name}:** {content}", 2000)
|
||||
|
||||
def format_audit_entry(action: str, user: str, target: str = "", details: str = "") -> dict:
|
||||
"""Create an audit log entry."""
|
||||
return {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"action": action,
|
||||
"user": user,
|
||||
"target": target,
|
||||
"details": details,
|
||||
}
|
||||
|
|
|
|||
3073
wormhole/wormhole.py
3073
wormhole/wormhole.py
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue