feat: Add VoiceMeister v1.0.0 — Ultimate temporary voice channel management
- 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
This commit is contained in:
parent
2f266e1add
commit
499c2bb291
6 changed files with 2217 additions and 1 deletions
|
|
@ -3,6 +3,6 @@
|
|||
"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 NexusCore (the ultimate all-in-one server management cog with tickets, applications, suggestions, reaction roles, giveaways, logging, moderation & economy), Wormhole v3.4.0 (cross-server relay with 85+ hybrid commands, context menus, ToS gate, reports, analytics & more), and NewHelpMenu (Components V2 help overhaul).",
|
||||
"description": "EveCogs is a premium cog repository featuring NexusCore (the ultimate all-in-one server management cog with tickets, applications, suggestions, reaction roles, giveaways, logging, moderation & economy), Wormhole v3.4.0 (cross-server relay with 85+ hybrid commands, context menus, ToS gate, reports, analytics & more), NewHelpMenu (Components V2 help overhaul), VoiceMeister (ultimate temporary voice channel system with Join-to-Create, persistent control panel, name templates, game detection & logging), and SelfbotGuard (advanced selfbot detection).",
|
||||
"type": "REPO"
|
||||
}
|
||||
|
|
|
|||
14
voicemeister/__init__.py
Normal file
14
voicemeister/__init__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""VoiceMeister — Ultimate temporary voice channel management for Red-DiscordBot."""
|
||||
|
||||
from .voicemeister import VoiceMeister
|
||||
|
||||
__red_end_user_data_statement__ = (
|
||||
"This cog stores Discord user IDs, guild IDs, and channel IDs for voice channel "
|
||||
"ownership and settings. Per-user data includes channel ownership, ban/permit lists, "
|
||||
"and usage cooldowns. Data is cleaned up when channels are deleted."
|
||||
)
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
cog = VoiceMeister(bot)
|
||||
await bot.add_cog(cog)
|
||||
32
voicemeister/info.json
Normal file
32
voicemeister/info.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"author": [
|
||||
"everestmcarthur"
|
||||
],
|
||||
"install_msg": "Thanks for installing **VoiceMeister v1.0.0** — the ultimate temporary voice channel system!\n\n**Quick Start:**\n• `[p]voicemeister setup` — Interactive setup wizard\n• `[p]voicemeister creator add #channel` — Add a Join-to-Create channel\n• `[p]voicemeister panel #channel` — Send the control panel\n• `[p]vc` — User voice channel commands\n\n**Panel Controls:** Lock, Unlock, Hide, Unhide, Rename, User Limit, Kick, Ban, Permit, Reject, Claim, Transfer, Bitrate, Info, Ghost (hide + lock), and Delete.\n\n**Name Templates:** `{user}`, `{game}`, `{count}`, `{custom}`\n\nFull help: `[p]help VoiceMeister`",
|
||||
"name": "VoiceMeister",
|
||||
"disabled": false,
|
||||
"short": "Ultimate temporary voice channels: Join-to-Create, full control panel with buttons, name templates, game detection, logging & more.",
|
||||
"description": "VoiceMeister v1.0.0 is a comprehensive temporary voice channel management system. Features: Join-to-Create channels that auto-spawn personal voice channels, a persistent button control panel (Lock/Unlock, Hide/Unhide, Rename, User Limit, Kick, Ban, Permit, Reject, Claim, Transfer, Bitrate, Info, Ghost, Delete), name templates with {user}/{game}/{count}/{custom} variables, automatic game-activity channel renaming, auto-cleanup when channels empty, logging to a dedicated channel, blacklist/whitelist system, per-user channel limits, cooldowns, default settings per creator, category management, and full slash-compatible hybrid commands. All views are persistent and survive bot restarts.",
|
||||
"tags": [
|
||||
"voice",
|
||||
"voicemeister",
|
||||
"temporary",
|
||||
"jtc",
|
||||
"join-to-create",
|
||||
"voice-channels",
|
||||
"panel",
|
||||
"control",
|
||||
"management",
|
||||
"temp-voice",
|
||||
"dynamic-voice"
|
||||
],
|
||||
"type": "COG",
|
||||
"end_user_data_statement": "This cog stores Discord user IDs, guild IDs, and channel IDs for voice channel ownership and settings. Per-user data includes channel ownership, ban/permit lists, and usage cooldowns. Data is cleaned up when channels are deleted.",
|
||||
"min_bot_version": "3.5.0",
|
||||
"min_python_version": [
|
||||
3,
|
||||
9,
|
||||
0
|
||||
],
|
||||
"requirements": []
|
||||
}
|
||||
193
voicemeister/utils.py
Normal file
193
voicemeister/utils.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
"""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
|
||||
699
voicemeister/views.py
Normal file
699
voicemeister/views.py
Normal file
|
|
@ -0,0 +1,699 @@
|
|||
"""VoiceMeister — Persistent views, buttons, modals, and select menus."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
import discord
|
||||
from discord import ui
|
||||
|
||||
from .utils import (
|
||||
Clr, ok_embed, err_embed, info_embed, warn_embed,
|
||||
is_channel_owner, can_manage, ts_relative, ts_now,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .voicemeister import VoiceMeister
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# HELPER — get the cog + validate the interaction
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async def _get_context(interaction: discord.Interaction):
|
||||
"""Return (cog, member, voice_channel, owner_id) or send error and return None tuple."""
|
||||
cog: VoiceMeister | None = interaction.client.get_cog("VoiceMeister")
|
||||
if cog is None:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("VoiceMeister cog is not loaded."), ephemeral=True,
|
||||
)
|
||||
return None, None, None, None
|
||||
|
||||
member = interaction.user
|
||||
if not isinstance(member, discord.Member):
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("Could not resolve you as a guild member."), ephemeral=True,
|
||||
)
|
||||
return None, None, None, None
|
||||
|
||||
vc = member.voice.channel if member.voice else None
|
||||
if vc is None:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("You must be **in a voice channel** to use this."), ephemeral=True,
|
||||
)
|
||||
return None, None, None, None
|
||||
|
||||
owner_id = cog.temp_channels.get(vc.id)
|
||||
if owner_id is None:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("Your current voice channel is **not** a VoiceMeister channel."),
|
||||
ephemeral=True,
|
||||
)
|
||||
return None, None, None, None
|
||||
|
||||
return cog, member, vc, owner_id
|
||||
|
||||
|
||||
async def _require_owner(interaction: discord.Interaction):
|
||||
"""Like _get_context but also requires ownership or Manage Channels."""
|
||||
cog, member, vc, owner_id = await _get_context(interaction)
|
||||
if cog is None:
|
||||
return None, None, None, None
|
||||
|
||||
if not can_manage(member, owner_id):
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("Only the channel **owner** (or a moderator) can do this."),
|
||||
ephemeral=True,
|
||||
)
|
||||
return None, None, None, None
|
||||
|
||||
return cog, member, vc, owner_id
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# MODALS
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class RenameModal(ui.Modal, title="✏️ Rename Channel"):
|
||||
new_name = ui.TextInput(
|
||||
label="New channel name",
|
||||
placeholder="e.g. Gaming Lounge",
|
||||
max_length=100,
|
||||
style=discord.TextStyle.short,
|
||||
)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
try:
|
||||
await vc.edit(name=str(self.new_name), reason=f"VoiceMeister: renamed by {member}")
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Channel renamed to **{self.new_name}**."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "✏️ Channel Renamed", member=member, channel=vc,
|
||||
detail=f"New name: `{self.new_name}`")
|
||||
except discord.HTTPException as e:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed(f"Failed to rename: {e}"), ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
class LimitModal(ui.Modal, title="👥 Set User Limit"):
|
||||
limit = ui.TextInput(
|
||||
label="User limit (0 = unlimited)",
|
||||
placeholder="e.g. 5",
|
||||
max_length=3,
|
||||
style=discord.TextStyle.short,
|
||||
)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
try:
|
||||
val = int(str(self.limit))
|
||||
if val < 0 or val > 99:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("Enter a number between **0** and **99**."), ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await vc.edit(user_limit=val, reason=f"VoiceMeister: limit set by {member}")
|
||||
label = f"**{val}**" if val > 0 else "**unlimited**"
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"User limit set to {label}."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "👥 Limit Changed", member=member, channel=vc,
|
||||
detail=f"New limit: {label}")
|
||||
except discord.HTTPException as e:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed(f"Failed to set limit: {e}"), ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
class BitrateModal(ui.Modal, title="📡 Set Bitrate"):
|
||||
bitrate = ui.TextInput(
|
||||
label="Bitrate in kbps (8–384)",
|
||||
placeholder="e.g. 96",
|
||||
max_length=3,
|
||||
style=discord.TextStyle.short,
|
||||
)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
try:
|
||||
val = int(str(self.bitrate))
|
||||
max_br = interaction.guild.bitrate_limit // 1000
|
||||
if val < 8 or val > max_br:
|
||||
raise ValueError
|
||||
except (ValueError, AttributeError):
|
||||
max_br = interaction.guild.bitrate_limit // 1000 if interaction.guild else 96
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed(f"Enter a number between **8** and **{max_br}** kbps."),
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await vc.edit(bitrate=val * 1000, reason=f"VoiceMeister: bitrate set by {member}")
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Bitrate set to **{val} kbps**."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "📡 Bitrate Changed", member=member, channel=vc,
|
||||
detail=f"New bitrate: {val} kbps")
|
||||
except discord.HTTPException as e:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed(f"Failed to set bitrate: {e}"), ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
class RegionModal(ui.Modal, title="🌍 Set Voice Region"):
|
||||
region = ui.TextInput(
|
||||
label="Region (e.g. us-east, eu-west, auto)",
|
||||
placeholder="auto",
|
||||
max_length=30,
|
||||
style=discord.TextStyle.short,
|
||||
required=False,
|
||||
)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
region_str = str(self.region).strip().lower() if self.region else ""
|
||||
rtc_region = None if region_str in ("auto", "") else region_str
|
||||
|
||||
try:
|
||||
await vc.edit(rtc_region=rtc_region, reason=f"VoiceMeister: region set by {member}")
|
||||
label = rtc_region or "Automatic"
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Voice region set to **{label}**."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "🌍 Region Changed", member=member, channel=vc,
|
||||
detail=f"New region: {label}")
|
||||
except discord.HTTPException as e:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed(f"Failed to set region: {e}"), ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# USER SELECT MENUS (for Kick, Ban, Permit, Reject, Transfer)
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class UserActionView(ui.View):
|
||||
"""Ephemeral view with a user select for a specific action."""
|
||||
|
||||
def __init__(self, action: str):
|
||||
super().__init__(timeout=60)
|
||||
self.action = action
|
||||
select = UserActionSelect(action=action)
|
||||
self.add_item(select)
|
||||
|
||||
|
||||
class UserActionSelect(ui.UserSelect):
|
||||
def __init__(self, action: str):
|
||||
labels = {
|
||||
"kick": "Select user to kick",
|
||||
"ban": "Select user to ban",
|
||||
"permit": "Select user to permit",
|
||||
"reject": "Select user to reject",
|
||||
"transfer": "Select new owner",
|
||||
}
|
||||
super().__init__(
|
||||
placeholder=labels.get(action, "Select a user"),
|
||||
min_values=1,
|
||||
max_values=1,
|
||||
)
|
||||
self.action = action
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
target = self.values[0]
|
||||
if not isinstance(target, discord.Member):
|
||||
target = interaction.guild.get_member(target.id)
|
||||
if target is None:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("Could not find that member."), ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
if target.id == member.id:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("You can't target yourself."), ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
if self.action == "kick":
|
||||
if target.voice and target.voice.channel == vc:
|
||||
await target.move_to(None, reason=f"VoiceMeister: kicked by {member}")
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Kicked {target.mention} from the channel."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "👢 User Kicked", member=member, channel=vc,
|
||||
detail=f"Target: {target} ({target.id})")
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed(f"{target.mention} is not in your channel."), ephemeral=True,
|
||||
)
|
||||
|
||||
elif self.action == "ban":
|
||||
# Set permission override to deny connect
|
||||
await vc.set_permissions(target, connect=False, view_channel=False,
|
||||
reason=f"VoiceMeister: banned by {member}")
|
||||
# If they're in the channel, kick them
|
||||
if target.voice and target.voice.channel == vc:
|
||||
await target.move_to(None, reason=f"VoiceMeister: banned by {member}")
|
||||
# Save to persistent ban list
|
||||
await cog._add_channel_ban(vc.guild.id, vc.id, target.id)
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Banned {target.mention} from the channel."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "🔨 User Banned", member=member, channel=vc,
|
||||
detail=f"Target: {target} ({target.id})")
|
||||
|
||||
elif self.action == "permit":
|
||||
await vc.set_permissions(target, connect=True, view_channel=True,
|
||||
reason=f"VoiceMeister: permitted by {member}")
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Permitted {target.mention} to join the channel."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "➕ User Permitted", member=member, channel=vc,
|
||||
detail=f"Target: {target} ({target.id})")
|
||||
|
||||
elif self.action == "reject":
|
||||
await vc.set_permissions(target, connect=False,
|
||||
reason=f"VoiceMeister: rejected by {member}")
|
||||
if target.voice and target.voice.channel == vc:
|
||||
await target.move_to(None, reason=f"VoiceMeister: rejected by {member}")
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Rejected {target.mention} from the channel."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "➖ User Rejected", member=member, channel=vc,
|
||||
detail=f"Target: {target} ({target.id})")
|
||||
|
||||
elif self.action == "transfer":
|
||||
if target.voice and target.voice.channel == vc:
|
||||
cog.temp_channels[vc.id] = target.id
|
||||
await cog._save_temp_channels()
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Ownership transferred to {target.mention}."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "🔄 Ownership Transferred", member=member,
|
||||
channel=vc, detail=f"New owner: {target} ({target.id})")
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed(f"{target.mention} must be in the channel to receive ownership."),
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# MAIN CONTROL PANEL — PERSISTENT VIEW
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class VoiceMeisterPanel(ui.View):
|
||||
"""The main persistent control panel. All buttons use custom_ids so the
|
||||
view survives restarts. This is registered once in cog_load."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(timeout=None)
|
||||
|
||||
# ── Row 1: Access Controls ─────────────────────────────────────────────
|
||||
|
||||
@ui.button(label="Lock", emoji="🔒", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:lock", row=0)
|
||||
async def lock_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await vc.set_permissions(
|
||||
vc.guild.default_role, connect=False,
|
||||
reason=f"VoiceMeister: locked by {member}",
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed("Channel **locked** 🔒 — no one else can join."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "🔒 Channel Locked", member=member, channel=vc)
|
||||
|
||||
@ui.button(label="Unlock", emoji="🔓", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:unlock", row=0)
|
||||
async def unlock_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await vc.set_permissions(
|
||||
vc.guild.default_role, connect=None,
|
||||
reason=f"VoiceMeister: unlocked by {member}",
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed("Channel **unlocked** 🔓 — anyone can join."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "🔓 Channel Unlocked", member=member, channel=vc)
|
||||
|
||||
@ui.button(label="Ghost", emoji="👻", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:ghost", row=0)
|
||||
async def ghost_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await vc.set_permissions(
|
||||
vc.guild.default_role, connect=False, view_channel=False,
|
||||
reason=f"VoiceMeister: ghosted by {member}",
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed("Channel is now **ghosted** 👻 — hidden and locked."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "👻 Channel Ghosted", member=member, channel=vc)
|
||||
|
||||
@ui.button(label="Reveal", emoji="👁️", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:reveal", row=0)
|
||||
async def reveal_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await vc.set_permissions(
|
||||
vc.guild.default_role, connect=None, view_channel=None,
|
||||
reason=f"VoiceMeister: revealed by {member}",
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed("Channel is now **revealed** 👁️ — visible and unlocked."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "👁️ Channel Revealed", member=member, channel=vc)
|
||||
|
||||
@ui.button(label="Hide", emoji="👤", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:hide", row=0)
|
||||
async def hide_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await vc.set_permissions(
|
||||
vc.guild.default_role, view_channel=False,
|
||||
reason=f"VoiceMeister: hidden by {member}",
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed("Channel is now **hidden** 👤 — invisible to others."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "👤 Channel Hidden", member=member, channel=vc)
|
||||
|
||||
# ── Row 2: User Management ─────────────────────────────────────────────
|
||||
|
||||
@ui.button(label="Permit", emoji="➕", style=discord.ButtonStyle.success,
|
||||
custom_id="vm:permit", row=1)
|
||||
async def permit_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_message(
|
||||
embed=info_embed("Select a user to **permit** into your channel:"),
|
||||
view=UserActionView("permit"), ephemeral=True,
|
||||
)
|
||||
|
||||
@ui.button(label="Reject", emoji="➖", style=discord.ButtonStyle.danger,
|
||||
custom_id="vm:reject", row=1)
|
||||
async def reject_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_message(
|
||||
embed=info_embed("Select a user to **reject** from your channel:"),
|
||||
view=UserActionView("reject"), ephemeral=True,
|
||||
)
|
||||
|
||||
@ui.button(label="Kick", emoji="👢", style=discord.ButtonStyle.danger,
|
||||
custom_id="vm:kick", row=1)
|
||||
async def kick_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_message(
|
||||
embed=info_embed("Select a user to **kick** from your channel:"),
|
||||
view=UserActionView("kick"), ephemeral=True,
|
||||
)
|
||||
|
||||
@ui.button(label="Ban", emoji="🔨", style=discord.ButtonStyle.danger,
|
||||
custom_id="vm:ban", row=1)
|
||||
async def ban_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_message(
|
||||
embed=info_embed("Select a user to **ban** from your channel:"),
|
||||
view=UserActionView("ban"), ephemeral=True,
|
||||
)
|
||||
|
||||
@ui.button(label="Unhide", emoji="👁️🗨️", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:unhide", row=1)
|
||||
async def unhide_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await vc.set_permissions(
|
||||
vc.guild.default_role, view_channel=None,
|
||||
reason=f"VoiceMeister: unhidden by {member}",
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed("Channel is now **visible** 👁️🗨️."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "👁️🗨️ Channel Unhidden", member=member, channel=vc)
|
||||
|
||||
# ── Row 3: Channel Settings ────────────────────────────────────────────
|
||||
|
||||
@ui.button(label="Rename", emoji="✏️", style=discord.ButtonStyle.primary,
|
||||
custom_id="vm:rename", row=2)
|
||||
async def rename_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_modal(RenameModal())
|
||||
|
||||
@ui.button(label="Limit", emoji="👥", style=discord.ButtonStyle.primary,
|
||||
custom_id="vm:limit", row=2)
|
||||
async def limit_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_modal(LimitModal())
|
||||
|
||||
@ui.button(label="Bitrate", emoji="📡", style=discord.ButtonStyle.primary,
|
||||
custom_id="vm:bitrate", row=2)
|
||||
async def bitrate_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_modal(BitrateModal())
|
||||
|
||||
@ui.button(label="Region", emoji="🌍", style=discord.ButtonStyle.primary,
|
||||
custom_id="vm:region", row=2)
|
||||
async def region_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_modal(RegionModal())
|
||||
|
||||
@ui.button(label="Mute All", emoji="🔇", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:muteall", row=2)
|
||||
async def muteall_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
count = 0
|
||||
for m in vc.members:
|
||||
if m.id != member.id and not m.voice.mute:
|
||||
try:
|
||||
await m.edit(mute=True, reason=f"VoiceMeister: muted by {member}")
|
||||
count += 1
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Server-muted **{count}** user(s) 🔇."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "🔇 All Muted", member=member, channel=vc,
|
||||
detail=f"Muted {count} user(s)")
|
||||
|
||||
# ── Row 4: Ownership & Meta ────────────────────────────────────────────
|
||||
|
||||
@ui.button(label="Claim", emoji="👑", style=discord.ButtonStyle.success,
|
||||
custom_id="vm:claim", row=3)
|
||||
async def claim_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _get_context(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
# Check if owner is still in the channel
|
||||
owner_in_channel = any(m.id == owner_id for m in vc.members)
|
||||
if owner_in_channel and member.id != owner_id:
|
||||
await interaction.response.send_message(
|
||||
embed=err_embed("The channel owner is still in the channel. You can't claim it."),
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
if member.id == owner_id:
|
||||
await interaction.response.send_message(
|
||||
embed=info_embed("You already own this channel."), ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
cog.temp_channels[vc.id] = member.id
|
||||
await cog._save_temp_channels()
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"You are now the **owner** of this channel 👑."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "👑 Channel Claimed", member=member, channel=vc)
|
||||
|
||||
@ui.button(label="Transfer", emoji="🔄", style=discord.ButtonStyle.success,
|
||||
custom_id="vm:transfer", row=3)
|
||||
async def transfer_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
await interaction.response.send_message(
|
||||
embed=info_embed("Select the user to **transfer ownership** to:"),
|
||||
view=UserActionView("transfer"), ephemeral=True,
|
||||
)
|
||||
|
||||
@ui.button(label="Info", emoji="ℹ️", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:info", row=3)
|
||||
async def info_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _get_context(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
owner = vc.guild.get_member(owner_id)
|
||||
owner_str = owner.mention if owner else f"Unknown (`{owner_id}`)"
|
||||
|
||||
# Channel state
|
||||
overwrites = vc.overwrites_for(vc.guild.default_role)
|
||||
locked = overwrites.connect is False
|
||||
hidden = overwrites.view_channel is False
|
||||
|
||||
status_parts = []
|
||||
if locked:
|
||||
status_parts.append("🔒 Locked")
|
||||
else:
|
||||
status_parts.append("🔓 Unlocked")
|
||||
if hidden:
|
||||
status_parts.append("👤 Hidden")
|
||||
else:
|
||||
status_parts.append("👁️ Visible")
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"ℹ️ {vc.name}",
|
||||
colour=Clr.VOICE,
|
||||
)
|
||||
embed.add_field(name="👑 Owner", value=owner_str, inline=True)
|
||||
embed.add_field(name="👥 Members", value=f"{len(vc.members)}/{vc.user_limit or '∞'}",
|
||||
inline=True)
|
||||
embed.add_field(name="📡 Bitrate", value=f"{vc.bitrate // 1000} kbps", inline=True)
|
||||
embed.add_field(name="🔐 Status", value=" • ".join(status_parts), inline=False)
|
||||
embed.add_field(name="🌍 Region", value=vc.rtc_region or "Automatic", inline=True)
|
||||
embed.add_field(name="📅 Created", value=f"<t:{int(vc.created_at.timestamp())}:R>",
|
||||
inline=True)
|
||||
|
||||
# List permitted/rejected users
|
||||
permitted = []
|
||||
rejected = []
|
||||
for target, overwrite in vc.overwrites.items():
|
||||
if isinstance(target, (discord.Member, discord.User)):
|
||||
if target.id == owner_id:
|
||||
continue
|
||||
if overwrite.connect is True:
|
||||
permitted.append(target.mention)
|
||||
elif overwrite.connect is False:
|
||||
rejected.append(target.mention)
|
||||
|
||||
if permitted:
|
||||
embed.add_field(name="✅ Permitted", value=", ".join(permitted[:10]), inline=False)
|
||||
if rejected:
|
||||
embed.add_field(name="🚫 Rejected/Banned", value=", ".join(rejected[:10]), inline=False)
|
||||
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
|
||||
@ui.button(label="Unmute All", emoji="🔊", style=discord.ButtonStyle.secondary,
|
||||
custom_id="vm:unmuteall", row=3)
|
||||
async def unmuteall_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
count = 0
|
||||
for m in vc.members:
|
||||
if m.voice.mute:
|
||||
try:
|
||||
await m.edit(mute=False, reason=f"VoiceMeister: unmuted by {member}")
|
||||
count += 1
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Unmuted **{count}** user(s) 🔊."), ephemeral=True,
|
||||
)
|
||||
await cog._log_action(vc.guild, "🔊 All Unmuted", member=member, channel=vc,
|
||||
detail=f"Unmuted {count} user(s)")
|
||||
|
||||
# ── Row 4 continued ───────────────────────────────────────────────────
|
||||
|
||||
@ui.button(label="Delete", emoji="🗑️", style=discord.ButtonStyle.danger,
|
||||
custom_id="vm:delete", row=4)
|
||||
async def delete_btn(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog, member, vc, owner_id = await _require_owner(interaction)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
# Confirmation
|
||||
confirm_view = DeleteConfirmView(member.id, vc.id)
|
||||
await interaction.response.send_message(
|
||||
embed=warn_embed(f"Are you sure you want to **delete** `{vc.name}`? This cannot be undone."),
|
||||
view=confirm_view, ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
class DeleteConfirmView(ui.View):
|
||||
def __init__(self, author_id: int, channel_id: int):
|
||||
super().__init__(timeout=30)
|
||||
self.author_id = author_id
|
||||
self.channel_id = channel_id
|
||||
|
||||
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
|
||||
|
||||
@ui.button(label="Yes, Delete", style=discord.ButtonStyle.danger, emoji="🗑️")
|
||||
async def confirm(self, interaction: discord.Interaction, button: ui.Button):
|
||||
cog: VoiceMeister | None = interaction.client.get_cog("VoiceMeister")
|
||||
channel = interaction.guild.get_channel(self.channel_id)
|
||||
if channel and cog:
|
||||
await cog._log_action(interaction.guild, "🗑️ Channel Deleted",
|
||||
member=interaction.user, channel=channel)
|
||||
cog.temp_channels.pop(channel.id, None)
|
||||
await cog._save_temp_channels()
|
||||
try:
|
||||
await channel.delete(reason=f"VoiceMeister: deleted by {interaction.user}")
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
await interaction.response.edit_message(
|
||||
embed=ok_embed("Channel deleted."), view=None,
|
||||
)
|
||||
self.stop()
|
||||
|
||||
@ui.button(label="Cancel", style=discord.ButtonStyle.secondary, emoji="❌")
|
||||
async def cancel(self, interaction: discord.Interaction, button: ui.Button):
|
||||
await interaction.response.edit_message(
|
||||
embed=info_embed("Deletion cancelled."), view=None,
|
||||
)
|
||||
self.stop()
|
||||
1278
voicemeister/voicemeister.py
Normal file
1278
voicemeister/voicemeister.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue