feat: Add VoiceMeister v1.0.0 — Ultimate temporary voice channel management #1

Merged
everestmcarthur merged 1 commit from feat/voicemeister into main 2026-05-08 09:50:25 -04:00
6 changed files with 2217 additions and 1 deletions

View file

@ -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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff