CustomVC Rework
- autoname implementation - Automatic channel naming can be toggled with `!customvc autoname` - Autoname status can be verified with `!customvc settings` - Autonaming will automatically apply game name as channel name if owner plays a game - If owner is not playing a game, channel name will default to `{userdisplayname}'s Channel´ - Currently, checks are performed every 30s, and changes applied after a 5m cooldown - Minor fix for the latest logging changes - Caller function was not correctly listed in log messages. This has been corrected.experimental
parent
5023ea9919
commit
d5581710a7
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands, tasks
|
||||||
import datetime
|
import datetime
|
||||||
import globals
|
import globals
|
||||||
|
|
||||||
|
@ -35,11 +35,112 @@ class CustomVCCog(commands.Cog):
|
||||||
self.MAX_CUSTOM_CHANNELS = self.settings_data[guild_id]["customvc_settings"]["customvc_max_limit"]
|
self.MAX_CUSTOM_CHANNELS = self.settings_data[guild_id]["customvc_settings"]["customvc_max_limit"]
|
||||||
|
|
||||||
self.CUSTOM_VC_INFO = {}
|
self.CUSTOM_VC_INFO = {}
|
||||||
|
"""
|
||||||
|
Each entry in CUSTOM_VC_INFO will look like:
|
||||||
|
{
|
||||||
|
"owner_id": <int>,
|
||||||
|
"autoname": <bool>,
|
||||||
|
"locked": <bool>,
|
||||||
|
"allowed_ids": set(),
|
||||||
|
"denied_ids": set(),
|
||||||
|
"user_limit": <int or None>,
|
||||||
|
"bitrate": <int or None>,
|
||||||
|
"last_rename_time": datetime.datetime or None # We'll add this for the rename cooldown
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
self.USER_LAST_CREATED = {}
|
self.USER_LAST_CREATED = {}
|
||||||
self.GLOBAL_CREATIONS = []
|
self.GLOBAL_CREATIONS = []
|
||||||
self.CHANNEL_COUNTER = 0
|
self.CHANNEL_COUNTER = 0
|
||||||
self.PENDING_DELETIONS = {}
|
self.PENDING_DELETIONS = {}
|
||||||
|
|
||||||
|
# Start the auto-rename loop
|
||||||
|
self.rename_cooldown_seconds = 300 # For example, 5 minutes
|
||||||
|
self.auto_rename_loop.start()
|
||||||
|
|
||||||
|
def cog_unload(self):
|
||||||
|
"""Stop the loop when the cog is unloaded."""
|
||||||
|
self.auto_rename_loop.cancel()
|
||||||
|
|
||||||
|
@tasks.loop(seconds=30.0) # Desired interval
|
||||||
|
async def auto_rename_loop(self):
|
||||||
|
"""
|
||||||
|
Periodically checks for auto-named channels and updates their name
|
||||||
|
based on the owner’s current game, respecting a cooldown.
|
||||||
|
"""
|
||||||
|
# For debug, let's log that the loop is running:
|
||||||
|
logger.debug("[AutoRenameLoop] Starting iteration over all custom VCs...")
|
||||||
|
|
||||||
|
# We can fetch the one guild if you only have one, or do this for multiple
|
||||||
|
# For simplicity, we assume the single guild from your JSON:
|
||||||
|
guild_id = 896713616089309184
|
||||||
|
guild = self.bot.get_guild(guild_id)
|
||||||
|
if not guild:
|
||||||
|
logger.warning("[AutoRenameLoop] Guild not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
for vc_id, info in list(self.CUSTOM_VC_INFO.items()):
|
||||||
|
if not info.get("autoname"):
|
||||||
|
continue # skip if auto-naming disabled
|
||||||
|
|
||||||
|
vc = guild.get_channel(vc_id)
|
||||||
|
if not vc or not isinstance(vc, discord.VoiceChannel):
|
||||||
|
logger.debug(f"[AutoRenameLoop] VC {vc_id} not found or not a VoiceChannel.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
owner_id = info["owner_id"]
|
||||||
|
owner = guild.get_member(owner_id)
|
||||||
|
if not owner:
|
||||||
|
logger.debug(f"[AutoRenameLoop] Owner {owner_id} not found in guild.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Determine what name we *want* the channel to have
|
||||||
|
desired_name = self.get_desired_name(owner)
|
||||||
|
current_name = vc.name
|
||||||
|
|
||||||
|
# Compare to see if there's a difference
|
||||||
|
if desired_name != current_name:
|
||||||
|
# Check when we last renamed
|
||||||
|
last_rename = info.get("last_rename_time")
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
if last_rename is None:
|
||||||
|
# Means we've never renamed, so let's rename immediately
|
||||||
|
logger.debug(
|
||||||
|
f"[AutoRenameLoop] VC {vc.id}: No last rename; renaming from '{current_name}' to '{desired_name}'."
|
||||||
|
)
|
||||||
|
await vc.edit(name=desired_name, reason="Auto-naming initial rename.")
|
||||||
|
info["last_rename_time"] = now
|
||||||
|
else:
|
||||||
|
# We have a last rename time, see how many seconds
|
||||||
|
diff = (now - last_rename).total_seconds()
|
||||||
|
logger.debug(
|
||||||
|
f"[AutoRenameLoop] VC {vc.id} name differ. Last rename was {int(diff)}s ago. "
|
||||||
|
f"Cooldown = {self.rename_cooldown_seconds}."
|
||||||
|
)
|
||||||
|
if diff >= self.rename_cooldown_seconds:
|
||||||
|
# We rename now
|
||||||
|
logger.debug(
|
||||||
|
f"[AutoRenameLoop] Renaming VC {vc.id} from '{current_name}' to '{desired_name}'"
|
||||||
|
)
|
||||||
|
await vc.edit(name=desired_name, reason="Auto-naming update.")
|
||||||
|
info["last_rename_time"] = now
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"[AutoRenameLoop] Skipping rename of VC {vc.id} because cooldown not reached ({int(diff)} < {self.rename_cooldown_seconds})."
|
||||||
|
)
|
||||||
|
# end if last_rename is None
|
||||||
|
else:
|
||||||
|
logger.debug(f"[AutoRenameLoop] VC {vc.id} already at desired name '{current_name}'. No rename needed.")
|
||||||
|
|
||||||
|
logger.debug("[AutoRenameLoop] Finished iteration.")
|
||||||
|
|
||||||
|
@auto_rename_loop.before_loop
|
||||||
|
async def before_auto_rename_loop(self):
|
||||||
|
"""
|
||||||
|
If we need the bot to be ready, wait until on_ready is done.
|
||||||
|
"""
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
"""Handles checking existing voice channels on bot startup."""
|
"""Handles checking existing voice channels on bot startup."""
|
||||||
|
@ -145,67 +246,157 @@ class CustomVCCog(commands.Cog):
|
||||||
# Main voice update logic
|
# Main voice update logic
|
||||||
#
|
#
|
||||||
|
|
||||||
|
def get_desired_name(self, member: discord.Member) -> str:
|
||||||
|
game_name = self.get_current_game(member)
|
||||||
|
if game_name:
|
||||||
|
return game_name
|
||||||
|
else:
|
||||||
|
return f"{member.display_name}'s Channel"
|
||||||
|
|
||||||
|
@auto_rename_loop.before_loop
|
||||||
|
async def before_auto_rename_loop(self):
|
||||||
|
"""
|
||||||
|
If we need the bot to be ready, wait until on_ready is done.
|
||||||
|
"""
|
||||||
|
logger.debug("[AutoRenameLoop] Waiting for bot to be ready...")
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
logger.debug("[AutoRenameLoop] Bot is ready. Starting loop...")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self):
|
||||||
|
"""Handles checking existing voice channels on bot startup."""
|
||||||
|
logger.info("[CustomVCCog] on_ready triggered, scanning for existing custom VCs.")
|
||||||
|
await self.scan_existing_custom_vcs()
|
||||||
|
|
||||||
|
def get_current_game(self, member: discord.Member) -> str | None:
|
||||||
|
"""
|
||||||
|
Return the name of the game if the member is playing one, else None.
|
||||||
|
"""
|
||||||
|
for activity in member.activities or []:
|
||||||
|
# In modern discord.py, a "game" is typically an activity with type=ActivityType.playing
|
||||||
|
if activity.type == discord.ActivityType.playing and activity.name:
|
||||||
|
return activity.name
|
||||||
|
return None
|
||||||
|
|
||||||
|
# --------------
|
||||||
|
# Helper: identify if user is playing a game
|
||||||
|
# --------------
|
||||||
|
def get_current_game(self, member: discord.Member) -> str | None:
|
||||||
|
"""
|
||||||
|
Return the name of the game if the member is playing one, else None.
|
||||||
|
"""
|
||||||
|
for activity in member.activities or []:
|
||||||
|
# In modern discord.py, a "game" is typically an activity with type=ActivityType.playing
|
||||||
|
if activity.type == discord.ActivityType.playing and activity.name:
|
||||||
|
return activity.name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# --------------
|
||||||
|
# EVENT: on_voice_state_update
|
||||||
|
# --------------
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_voice_state_update(
|
||||||
|
self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState
|
||||||
|
):
|
||||||
|
"""Handles user movement between voice channels."""
|
||||||
|
await self.on_voice_update(member, before, after)
|
||||||
|
|
||||||
async def on_voice_update(self, member, before, after):
|
async def on_voice_update(self, member, before, after):
|
||||||
|
# If user changed channels
|
||||||
if before.channel != after.channel:
|
if before.channel != after.channel:
|
||||||
# 1) If user just joined the lobby
|
logger.debug(
|
||||||
|
f"[on_voice_update] {member.display_name} changed channels "
|
||||||
|
f"from '{getattr(before.channel, 'name', None)}' to '{getattr(after.channel, 'name', None)}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1) If user just joined the "lobby"
|
||||||
if after.channel and after.channel.id == self.LOBBY_VC_ID:
|
if after.channel and after.channel.id == self.LOBBY_VC_ID:
|
||||||
|
logger.debug(f"[on_voice_update] {member.display_name} joined the lobby.")
|
||||||
|
# Respect rate-limits
|
||||||
if not await self.can_create_vc(member):
|
if not await self.can_create_vc(member):
|
||||||
# Rate-limit => forcibly disconnect
|
# forcibly disconnect
|
||||||
await member.move_to(None)
|
await member.move_to(None)
|
||||||
# Also mention them in the ephemeral chat of LOBBY_VC_ID if we want (if possible)
|
logger.debug(
|
||||||
|
f"[on_voice_update] {member.display_name} exceeded creation limits. Disconnected."
|
||||||
|
)
|
||||||
lobby_chan = after.channel
|
lobby_chan = after.channel
|
||||||
if lobby_chan and hasattr(lobby_chan, "send"):
|
if lobby_chan and hasattr(lobby_chan, "send"):
|
||||||
# We'll try to mention them
|
await lobby_chan.send(
|
||||||
await lobby_chan.send(f"{member.mention}, you’ve exceeded custom VC creation limits. Try again later!")
|
f"{member.mention}, you’ve exceeded custom VC creation limits. Try again later!"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2) Create a new custom VC
|
# Identify category
|
||||||
self.CHANNEL_COUNTER += 1
|
|
||||||
vc_name = f"VC {self.CHANNEL_COUNTER}"
|
|
||||||
if self.CHANNEL_COUNTER > self.MAX_CUSTOM_CHANNELS:
|
|
||||||
vc_name = f"Overflow {self.CHANNEL_COUNTER}"
|
|
||||||
|
|
||||||
category = member.guild.get_channel(self.CUSTOM_VC_CATEGORY_ID)
|
category = member.guild.get_channel(self.CUSTOM_VC_CATEGORY_ID)
|
||||||
if not category or not isinstance(category, discord.CategoryChannel):
|
if not category or not isinstance(category, discord.CategoryChannel):
|
||||||
logger.info("Could not find a valid custom VC category.")
|
logger.error(
|
||||||
|
f"[CustomVC] Could not find valid category for guild {member.guild.id}. Aborting creation."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
new_vc = await category.create_voice_channel(name=vc_name)
|
# Attempt to figure out the game or fallback
|
||||||
|
game_name = self.get_current_game(member)
|
||||||
|
if game_name:
|
||||||
|
vc_name = game_name
|
||||||
|
else:
|
||||||
|
vc_name = f"{member.display_name}'s Channel"
|
||||||
|
|
||||||
|
if not vc_name:
|
||||||
|
logger.error(
|
||||||
|
f"[CustomVC] Could not derive channel name from user {member.display_name}"
|
||||||
|
)
|
||||||
|
lobby_chan = after.channel
|
||||||
|
if lobby_chan and hasattr(lobby_chan, "send"):
|
||||||
|
await lobby_chan.send(
|
||||||
|
f"Could not create your channel, {member.mention}. Please try again."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.CHANNEL_COUNTER += 1
|
||||||
|
new_vc = await category.create_voice_channel(name=vc_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[CustomVC] Failed to create voice channel: {e}", exc_info=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[CustomVC] Created new channel '{vc_name}' (ID {new_vc.id}) for {member.display_name}."
|
||||||
|
)
|
||||||
await member.move_to(new_vc)
|
await member.move_to(new_vc)
|
||||||
|
|
||||||
# Record memory
|
now = datetime.datetime.utcnow()
|
||||||
|
self.USER_LAST_CREATED[member.id] = now
|
||||||
|
self.GLOBAL_CREATIONS.append(now)
|
||||||
|
# prune old from GLOBAL_CREATIONS
|
||||||
|
cutoff = now - datetime.timedelta(seconds=60)
|
||||||
|
self.GLOBAL_CREATIONS[:] = [t for t in self.GLOBAL_CREATIONS if t > cutoff]
|
||||||
|
|
||||||
|
# Store custom VC info
|
||||||
self.CUSTOM_VC_INFO[new_vc.id] = {
|
self.CUSTOM_VC_INFO[new_vc.id] = {
|
||||||
"owner_id": member.id,
|
"owner_id": member.id,
|
||||||
|
"autoname": False,
|
||||||
"locked": False,
|
"locked": False,
|
||||||
"allowed_ids": set(),
|
"allowed_ids": set(),
|
||||||
"denied_ids": set(),
|
"denied_ids": set(),
|
||||||
"user_limit": None,
|
"user_limit": None,
|
||||||
"bitrate": None,
|
"bitrate": None,
|
||||||
|
"last_rename_time": None, # We haven't renamed it yet
|
||||||
}
|
}
|
||||||
|
|
||||||
now = datetime.datetime.utcnow()
|
|
||||||
self.USER_LAST_CREATED[member.id] = now
|
|
||||||
self.GLOBAL_CREATIONS.append(now)
|
|
||||||
# prune old
|
|
||||||
cutoff = now - datetime.timedelta(seconds=60)
|
|
||||||
self.GLOBAL_CREATIONS[:] = [t for t in self.GLOBAL_CREATIONS if t > cutoff]
|
|
||||||
|
|
||||||
# Announce in the new VC's ephemeral chat
|
|
||||||
if hasattr(new_vc, "send"):
|
if hasattr(new_vc, "send"):
|
||||||
await new_vc.send(
|
await new_vc.send(
|
||||||
f"{member.name}, your custom voice channel is ready! "
|
f"{member.display_name}, your custom voice channel is ready! "
|
||||||
"Type `!customvc` here for help with subcommands."
|
"Type `!customvc` here for help with subcommands."
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.update_channel_name(member, after.channel)
|
# If user left a channel that is tracked
|
||||||
|
|
||||||
# 3) If user left a custom VC -> maybe it’s now empty
|
|
||||||
if before.channel and before.channel.id in self.CUSTOM_VC_INFO:
|
if before.channel and before.channel.id in self.CUSTOM_VC_INFO:
|
||||||
old_vc = before.channel
|
old_vc = before.channel
|
||||||
if len(old_vc.members) == 0:
|
if len(old_vc.members) == 0:
|
||||||
self.schedule_deletion(old_vc)
|
self.schedule_deletion(old_vc)
|
||||||
|
|
||||||
# If user joined a custom VC that was pending deletion, cancel it
|
# Also check if they joined a channel pending deletion
|
||||||
if after.channel and after.channel.id in self.CUSTOM_VC_INFO:
|
if after.channel and after.channel.id in self.CUSTOM_VC_INFO:
|
||||||
if after.channel.id in self.PENDING_DELETIONS:
|
if after.channel.id in self.PENDING_DELETIONS:
|
||||||
self.PENDING_DELETIONS[after.channel.id].cancel()
|
self.PENDING_DELETIONS[after.channel.id].cancel()
|
||||||
|
@ -214,29 +405,32 @@ class CustomVCCog(commands.Cog):
|
||||||
|
|
||||||
def schedule_deletion(self, vc: discord.VoiceChannel):
|
def schedule_deletion(self, vc: discord.VoiceChannel):
|
||||||
"""
|
"""
|
||||||
Schedules this custom VC for deletion in 10s if it stays empty.
|
Schedules this custom VC for deletion in self.DELETE_DELAY_SECONDS if it stays empty.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
async def delete_task():
|
async def delete_task():
|
||||||
await asyncio.sleep(self.DELETE_DELAY_SECONDS)
|
await asyncio.sleep(self.DELETE_DELAY_SECONDS)
|
||||||
if len(vc.members) == 0:
|
if len(vc.members) == 0:
|
||||||
|
logger.info(f"[CustomVC] Deleting empty channel '{vc.name}' (ID {vc.id}).")
|
||||||
self.CUSTOM_VC_INFO.pop(vc.id, None)
|
self.CUSTOM_VC_INFO.pop(vc.id, None)
|
||||||
try:
|
try:
|
||||||
await vc.delete()
|
await vc.delete()
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.error(f"[CustomVC] Could not delete VC {vc.id}: {e}", exc_info=True)
|
||||||
self.PENDING_DELETIONS.pop(vc.id, None)
|
self.PENDING_DELETIONS.pop(vc.id, None)
|
||||||
|
|
||||||
loop = vc.guild._state.loop
|
loop = vc.guild._state.loop
|
||||||
task = loop.create_task(delete_task())
|
task = loop.create_task(delete_task())
|
||||||
self.PENDING_DELETIONS[vc.id] = task
|
self.PENDING_DELETIONS[vc.id] = task
|
||||||
|
logger.debug(f"[CustomVC] Scheduled deletion for channel '{vc.name}' in {self.DELETE_DELAY_SECONDS}s.")
|
||||||
|
|
||||||
|
|
||||||
async def can_create_vc(self, member: discord.Member) -> bool:
|
async def can_create_vc(self, member: discord.Member) -> bool:
|
||||||
"""
|
"""
|
||||||
Return False if user or global rate-limits are hit:
|
Return False if user or global rate-limits are hit:
|
||||||
- user can only create 1 channel every 5 minutes
|
- user can only create 1 channel every X minutes
|
||||||
- globally only 2 channels per minute
|
- globally only Y channels per minute
|
||||||
"""
|
"""
|
||||||
now = datetime.datetime.utcnow()
|
now = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
@ -244,12 +438,19 @@ class CustomVCCog(commands.Cog):
|
||||||
if last_time:
|
if last_time:
|
||||||
diff = (now - last_time).total_seconds()
|
diff = (now - last_time).total_seconds()
|
||||||
if diff < (self.USER_COOLDOWN_MINUTES * 60):
|
if diff < (self.USER_COOLDOWN_MINUTES * 60):
|
||||||
|
logger.debug(
|
||||||
|
f"[can_create_vc] {member.display_name} last created a VC {int(diff)}s ago, "
|
||||||
|
f"less than cooldown of {self.USER_COOLDOWN_MINUTES*60}s."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# global limit
|
# global limit
|
||||||
cutoff = now - datetime.timedelta(seconds=60)
|
cutoff = now - datetime.timedelta(seconds=60)
|
||||||
recent = [t for t in self.GLOBAL_CREATIONS if t > cutoff]
|
recent = [t for t in self.GLOBAL_CREATIONS if t > cutoff]
|
||||||
if len(recent) >= self.GLOBAL_CHANNELS_PER_MINUTE:
|
if len(recent) >= self.GLOBAL_CHANNELS_PER_MINUTE:
|
||||||
|
logger.debug(
|
||||||
|
f"[can_create_vc] Global creation limit of {self.GLOBAL_CHANNELS_PER_MINUTE}/min reached."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -258,45 +459,54 @@ class CustomVCCog(commands.Cog):
|
||||||
async def scan_existing_custom_vcs(self):
|
async def scan_existing_custom_vcs(self):
|
||||||
"""
|
"""
|
||||||
On startup: check the custom VC category for leftover channels:
|
On startup: check the custom VC category for leftover channels:
|
||||||
- If channel.id == LOBBY_VC_ID -> skip (don't delete the main lobby).
|
- If channel.id == LOBBY_VC_ID -> skip
|
||||||
- If empty, delete immediately.
|
- If empty, delete immediately
|
||||||
- If non-empty, first occupant is new owner. Mention them in ephemeral chat if possible.
|
- If non-empty, first occupant is new owner
|
||||||
"""
|
"""
|
||||||
|
logger.debug("[scan_existing_custom_vcs] Checking leftover channels at startup...")
|
||||||
for g in self.bot.guilds:
|
for g in self.bot.guilds:
|
||||||
cat = g.get_channel(self.CUSTOM_VC_CATEGORY_ID)
|
cat = g.get_channel(self.CUSTOM_VC_CATEGORY_ID)
|
||||||
if not cat or not isinstance(cat, discord.CategoryChannel):
|
if not cat or not isinstance(cat, discord.CategoryChannel):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for ch in cat.voice_channels:
|
for ch in cat.voice_channels:
|
||||||
# skip the LOBBY
|
|
||||||
if ch.id == self.LOBBY_VC_ID:
|
if ch.id == self.LOBBY_VC_ID:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(ch.members) == 0:
|
if len(ch.members) == 0:
|
||||||
# safe to delete
|
# safe to delete
|
||||||
try:
|
try:
|
||||||
await ch.delete()
|
await ch.delete()
|
||||||
except:
|
logger.info(
|
||||||
pass
|
f"[scan_existing_custom_vcs] Deleted empty leftover channel: {ch.name} (ID {ch.id})"
|
||||||
logger.info(f"Deleted empty leftover channel: {ch.name}")
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"[scan_existing_custom_vcs] Could not delete leftover VC {ch.id}: {e}",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# pick first occupant
|
# pick first occupant as owner
|
||||||
first = ch.members[0]
|
first = ch.members[0]
|
||||||
self.CHANNEL_COUNTER += 1
|
self.CHANNEL_COUNTER += 1
|
||||||
self.CUSTOM_VC_INFO[ch.id] = {
|
self.CUSTOM_VC_INFO[ch.id] = {
|
||||||
"owner_id": first.id,
|
"owner_id": first.id,
|
||||||
|
"autoname": False,
|
||||||
"locked": False,
|
"locked": False,
|
||||||
"allowed_ids": set(),
|
"allowed_ids": set(),
|
||||||
"denied_ids": set(),
|
"denied_ids": set(),
|
||||||
"user_limit": None,
|
"user_limit": None,
|
||||||
"bitrate": None,
|
"bitrate": None,
|
||||||
|
"last_rename_time": None,
|
||||||
}
|
}
|
||||||
logger.info(f"Assigned {first.display_name} as owner of leftover VC: {ch.name}")
|
logger.info(
|
||||||
|
f"[scan_existing_custom_vcs] Assigned {first.display_name} as owner "
|
||||||
|
f"of leftover VC: {ch.name} (ID {ch.id})."
|
||||||
|
)
|
||||||
if hasattr(ch, "send"):
|
if hasattr(ch, "send"):
|
||||||
try:
|
try:
|
||||||
await ch.send(
|
await ch.send(
|
||||||
f"{first.mention}, you're now the owner of leftover channel **{ch.name}** after a restart. "
|
f"{first.mention}, you're now the owner of leftover channel **{ch.name}** "
|
||||||
"Type `!customvc` here to see available commands."
|
"after a restart. Type `!customvc` here to see available commands."
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
@ -337,20 +547,48 @@ class CustomVCCog(commands.Cog):
|
||||||
await ctx.send(f"Renamed channel to **{new_name}**.")
|
await ctx.send(f"Renamed channel to **{new_name}**.")
|
||||||
|
|
||||||
@customvc.command(name="autoname")
|
@customvc.command(name="autoname")
|
||||||
async def enable_autoname(self, ctx: commands.Context):
|
async def toggle_autoname(self, ctx: commands.Context):
|
||||||
"""Enables automatic channel naming based on the owner's game."""
|
"""
|
||||||
|
Toggles automatic renaming of the channel based on the owner's current game.
|
||||||
|
- If enabling, we rename immediately.
|
||||||
|
- If disabling, we do nothing else but set the flag to False.
|
||||||
|
"""
|
||||||
vc = self.get_custom_vc_for(ctx)
|
vc = self.get_custom_vc_for(ctx)
|
||||||
if not vc:
|
if not vc:
|
||||||
return await ctx.send("Not in a custom VC.")
|
await ctx.send("You are not in a custom voice channel.")
|
||||||
|
return
|
||||||
if not self.is_initiator_or_mod(ctx, vc.id):
|
if not self.is_initiator_or_mod(ctx, vc.id):
|
||||||
return await ctx.send("No permission to enable auto-naming.")
|
await ctx.send("No permission to toggle auto-naming.")
|
||||||
|
return
|
||||||
|
|
||||||
info = self.CUSTOM_VC_INFO[vc.id]
|
info = self.CUSTOM_VC_INFO[vc.id]
|
||||||
info["autoname"] = True
|
if info["autoname"]:
|
||||||
|
info["autoname"] = False
|
||||||
|
logger.info(f"[autoname] Disabled for VC {vc.id}.")
|
||||||
|
await ctx.send("Auto-naming disabled. The channel name will remain as-is.")
|
||||||
|
else:
|
||||||
|
info["autoname"] = True
|
||||||
|
logger.info(f"[autoname] Enabled for VC {vc.id}.")
|
||||||
|
await ctx.send(
|
||||||
|
"Auto-naming enabled! The channel will periodically update to reflect your current game."
|
||||||
|
)
|
||||||
|
# rename now
|
||||||
|
owner_id = info["owner_id"]
|
||||||
|
owner = vc.guild.get_member(owner_id)
|
||||||
|
if owner:
|
||||||
|
await self.update_channel_name(owner, vc, immediate=True)
|
||||||
|
|
||||||
await self.update_channel_name(ctx.author, vc)
|
|
||||||
|
|
||||||
await ctx.send("Auto-naming enabled! Your channel will update based on the game you're playing.")
|
|
||||||
|
def get_current_game(self, member: discord.Member) -> str | None:
|
||||||
|
"""
|
||||||
|
Return the name of the game if the member is playing one, else None.
|
||||||
|
"""
|
||||||
|
for activity in member.activities or []:
|
||||||
|
# In modern discord.py, a "game" is typically an activity with type=ActivityType.playing
|
||||||
|
if activity.type == discord.ActivityType.playing and activity.name:
|
||||||
|
return activity.name
|
||||||
|
return None
|
||||||
|
|
||||||
async def claim_channel_logic(self, ctx: commands.Context):
|
async def claim_channel_logic(self, ctx: commands.Context):
|
||||||
vc = self.get_custom_vc_for(ctx)
|
vc = self.get_custom_vc_for(ctx)
|
||||||
|
@ -510,24 +748,59 @@ class CustomVCCog(commands.Cog):
|
||||||
info["status"] = status_str
|
info["status"] = status_str
|
||||||
await ctx.send(f"Channel status set to: **{status_str}** (placeholder).")
|
await ctx.send(f"Channel status set to: **{status_str}** (placeholder).")
|
||||||
|
|
||||||
async def update_channel_name(self, member: discord.Member, vc: discord.VoiceChannel):
|
async def update_channel_name(
|
||||||
"""Dynamically renames VC based on the user's game or predefined rules."""
|
self, member: discord.Member, vc: discord.VoiceChannel, immediate: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Attempt to rename the channel to reflect the owner’s current game or fallback.
|
||||||
|
If `immediate=True`, we ignore the rename cooldown (used for first-time enabling).
|
||||||
|
"""
|
||||||
if not vc or not member:
|
if not vc or not member:
|
||||||
return
|
return
|
||||||
|
|
||||||
game_name = None
|
desired_name = self.get_desired_name(member)
|
||||||
if member.activities:
|
current_name = vc.name
|
||||||
for activity in member.activities:
|
if current_name == desired_name:
|
||||||
if isinstance(activity, discord.Game):
|
logger.debug(
|
||||||
game_name = activity.name
|
f"[update_channel_name] VC {vc.id} already named '{current_name}', no change needed."
|
||||||
break
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if game_name:
|
info = self.CUSTOM_VC_INFO.get(vc.id)
|
||||||
new_name = f"{game_name}"
|
if not info:
|
||||||
|
return # safety check
|
||||||
|
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
last_rename = info.get("last_rename_time")
|
||||||
|
if immediate:
|
||||||
|
logger.debug(
|
||||||
|
f"[update_channel_name] Doing an immediate rename of VC {vc.id} to '{desired_name}'."
|
||||||
|
)
|
||||||
|
await vc.edit(name=desired_name, reason="Manual immediate rename (autoname toggled on).")
|
||||||
|
info["last_rename_time"] = now
|
||||||
else:
|
else:
|
||||||
new_name = f"{member.display_name}'s Channel"
|
# Normal logic with cooldown
|
||||||
|
if last_rename is None:
|
||||||
|
logger.debug(
|
||||||
|
f"[update_channel_name] VC {vc.id}: first rename => '{current_name}' -> '{desired_name}'."
|
||||||
|
)
|
||||||
|
await vc.edit(name=desired_name, reason="Auto-naming initial rename.")
|
||||||
|
info["last_rename_time"] = now
|
||||||
|
else:
|
||||||
|
diff = (now - last_rename).total_seconds()
|
||||||
|
if diff >= self.rename_cooldown_seconds:
|
||||||
|
logger.debug(
|
||||||
|
f"[update_channel_name] VC {vc.id} rename from '{current_name}' -> '{desired_name}'"
|
||||||
|
f"(cooldown {int(diff)}s >= {self.rename_cooldown_seconds})."
|
||||||
|
)
|
||||||
|
await vc.edit(name=desired_name, reason="Auto-naming update.")
|
||||||
|
info["last_rename_time"] = now
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"[update_channel_name] Skipping rename for VC {vc.id}, only {int(diff)}s "
|
||||||
|
f"since last rename (cooldown {self.rename_cooldown_seconds}s)."
|
||||||
|
)
|
||||||
|
|
||||||
await vc.edit(name=new_name, reason="Automatic VC Naming")
|
|
||||||
|
|
||||||
async def op_user_logic(self, ctx: commands.Context, user: str):
|
async def op_user_logic(self, ctx: commands.Context, user: str):
|
||||||
vc = self.get_custom_vc_for(ctx)
|
vc = self.get_custom_vc_for(ctx)
|
||||||
|
@ -552,6 +825,7 @@ class CustomVCCog(commands.Cog):
|
||||||
if not info:
|
if not info:
|
||||||
return await ctx.send("No data found for this channel?")
|
return await ctx.send("No data found for this channel?")
|
||||||
|
|
||||||
|
autoname_str = "Yes" if info["autoname"] else "No"
|
||||||
locked_str = "Yes" if info["locked"] else "No"
|
locked_str = "Yes" if info["locked"] else "No"
|
||||||
user_lim = info["user_limit"] if info["user_limit"] else "Default"
|
user_lim = info["user_limit"] if info["user_limit"] else "Default"
|
||||||
bitrate_str = info["bitrate"] if info["bitrate"] else "Default (64 kbps)"
|
bitrate_str = info["bitrate"] if info["bitrate"] else "Default (64 kbps)"
|
||||||
|
@ -566,6 +840,7 @@ class CustomVCCog(commands.Cog):
|
||||||
msg = (
|
msg = (
|
||||||
f"**Channel Settings**\n"
|
f"**Channel Settings**\n"
|
||||||
f"Owner: {owner_str}\n"
|
f"Owner: {owner_str}\n"
|
||||||
|
f"Auto rename: {autoname_str}\n"
|
||||||
f"Locked: {locked_str}\n"
|
f"Locked: {locked_str}\n"
|
||||||
f"User Limit: {user_lim}\n"
|
f"User Limit: {user_lim}\n"
|
||||||
f"Bitrate: {bitrate_str}\n"
|
f"Bitrate: {bitrate_str}\n"
|
||||||
|
|
|
@ -197,7 +197,7 @@ class Logger:
|
||||||
Retrieves the calling function's name using `inspect`.
|
Retrieves the calling function's name using `inspect`.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
caller_frame = inspect.stack()[2]
|
caller_frame = inspect.stack()[3]
|
||||||
return caller_frame.function
|
return caller_frame.function
|
||||||
except Exception:
|
except Exception:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
Loading…
Reference in New Issue