feat: Add VoiceMeister v1.0.0
feat: Add VoiceMeister v1.0.0 — Ultimate temporary voice channel management
This commit is contained in:
commit
ec7767079e
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.",
|
"install_msg": "Welcome to **EveCogs**! Use `[p]cog list` to see available cogs.",
|
||||||
"name": "EveCogs",
|
"name": "EveCogs",
|
||||||
"short": "A collection of advanced cogs for Red-DiscordBot.",
|
"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"
|
"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