2026-05-08 13:23:03 +00:00
|
|
|
|
"""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
|
2026-05-08 14:48:42 +00:00
|
|
|
|
overwrite = vc.overwrites_for(vc.guild.default_role)
|
|
|
|
|
|
overwrite.connect = False
|
2026-05-08 13:23:03 +00:00
|
|
|
|
await vc.set_permissions(
|
2026-05-08 14:48:42 +00:00
|
|
|
|
vc.guild.default_role, overwrite=overwrite,
|
2026-05-08 13:23:03 +00:00
|
|
|
|
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
|
2026-05-08 14:48:42 +00:00
|
|
|
|
overwrite = vc.overwrites_for(vc.guild.default_role)
|
|
|
|
|
|
overwrite.connect = None
|
2026-05-08 13:23:03 +00:00
|
|
|
|
await vc.set_permissions(
|
2026-05-08 14:48:42 +00:00
|
|
|
|
vc.guild.default_role, overwrite=overwrite,
|
2026-05-08 13:23:03 +00:00
|
|
|
|
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
|
2026-05-08 14:48:42 +00:00
|
|
|
|
overwrite = vc.overwrites_for(vc.guild.default_role)
|
|
|
|
|
|
overwrite.connect = False
|
|
|
|
|
|
overwrite.view_channel = False
|
2026-05-08 13:23:03 +00:00
|
|
|
|
await vc.set_permissions(
|
2026-05-08 14:48:42 +00:00
|
|
|
|
vc.guild.default_role, overwrite=overwrite,
|
2026-05-08 13:23:03 +00:00
|
|
|
|
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
|
2026-05-08 14:48:42 +00:00
|
|
|
|
overwrite = vc.overwrites_for(vc.guild.default_role)
|
|
|
|
|
|
overwrite.connect = None
|
|
|
|
|
|
overwrite.view_channel = None
|
2026-05-08 13:23:03 +00:00
|
|
|
|
await vc.set_permissions(
|
2026-05-08 14:48:42 +00:00
|
|
|
|
vc.guild.default_role, overwrite=overwrite,
|
2026-05-08 13:23:03 +00:00
|
|
|
|
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
|
2026-05-08 14:48:42 +00:00
|
|
|
|
overwrite = vc.overwrites_for(vc.guild.default_role)
|
|
|
|
|
|
overwrite.view_channel = False
|
2026-05-08 13:23:03 +00:00
|
|
|
|
await vc.set_permissions(
|
2026-05-08 14:48:42 +00:00
|
|
|
|
vc.guild.default_role, overwrite=overwrite,
|
2026-05-08 13:23:03 +00:00
|
|
|
|
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
|
2026-05-08 14:48:42 +00:00
|
|
|
|
overwrite = vc.overwrites_for(vc.guild.default_role)
|
|
|
|
|
|
overwrite.view_channel = None
|
2026-05-08 13:23:03 +00:00
|
|
|
|
await vc.set_permissions(
|
2026-05-08 14:48:42 +00:00
|
|
|
|
vc.guild.default_role, overwrite=overwrite,
|
2026-05-08 13:23:03 +00:00
|
|
|
|
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()
|