EveCogs/wormhole/utils.py
everestmcarthur 6ca37b9ba1 Initial commit — Wormhole v3.0.0: advanced cross-server relay cog
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
2026-05-07 21:53:21 +00:00

113 lines
4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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)