- Join-to-Create system with auto-spawn temp voice channels - Persistent 20+ button control panel (survives restarts) - Name templates, game detection, auto-cleanup, logging - Blacklist/whitelist, cooldowns, max-channels-per-user - Interactive setup wizard + full [p]vc user commands
193 lines
7.1 KiB
Python
193 lines
7.1 KiB
Python
"""VoiceMeister — shared utilities, embed builders, helpers."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import datetime
|
||
from typing import Optional
|
||
|
||
import discord
|
||
|
||
|
||
# ── Colour palette ─────────────────────────────────────────────────────────
|
||
class Clr:
|
||
PRIMARY = discord.Colour(0x5865F2) # blurple
|
||
SUCCESS = discord.Colour(0x57F287) # green
|
||
WARNING = discord.Colour(0xFEE75C) # yellow
|
||
ERROR = discord.Colour(0xED4245) # red
|
||
INFO = discord.Colour(0x3498DB) # light blue
|
||
VOICE = discord.Colour(0x9B59B6) # purple — main theme
|
||
PANEL = discord.Colour(0x2F3136) # dark — panel background
|
||
|
||
|
||
# ── 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) -> discord.Embed:
|
||
e = discord.Embed(description=desc, colour=Clr.INFO)
|
||
if title:
|
||
e.title = title
|
||
return e
|
||
|
||
|
||
def warn_embed(desc: str, *, title: str | None = None) -> discord.Embed:
|
||
e = discord.Embed(description=f"⚠️ {desc}", colour=Clr.WARNING)
|
||
if title:
|
||
e.title = title
|
||
return e
|
||
|
||
|
||
# ── Timestamp helpers ──────────────────────────────────────────────────────
|
||
def ts_now() -> int:
|
||
return int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||
|
||
|
||
def ts_relative(ts: int) -> str:
|
||
return f"<t:{ts}:R>"
|
||
|
||
|
||
# ── Name template renderer ────────────────────────────────────────────────
|
||
def render_name(
|
||
template: str,
|
||
*,
|
||
member: discord.Member,
|
||
count: int = 1,
|
||
custom: str = "",
|
||
) -> str:
|
||
"""Render a channel name template with variable substitution."""
|
||
game = None
|
||
for activity in member.activities:
|
||
if activity.type == discord.ActivityType.playing:
|
||
game = activity.name
|
||
break
|
||
|
||
name = template.replace("{user}", member.display_name)
|
||
name = name.replace("{game}", game or "Chilling")
|
||
name = name.replace("{count}", str(count))
|
||
name = name.replace("{custom}", custom or member.display_name)
|
||
|
||
# Discord channel name limit is 100 chars
|
||
return name[:100]
|
||
|
||
|
||
# ── Permission helpers ─────────────────────────────────────────────────────
|
||
def is_channel_owner(member: discord.Member, owner_id: int) -> bool:
|
||
return member.id == owner_id
|
||
|
||
|
||
def can_manage(member: discord.Member, owner_id: int) -> bool:
|
||
"""Check if member is channel owner or has Manage Channels."""
|
||
return member.id == owner_id or member.guild_permissions.manage_channels
|
||
|
||
|
||
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
|
||
|
||
|
||
# ── Panel embed builder ───────────────────────────────────────────────────
|
||
def build_panel_embed(guild: discord.Guild) -> discord.Embed:
|
||
"""Build the main control panel embed."""
|
||
embed = discord.Embed(
|
||
title="🎙️ VoiceMeister — Channel Control Panel",
|
||
description=(
|
||
"Click any button below to manage your temporary voice channel.\n"
|
||
"You must be *in* a VoiceMeister channel and be its *owner* (or have Manage Channels).\n\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
),
|
||
colour=Clr.VOICE,
|
||
)
|
||
|
||
embed.add_field(
|
||
name="🔐 Access Controls",
|
||
value=(
|
||
"🔒 **Lock** — Prevent others from joining\n"
|
||
"🔓 **Unlock** — Allow anyone to join\n"
|
||
"👻 **Ghost** — Hide + Lock combined\n"
|
||
"👁️ **Reveal** — Unhide + Unlock combined\n"
|
||
"👤 **Hide** — Make invisible in channel list\n"
|
||
"👁️🗨️ **Unhide** — Make visible again"
|
||
),
|
||
inline=True,
|
||
)
|
||
|
||
embed.add_field(
|
||
name="👥 User Management",
|
||
value=(
|
||
"➕ **Permit** — Allow a specific user\n"
|
||
"➖ **Reject** — Block a specific user\n"
|
||
"👢 **Kick** — Kick a user from channel\n"
|
||
"🔨 **Ban** — Permanently block a user\n"
|
||
"🔇 **Mute All** — Server-mute everyone\n"
|
||
"🔊 **Unmute All** — Remove server-mute"
|
||
),
|
||
inline=True,
|
||
)
|
||
|
||
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||
|
||
embed.add_field(
|
||
name="⚙️ Channel Settings",
|
||
value=(
|
||
"✏️ **Rename** — Change channel name\n"
|
||
"👥 **Limit** — Set user limit (0 = ∞)\n"
|
||
"📡 **Bitrate** — Change audio quality\n"
|
||
"🌍 **Region** — Set voice region"
|
||
),
|
||
inline=True,
|
||
)
|
||
|
||
embed.add_field(
|
||
name="👑 Ownership",
|
||
value=(
|
||
"👑 **Claim** — Claim if owner left\n"
|
||
"🔄 **Transfer** — Give ownership to another\n"
|
||
"ℹ️ **Info** — View channel details\n"
|
||
"🗑️ **Delete** — Destroy the channel"
|
||
),
|
||
inline=True,
|
||
)
|
||
|
||
embed.set_footer(text="VoiceMeister v1.0.0 • Buttons persist across restarts")
|
||
return embed
|
||
|
||
|
||
# ── Log embed builder ─────────────────────────────────────────────────────
|
||
def log_embed(
|
||
action: str,
|
||
*,
|
||
member: discord.Member,
|
||
channel: discord.VoiceChannel | None = None,
|
||
detail: str = "",
|
||
) -> discord.Embed:
|
||
"""Build a log embed for voice actions."""
|
||
embed = discord.Embed(
|
||
description=f"**{action}**\n{detail}" if detail else f"**{action}**",
|
||
colour=Clr.VOICE,
|
||
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
||
)
|
||
embed.set_author(name=str(member), icon_url=member.display_avatar.url)
|
||
if channel:
|
||
embed.add_field(name="Channel", value=channel.mention, inline=True)
|
||
embed.set_footer(text=f"User ID: {member.id}")
|
||
return embed
|