8 fully-featured subsystems, 5,673 lines of code, 100+ commands: 🎫 Tickets — Panel-based creation, categories with modal questions, staff claim, priority system (low/med/high/urgent), HTML transcripts, feedback ratings, DM notifs 📋 Applications — Multi-type forms with paginated modals, accept/deny/interview review, auto-role, staff notes, auto-thread, cooldowns, age requirements 💡 Suggestions — Button voting, 8 status types, anonymous mode, auto-thread, staff responses, categories, top leaderboard, status-specific channels 🎭 Reaction Roles — Button/select/reaction modes, exclusive groups, role requirements, sticky roles, temporary roles, max picks, DM confirm 🎉 Giveaways — Button entry, role/age requirements, bonus entries, drop mode, multiple winners, reroll, scheduled end, DM notifications 📋 Logging — 20+ event types, per-event channels, message cache, invite tracking, ignore filters (channel/role/user/bot), bulk delete file export 🛡️ Moderation — Case system, warn auto-escalation, mute/kick/ban/softban/tempban, notes, channel/server lockdown, anti-raid, anti-nuke, automod (6 modules), appeals 🪙 Economy — Wallet+bank, daily/weekly streaks, work/crime/rob, coinflip/slots/blackjack, shop (items+roles), inventory, heist mini-game, pet system (6 types), leaderboard
222 lines
9 KiB
Python
222 lines
9 KiB
Python
"""NexusCore — shared utilities, embed builders, paginators, config helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import datetime
|
|
import math
|
|
import random
|
|
import string
|
|
from io import BytesIO
|
|
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
|
|
import discord
|
|
from redbot.core import commands
|
|
|
|
|
|
# ── Colour palette ─────────────────────────────────────────────────────────
|
|
class Clr:
|
|
TICKET = discord.Colour(0x5865F2) # blurple
|
|
APP = discord.Colour(0x57F287) # green
|
|
SUGGEST = discord.Colour(0xFEE75C) # yellow
|
|
ROLES = discord.Colour(0xEB459E) # fuchsia
|
|
GIVE = discord.Colour(0xED4245) # red
|
|
LOG = discord.Colour(0x99AAB5) # grey
|
|
MOD = discord.Colour(0xE67E22) # orange
|
|
ECO = discord.Colour(0xF1C40F) # gold
|
|
SUCCESS = discord.Colour(0x2ECC71)
|
|
ERROR = discord.Colour(0xE74C3C)
|
|
INFO = discord.Colour(0x3498DB)
|
|
|
|
|
|
# ── Quick embeds ───────────────────────────────────────────────────────────
|
|
def ok_embed(desc: str, *, title: str | None = None) -> discord.Embed:
|
|
e = discord.Embed(description=f"✅ {desc}", colour=Clr.SUCCESS)
|
|
if title:
|
|
e.title = title
|
|
return e
|
|
|
|
|
|
def err_embed(desc: str, *, title: str | None = None) -> discord.Embed:
|
|
e = discord.Embed(description=f"❌ {desc}", colour=Clr.ERROR)
|
|
if title:
|
|
e.title = title
|
|
return e
|
|
|
|
|
|
def info_embed(desc: str, *, title: str | None = None, colour: discord.Colour | None = None) -> discord.Embed:
|
|
e = discord.Embed(description=desc, colour=colour or Clr.INFO)
|
|
if title:
|
|
e.title = title
|
|
return e
|
|
|
|
|
|
# ── ID generator ───────────────────────────────────────────────────────────
|
|
def short_id(length: int = 8) -> str:
|
|
return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
|
|
|
|
|
# ── Timestamp helpers ──────────────────────────────────────────────────────
|
|
def ts_now() -> int:
|
|
return int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
|
|
|
|
|
def ts_relative(ts: int, style: str = "R") -> str:
|
|
return f"<t:{ts}:{style}>"
|
|
|
|
|
|
def ts_full(ts: int) -> str:
|
|
return f"<t:{ts}:F>"
|
|
|
|
|
|
def duration_str(seconds: int) -> str:
|
|
if seconds < 60:
|
|
return f"{seconds}s"
|
|
if seconds < 3600:
|
|
return f"{seconds // 60}m {seconds % 60}s"
|
|
if seconds < 86400:
|
|
h = seconds // 3600
|
|
m = (seconds % 3600) // 60
|
|
return f"{h}h {m}m"
|
|
d = seconds // 86400
|
|
h = (seconds % 86400) // 3600
|
|
return f"{d}d {h}h"
|
|
|
|
|
|
# ── Time parser — "1d2h30m" → seconds ─────────────────────────────────────
|
|
def parse_duration(text: str) -> int | None:
|
|
import re
|
|
total = 0
|
|
matches = re.findall(r"(\d+)\s*([smhdw])", text.lower())
|
|
if not matches:
|
|
return None
|
|
multipliers = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}
|
|
for val, unit in matches:
|
|
total += int(val) * multipliers[unit]
|
|
return total if total > 0 else None
|
|
|
|
|
|
# ── Paginator view ─────────────────────────────────────────────────────────
|
|
class Paginator(discord.ui.View):
|
|
"""A generic embed paginator with first/prev/next/last + page counter."""
|
|
|
|
def __init__(
|
|
self,
|
|
pages: list[discord.Embed],
|
|
*,
|
|
author_id: int | None = None,
|
|
timeout: float = 180.0,
|
|
):
|
|
super().__init__(timeout=timeout)
|
|
self.pages = pages
|
|
self.current = 0
|
|
self.author_id = author_id
|
|
self.message: discord.Message | None = None
|
|
self._update_buttons()
|
|
|
|
def _update_buttons(self):
|
|
self.first_btn.disabled = self.current == 0
|
|
self.prev_btn.disabled = self.current == 0
|
|
self.next_btn.disabled = self.current >= len(self.pages) - 1
|
|
self.last_btn.disabled = self.current >= len(self.pages) - 1
|
|
self.page_btn.label = f"{self.current + 1}/{len(self.pages)}"
|
|
|
|
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
|
if self.author_id and interaction.user.id != self.author_id:
|
|
await interaction.response.send_message("Not your menu.", ephemeral=True)
|
|
return False
|
|
return True
|
|
|
|
async def _update(self, interaction: discord.Interaction):
|
|
self._update_buttons()
|
|
await interaction.response.edit_message(embed=self.pages[self.current], view=self)
|
|
|
|
@discord.ui.button(emoji="⏮", style=discord.ButtonStyle.secondary)
|
|
async def first_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
self.current = 0
|
|
await self._update(interaction)
|
|
|
|
@discord.ui.button(emoji="◀", style=discord.ButtonStyle.primary)
|
|
async def prev_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
self.current = max(0, self.current - 1)
|
|
await self._update(interaction)
|
|
|
|
@discord.ui.button(label="1/1", style=discord.ButtonStyle.secondary, disabled=True)
|
|
async def page_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await interaction.response.defer()
|
|
|
|
@discord.ui.button(emoji="▶", style=discord.ButtonStyle.primary)
|
|
async def next_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
self.current = min(len(self.pages) - 1, self.current + 1)
|
|
await self._update(interaction)
|
|
|
|
@discord.ui.button(emoji="⏭", style=discord.ButtonStyle.secondary)
|
|
async def last_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
self.current = len(self.pages) - 1
|
|
await self._update(interaction)
|
|
|
|
async def on_timeout(self):
|
|
for item in self.children:
|
|
if isinstance(item, discord.ui.Button):
|
|
item.disabled = True
|
|
if self.message:
|
|
try:
|
|
await self.message.edit(view=self)
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
async def send(self, dest, **kwargs):
|
|
self.message = await dest.send(embed=self.pages[0], view=self, **kwargs)
|
|
return self.message
|
|
|
|
|
|
# ── Confirmation view ──────────────────────────────────────────────────────
|
|
class ConfirmView(discord.ui.View):
|
|
def __init__(self, author_id: int, *, timeout: float = 60.0):
|
|
super().__init__(timeout=timeout)
|
|
self.author_id = author_id
|
|
self.value: bool | None = None
|
|
|
|
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
|
if interaction.user.id != self.author_id:
|
|
await interaction.response.send_message("Not your confirmation.", ephemeral=True)
|
|
return False
|
|
return True
|
|
|
|
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.success, emoji="✅")
|
|
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
self.value = True
|
|
self.stop()
|
|
await interaction.response.edit_message(view=None)
|
|
|
|
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.danger, emoji="❌")
|
|
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
self.value = False
|
|
self.stop()
|
|
await interaction.response.edit_message(view=None)
|
|
|
|
|
|
# ── Chunk helper ───────────────────────────────────────────────────────────
|
|
def chunk_list(lst: list, size: int) -> list[list]:
|
|
return [lst[i : i + size] for i in range(0, len(lst), size)]
|
|
|
|
|
|
# ── Permission helper ─────────────────────────────────────────────────────
|
|
def has_perms(member: discord.Member, channel: discord.abc.GuildChannel, *perms: str) -> bool:
|
|
p = channel.permissions_for(member)
|
|
return all(getattr(p, perm, False) for perm in perms)
|
|
|
|
|
|
# ── Safe send ──────────────────────────────────────────────────────────────
|
|
async def safe_send(dest, content=None, **kwargs) -> discord.Message | None:
|
|
try:
|
|
return await dest.send(content, **kwargs)
|
|
except (discord.HTTPException, discord.Forbidden):
|
|
return None
|
|
|
|
|
|
async def safe_dm(user: discord.User, content=None, **kwargs) -> discord.Message | None:
|
|
try:
|
|
return await user.send(content, **kwargs)
|
|
except (discord.HTTPException, discord.Forbidden):
|
|
return None
|