Features: - Named wormhole networks with full per-network config - Webhook relay with custom avatar/name modes (user, server, both, custom) - Message edit, delete, reply, reaction, thread & sticker sync - Staff system (owner + staff) with granular permissions - Moderation: user ban/mute, server ban/mute, allowlist - Word & regex content filters with auto-delete - Token-bucket rate limiting per user per network - Per-network logging channel - Statistics dashboard - NSFW gating, freeze/pause, silent mode - Custom network icon, description, accent colour - Server nickname overrides - Ownership transfer - Thread support - Embed forwarding - Professional README, info.json, LICENSE
113 lines
4 KiB
Python
113 lines
4 KiB
Python
"""Utility helpers for Wormhole cog."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import time
|
||
from collections import defaultdict
|
||
from typing import Optional
|
||
|
||
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
|
||
|
||
|
||
# ── Cooldown bucket ────────────────────────────────────────────────────────
|
||
|
||
class CooldownBucket:
|
||
"""Per-user per-network token-bucket rate limiter."""
|
||
|
||
def __init__(self, rate: int = 5, per: float = 10.0):
|
||
self.rate = rate
|
||
self.per = per
|
||
self._buckets: dict[str, list[float]] = defaultdict(list)
|
||
|
||
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]
|
||
if len(self._buckets[key]) >= self.rate:
|
||
return True
|
||
self._buckets[key].append(now)
|
||
return False
|
||
|
||
def update(self, rate: int, per: float):
|
||
self.rate = rate
|
||
self.per = per
|
||
|
||
|
||
# ── 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:
|
||
return word
|
||
for pattern in regex_filters:
|
||
try:
|
||
if re.search(pattern, content, re.IGNORECASE):
|
||
return pattern
|
||
except re.error:
|
||
continue
|
||
return None
|
||
|
||
|
||
# ── Embed builders ──────────────────────────────────────────────────────────
|
||
|
||
def make_embed(
|
||
title: str = "",
|
||
description: str = "",
|
||
colour: discord.Colour = COLOUR_INFO,
|
||
**kwargs,
|
||
) -> discord.Embed:
|
||
em = discord.Embed(title=title, description=description, colour=colour, **kwargs)
|
||
return em
|
||
|
||
|
||
def ok_embed(description: str, title: str = "✅ Success") -> discord.Embed:
|
||
return make_embed(title=title, description=description, colour=COLOUR_OK)
|
||
|
||
|
||
def err_embed(description: str, title: str = "❌ Error") -> discord.Embed:
|
||
return make_embed(title=title, description=description, colour=COLOUR_ERR)
|
||
|
||
|
||
def warn_embed(description: str, title: str = "⚠️ Warning") -> discord.Embed:
|
||
return make_embed(title=title, description=description, colour=COLOUR_WARN)
|
||
|
||
|
||
def info_embed(description: str, title: str = "ℹ️ Info") -> discord.Embed:
|
||
return make_embed(title=title, description=description, colour=COLOUR_INFO)
|
||
|
||
|
||
# ── Formatting helpers ──────────────────────────────────────────────────────
|
||
|
||
def truncate(text: str, length: int = 2000) -> str:
|
||
if len(text) <= length:
|
||
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")
|
||
return " ".join(parts)
|