diff --git a/bot.py b/bot.py index b40c455..682a1be 100644 --- a/bot.py +++ b/bot.py @@ -19,7 +19,6 @@ from pathlib import Path from streamscrobbler import streamscrobbler from favorites_manager import get_favorites_manager -from permissions import get_permission_manager, can_set_favorites_check, can_remove_favorites_check, can_manage_roles_check from stream_validator import get_stream_validator from input_validator import get_input_validator from ui_components import FavoritesView, create_favorites_embed, create_favorites_list_embed, create_role_setup_embed, ConfirmationView @@ -57,7 +56,6 @@ # END CLUSTERING intents = discord.Intents.default() -intents.members = True intents.guilds = True intents.voice_states = True # minimal member cache: only cache members related to events / interactions @@ -457,253 +455,6 @@ async def maint(interaction: discord.Interaction, status: bool = True): logger.info("😂 Pleb tried to put me in maintenance mode") await interaction.response.send_message("Awww look at you, how cute") -### FAVORITES COMMANDS ### - -@bot.tree.command( - name='set-favorite', - description="Add a radio station to favorites" -) -@discord.app_commands.checks.cooldown(rate=1, per=5) -@is_channel() -async def set_favorite(interaction: discord.Interaction, url: str, name: str = None): - # Check permissions - perm_manager = get_permission_manager() - if not perm_manager.can_set_favorites(interaction.guild.id, interaction.user): - await interaction.response.send_message( - "❌ You don't have permission to set favorites. Ask an admin to assign you the appropriate role.", - ephemeral=True - ) - return - - # Validate URL format first - if not is_valid_url(url): - await interaction.response.send_message("❌ Please provide a valid URL.", ephemeral=True) - return - - await interaction.response.send_message("🔍 Validating stream and adding to favorites...") - - try: - favorites_manager = get_favorites_manager() - result = await favorites_manager.add_favorite( - guild_id=interaction.guild.id, - url=url, - name=name, - user_id=interaction.user.id - ) - - if result['success']: - await interaction.edit_original_response( - content=f"✅ Added **{result['station_name']}** as favorite #{result['favorite_number']}" - ) - else: - await interaction.edit_original_response( - content=f"❌ Failed to add favorite: {result['error']}" - ) - - except Exception as e: - logger.error(f"Error in set_favorite command: {e}") - await interaction.edit_original_response( - content="❌ An unexpected error occurred while adding the favorite." - ) - -@bot.tree.command( - name='play-favorite', - description="Play a favorite radio station by number" -) -@discord.app_commands.checks.cooldown(rate=1, per=5) -@is_channel() -async def play_favorite(interaction: discord.Interaction, number: int): - try: - favorites_manager = get_favorites_manager() - favorite = favorites_manager.get_favorite_by_number(interaction.guild.id, number) - - if not favorite: - await interaction.response.send_message(f"❌ Favorite #{number} not found.", ephemeral=True) - return - - await interaction.response.send_message( - f"🎵 Starting favorite #{number}: **{favorite['station_name']}**" - ) - await play_stream(interaction, favorite['stream_url']) - - except Exception as e: - logger.error(f"Error in play_favorite command: {e}") - if interaction.response.is_done(): - await interaction.followup.send("❌ An error occurred while playing the favorite.", ephemeral=True) - else: - await interaction.response.send_message("❌ An error occurred while playing the favorite.", ephemeral=True) - -@bot.tree.command( - name='favorites', - description="Show favorites with clickable buttons" -) -@discord.app_commands.checks.cooldown(rate=1, per=10) -@is_channel() -async def favorites(interaction: discord.Interaction): - try: - favorites_manager = get_favorites_manager() - favorites_list = favorites_manager.get_favorites(interaction.guild.id) - - if not favorites_list: - await interaction.response.send_message( - "📻 No favorites set for this server yet! Use `/set-favorite` to add some.", - ephemeral=True - ) - return - - # Create embed and view with buttons - embed = create_favorites_embed(favorites_list, 0, interaction.guild.name) - view = FavoritesView(favorites_list, 0) - - await interaction.response.send_message(embed=embed, view=view) - - except Exception as e: - logger.error(f"Error in favorites command: {e}") - await interaction.response.send_message("❌ An error occurred while loading favorites.", ephemeral=True) - -@bot.tree.command( - name='list-favorites', - description="List all favorites (text only, mobile-friendly)" -) -@discord.app_commands.checks.cooldown(rate=1, per=5) -@is_channel() -async def list_favorites(interaction: discord.Interaction): - try: - favorites_manager = get_favorites_manager() - favorites_list = favorites_manager.get_favorites(interaction.guild.id) - - embed = create_favorites_list_embed(favorites_list, interaction.guild.name) - await interaction.response.send_message(embed=embed) - - except Exception as e: - logger.error(f"Error in list_favorites command: {e}") - await interaction.response.send_message("❌ An error occurred while listing favorites.", ephemeral=True) - -@bot.tree.command( - name='remove-favorite', - description="Remove a favorite radio station" -) -@discord.app_commands.checks.cooldown(rate=1, per=5) -@is_channel() -async def remove_favorite(interaction: discord.Interaction, number: int): - # Check permissions - perm_manager = get_permission_manager() - if not perm_manager.can_remove_favorites(interaction.guild.id, interaction.user): - await interaction.response.send_message( - "❌ You don't have permission to remove favorites. Ask an admin to assign you the appropriate role.", - ephemeral=True - ) - return - - try: - favorites_manager = get_favorites_manager() - - # Check if favorite exists first - favorite = favorites_manager.get_favorite_by_number(interaction.guild.id, number) - if not favorite: - await interaction.response.send_message(f"❌ Favorite #{number} not found.", ephemeral=True) - return - - # Create confirmation view - view = ConfirmationView("remove", f"favorite #{number}: {favorite['station_name']}") - await interaction.response.send_message( - f"⚠️ Are you sure you want to remove favorite #{number}: **{favorite['station_name']}**?\n" - f"This will reorder all subsequent favorites.", - view=view - ) - - # Wait for confirmation - await view.wait() - - if view.confirmed: - result = favorites_manager.remove_favorite(interaction.guild.id, number) - if result['success']: - await interaction.followup.send( - f"✅ Removed **{result['station_name']}** from favorites. Subsequent favorites have been renumbered." - ) - else: - await interaction.followup.send(f"❌ Failed to remove favorite: {result['error']}") - - except Exception as e: - logger.error(f"Error in remove_favorite command: {e}") - if interaction.response.is_done(): - await interaction.followup.send("❌ An error occurred while removing the favorite.", ephemeral=True) - else: - await interaction.response.send_message("❌ An error occurred while removing the favorite.", ephemeral=True) - -@bot.tree.command( - name='setup-roles', - description="Configure which Discord roles can manage favorites" -) -@discord.app_commands.checks.cooldown(rate=1, per=5) -@is_channel() -async def setup_roles(interaction: discord.Interaction, role: discord.Role = None, permission_level: str = None): - # Check permissions - perm_manager = get_permission_manager() - if not perm_manager.can_manage_roles(interaction.guild.id, interaction.user): - await interaction.response.send_message( - "❌ You don't have permission to manage role assignments. Ask an admin to assign you the appropriate role.", - ephemeral=True - ) - return - - try: - # If no parameters provided, show current setup - if not role and not permission_level: - role_assignments = perm_manager.get_server_role_assignments(interaction.guild.id) - available_roles = perm_manager.get_available_permission_roles() - - embed = create_role_setup_embed(role_assignments, available_roles, interaction.guild.name) - await interaction.response.send_message(embed=embed) - return - - # Both parameters required for assignment - if not role or not permission_level: - await interaction.response.send_message( - "❌ Please provide both a role and permission level.\n" - "Example: `/setup-roles @DJ dj`\n" - "Available levels: user, dj, radio manager, admin", - ephemeral=True - ) - return - - # Validate permission level - available_roles = perm_manager.get_available_permission_roles() - valid_levels = [r['role_name'] for r in available_roles] - - if permission_level.lower() not in valid_levels: - await interaction.response.send_message( - f"❌ Invalid permission level. Available levels: {', '.join(valid_levels)}", - ephemeral=True - ) - return - - # Assign the role - success = perm_manager.assign_role_permission( - guild_id=interaction.guild.id, - role_id=role.id, - role_name=permission_level.lower() - ) - - if success: - await interaction.response.send_message( - f"✅ Assigned role {role.mention} to permission level **{permission_level}**" - ) - else: - await interaction.response.send_message( - "❌ Failed to assign role permission. Please check the permission level is valid.", - ephemeral=True - ) - - except Exception as e: - logger.error(f"Error in setup_roles command: {e}") - if interaction.response.is_done(): - await interaction.followup.send("❌ An error occurred while setting up roles.", ephemeral=True) - else: - await interaction.response.send_message("❌ An error occurred while setting up roles.", ephemeral=True) - -### END FAVORITES COMMANDS ### - @bot.tree.error async def on_command_error(interaction: discord.Interaction, error): original_error = error.original if hasattr(error, 'original') else error diff --git a/permissions.py b/permissions.py deleted file mode 100644 index 6986a8f..0000000 --- a/permissions.py +++ /dev/null @@ -1,293 +0,0 @@ -""" -Permission management system for BunBot favorites. -Handles role-based permissions with hierarchical levels. -""" - -import logging -from typing import List, Dict, Any, Optional -import discord -from database import get_database - -logger = logging.getLogger('discord') - -class PermissionManager: - """Manages role-based permissions for favorites system""" - - def __init__(self): - self.db = get_database() - - def get_user_permission_level(self, guild_id: int, user: discord.Member) -> int: - """ - Get highest permission level for user based on their roles - - Args: - guild_id: Discord guild ID - user: Discord member object - - Returns: - Highest permission level (1=user, 2=dj, 3=radio manager, 4=admin) - """ - max_level = 1 # Default 'user' level - - try: - # Check each role the user has - for role in user.roles: - level = self.get_role_permission_level(guild_id, role.id) - max_level = max(max_level, level) - - logger.debug(f"User {user.id} in guild {guild_id} has permission level {max_level}") - return max_level - - except Exception as e: - logger.error(f"Error getting permission level for user {user.id}: {e}") - return 1 # Default to user level on error - - def get_role_permission_level(self, guild_id: int, role_id: int) -> int: - """ - Get permission level for a specific Discord role - - Args: - guild_id: Discord guild ID - role_id: Discord role ID - - Returns: - Permission level for the role (1 if not found) - """ - try: - # Query server_roles to get the role mapping - results = self.db.execute_query(""" - SELECT rh.permission_level - FROM server_roles sr - JOIN role_hierarchy rh ON sr.role_name = rh.role_name - WHERE sr.guild_id = ? AND sr.discord_role_id = ? - """, (guild_id, role_id)) - - if results: - return results[0]['permission_level'] - - return 1 # Default user level - - except Exception as e: - logger.error(f"Error getting role permission level: {e}") - return 1 - - def has_permission(self, guild_id: int, user: discord.Member, permission: str) -> bool: - """ - Check if user has a specific permission - - Args: - guild_id: Discord guild ID - user: Discord member object - permission: Permission name (can_set_favorites, can_remove_favorites, can_manage_roles) - - Returns: - True if user has the permission - """ - # Whitelist valid permission columns to prevent SQL injection - valid_permissions = { - 'can_set_favorites', - 'can_remove_favorites', - 'can_manage_roles' - } - - if permission not in valid_permissions: - logger.error(f"Invalid permission requested: {permission}") - return False - - try: - # Get all permissions for user's roles - role_ids = [role.id for role in user.roles] - if not role_ids: - return False - - # Create placeholders for SQL IN clause - placeholders = ','.join('?' * len(role_ids)) - - # Use parameterized query with whitelisted column name - query = f""" - SELECT rh.{permission} - FROM server_roles sr - JOIN role_hierarchy rh ON sr.role_name = rh.role_name - WHERE sr.guild_id = ? AND sr.discord_role_id IN ({placeholders}) - AND rh.{permission} = 1 - """ - - results = self.db.execute_query(query, (guild_id, *role_ids)) - - has_perm = len(results) > 0 - logger.debug(f"User {user.id} permission check for {permission}: {has_perm}") - return has_perm - - except Exception as e: - logger.error(f"Error checking permission {permission} for user {user.id}: {e}") - return False - - def can_set_favorites(self, guild_id: int, user: discord.Member) -> bool: - """Check if user can set favorites""" - return self.has_permission(guild_id, user, 'can_set_favorites') - - def can_remove_favorites(self, guild_id: int, user: discord.Member) -> bool: - """Check if user can remove favorites""" - return self.has_permission(guild_id, user, 'can_remove_favorites') - - def can_manage_roles(self, guild_id: int, user: discord.Member) -> bool: - """Check if user can manage role assignments""" - return self.has_permission(guild_id, user, 'can_manage_roles') - - def assign_role_permission(self, guild_id: int, role_id: int, role_name: str) -> bool: - """ - Assign a permission level to a Discord role - - Args: - guild_id: Discord guild ID - role_id: Discord role ID - role_name: Permission role name (user, dj, radio manager, admin) - - Returns: - True if assignment was successful - """ - try: - # Check if role_name exists in hierarchy - hierarchy_check = self.db.execute_query( - "SELECT role_name FROM role_hierarchy WHERE role_name = ?", - (role_name,) - ) - - if not hierarchy_check: - logger.error(f"Invalid role name: {role_name}") - return False - - # Insert or update the role assignment - self.db.execute_non_query(""" - INSERT OR REPLACE INTO server_roles (guild_id, discord_role_id, role_name) - VALUES (?, ?, ?) - """, (guild_id, role_id, role_name)) - - logger.info(f"Assigned role {role_id} in guild {guild_id} to permission level {role_name}") - return True - - except Exception as e: - logger.error(f"Error assigning role permission: {e}") - return False - - def remove_role_permission(self, guild_id: int, role_id: int) -> bool: - """ - Remove permission assignment from a Discord role - - Args: - guild_id: Discord guild ID - role_id: Discord role ID - - Returns: - True if removal was successful - """ - try: - affected_rows = self.db.execute_non_query(""" - DELETE FROM server_roles - WHERE guild_id = ? AND discord_role_id = ? - """, (guild_id, role_id)) - - success = affected_rows > 0 - if success: - logger.info(f"Removed role permission for role {role_id} in guild {guild_id}") - else: - logger.warning(f"No permission found for role {role_id} in guild {guild_id}") - - return success - - except Exception as e: - logger.error(f"Error removing role permission: {e}") - return False - - def get_server_role_assignments(self, guild_id: int) -> List[Dict[str, Any]]: - """ - Get all role assignments for a server - - Args: - guild_id: Discord guild ID - - Returns: - List of role assignments with permission details - """ - try: - results = self.db.execute_query(""" - SELECT sr.discord_role_id, sr.role_name, rh.permission_level, - rh.can_set_favorites, rh.can_remove_favorites, rh.can_manage_roles - FROM server_roles sr - JOIN role_hierarchy rh ON sr.role_name = rh.role_name - WHERE sr.guild_id = ? - ORDER BY rh.permission_level DESC - """, (guild_id,)) - - return results - - except Exception as e: - logger.error(f"Error getting server role assignments: {e}") - return [] - - def get_available_permission_roles(self) -> List[Dict[str, Any]]: - """ - Get all available permission roles from hierarchy - - Returns: - List of available permission roles - """ - try: - results = self.db.execute_query(""" - SELECT role_name, permission_level, can_set_favorites, - can_remove_favorites, can_manage_roles - FROM role_hierarchy - ORDER BY permission_level ASC - """) - - return results - - except Exception as e: - logger.error(f"Error getting available permission roles: {e}") - return [] - -# Global permission manager instance -_permission_manager = None - -def get_permission_manager() -> PermissionManager: - """Get global permission manager instance""" - global _permission_manager - if _permission_manager is None: - _permission_manager = PermissionManager() - return _permission_manager - -# Decorator for checking permissions -def requires_permission(permission_check): - """ - Decorator to check permissions before executing a command - - Args: - permission_check: Function that takes (guild_id, user) and returns bool - """ - def decorator(func): - async def wrapper(interaction: discord.Interaction, *args, **kwargs): - perm_manager = get_permission_manager() - - if not permission_check(interaction.guild.id, interaction.user): - await interaction.response.send_message( - "❌ You don't have permission to use this command.", - ephemeral=True - ) - return - - return await func(interaction, *args, **kwargs) - return wrapper - return decorator - -# Common permission check functions -def can_set_favorites_check(guild_id: int, user: discord.Member) -> bool: - """Permission check function for setting favorites""" - return get_permission_manager().can_set_favorites(guild_id, user) - -def can_remove_favorites_check(guild_id: int, user: discord.Member) -> bool: - """Permission check function for removing favorites""" - return get_permission_manager().can_remove_favorites(guild_id, user) - -def can_manage_roles_check(guild_id: int, user: discord.Member) -> bool: - """Permission check function for managing roles""" - return get_permission_manager().can_manage_roles(guild_id, user) diff --git a/services/health_monitor.py b/services/health_monitor.py index e4fe04d..1eed806 100644 --- a/services/health_monitor.py +++ b/services/health_monitor.py @@ -27,7 +27,8 @@ async def execute(self, guild_id: int, state: dict[int, dict[str, str]], station # Update the last time we saw a user in the chat guild = self.client.get_guild(guild_id) # TODO: Check guild.voice_client.channel.members for any bots: https://discordpy.readthedocs.io/en/latest/api.html?highlight=voicechannel#discord.Member.bot - if guild.voice_client is not None and len(guild.voice_client.channel.members) > 1: + in_channel = [ x for x in guild.voice_client.channel.members if not x.bot ] + if guild.voice_client is not None and len(in_channel) > 1: self.state_manager.set_state(guild.id, 'last_active_user_time', datetime.datetime.now(datetime.UTC)) return result diff --git a/ui_components.py b/ui_components.py index 6d71acd..220384d 100644 --- a/ui_components.py +++ b/ui_components.py @@ -7,64 +7,63 @@ import discord from typing import List, Dict, Any from favorites_manager import get_favorites_manager -from permissions import get_permission_manager logger = logging.getLogger('discord') class FavoritesView(discord.ui.View): """Discord view with buttons for each favorite station""" - + def __init__(self, favorites: List[Dict[str, Any]], page: int = 0): super().__init__(timeout=300) # 5 minute timeout self.favorites = favorites self.page = page self.max_buttons = 20 # Leave room for navigation buttons - + # Calculate pagination start_idx = page * self.max_buttons end_idx = start_idx + self.max_buttons page_favorites = favorites[start_idx:end_idx] - + # Add button for each favorite on this page for favorite in page_favorites: button = FavoriteButton( - favorite['favorite_number'], + favorite['favorite_number'], favorite['station_name'], favorite['stream_url'] ) self.add_item(button) - + # Add navigation buttons if needed total_pages = (len(favorites) + self.max_buttons - 1) // self.max_buttons - + if total_pages > 1: # Previous page button if page > 0: prev_button = NavigationButton("◀️ Previous", page - 1, favorites) self.add_item(prev_button) - + # Next page button if page < total_pages - 1: next_button = NavigationButton("Next ▶️", page + 1, favorites) self.add_item(next_button) - + async def on_timeout(self): """Called when the view times out""" # Disable all buttons for item in self.children: item.disabled = True - + # Note: We can't edit the message here since we don't have access to it # The bot command should handle timeout by catching the timeout exception class FavoriteButton(discord.ui.Button): """Button for playing a specific favorite station""" - + def __init__(self, number: int, name: str, url: str): # Truncate long names to fit Discord's button label limit (80 chars) display_name = name[:70] if len(name) > 70 else name label = f"{number}. {display_name}" - + super().__init__( label=label, style=discord.ButtonStyle.primary, @@ -74,13 +73,13 @@ def __init__(self, number: int, name: str, url: str): self.favorite_number = number self.station_name = name self.stream_url = url - + async def callback(self, interaction: discord.Interaction): """Handle button click to play favorite""" try: # Import here to avoid circular imports from bot import play_stream - + # Check if user is in a voice channel if not interaction.user.voice or not interaction.user.voice.channel: await interaction.response.send_message( @@ -88,7 +87,7 @@ async def callback(self, interaction: discord.Interaction): ephemeral=True ) return - + # Check if bot is already playing voice_client = interaction.guild.voice_client if voice_client and voice_client.is_playing(): @@ -97,18 +96,18 @@ async def callback(self, interaction: discord.Interaction): ephemeral=True ) return - + # Start playing the favorite await interaction.response.send_message( f"🎵 Starting favorite #{self.favorite_number}: **{self.station_name}**" ) - + # Use the existing play_stream function await play_stream(interaction, self.stream_url) - + except Exception as e: logger.error(f"Error playing favorite #{self.favorite_number}: {e}") - + if interaction.response.is_done(): await interaction.followup.send( f"❌ Error playing {self.station_name}: {str(e)}", @@ -122,7 +121,7 @@ async def callback(self, interaction: discord.Interaction): class NavigationButton(discord.ui.Button): """Button for navigating between pages of favorites""" - + def __init__(self, label: str, target_page: int, all_favorites: List[Dict[str, Any]]): super().__init__( label=label, @@ -131,20 +130,20 @@ def __init__(self, label: str, target_page: int, all_favorites: List[Dict[str, A ) self.target_page = target_page self.all_favorites = all_favorites - + async def callback(self, interaction: discord.Interaction): """Handle navigation button click""" try: # Create new view for the target page new_view = FavoritesView(self.all_favorites, self.target_page) new_embed = create_favorites_embed( - self.all_favorites, - self.target_page, + self.all_favorites, + self.target_page, interaction.guild.name ) - + await interaction.response.edit_message(embed=new_embed, view=new_view) - + except Exception as e: logger.error(f"Error navigating to page {self.target_page}: {e}") await interaction.response.send_message( @@ -154,38 +153,38 @@ async def callback(self, interaction: discord.Interaction): class ConfirmationView(discord.ui.View): """View for confirmation dialogs (e.g., removing favorites)""" - + def __init__(self, action: str, target: str): super().__init__(timeout=60) # 1 minute timeout self.action = action self.target = target self.confirmed = False - + @discord.ui.button(label="✅ Confirm", style=discord.ButtonStyle.danger) async def confirm_button(self, interaction: discord.Interaction, button: discord.ui.Button): """Handle confirmation""" self.confirmed = True self.stop() - + # Disable all buttons for item in self.children: item.disabled = True - + await interaction.response.edit_message( content=f"✅ Confirmed: {self.action} {self.target}", view=self ) - + @discord.ui.button(label="❌ Cancel", style=discord.ButtonStyle.secondary) async def cancel_button(self, interaction: discord.Interaction, button: discord.ui.Button): """Handle cancellation""" self.confirmed = False self.stop() - + # Disable all buttons for item in self.children: item.disabled = True - + await interaction.response.edit_message( content=f"❌ Cancelled: {self.action} {self.target}", view=self @@ -194,25 +193,25 @@ async def cancel_button(self, interaction: discord.Interaction, button: discord. def create_favorites_embed(favorites: List[Dict[str, Any]], page: int = 0, guild_name: str = "Server") -> discord.Embed: """ Create an embed displaying favorites - + Args: favorites: List of favorite data page: Current page number guild_name: Name of the Discord server - + Returns: Discord embed object """ max_buttons = 20 total_pages = (len(favorites) + max_buttons - 1) // max_buttons if favorites else 1 - + # Create embed embed = discord.Embed( title="📻 Radio Station Favorites", color=0x0099ff, description=f"Click a button below to start playing a station!" ) - + if not favorites: embed.add_field( name="No Favorites", @@ -224,34 +223,34 @@ def create_favorites_embed(favorites: List[Dict[str, Any]], page: int = 0, guild start_idx = page * max_buttons end_idx = start_idx + max_buttons page_favorites = favorites[start_idx:end_idx] - + # Add field showing current page favorites favorites_text = [] for fav in page_favorites: favorites_text.append(f"**{fav['favorite_number']}.** {fav['station_name']}") - + embed.add_field( name=f"Favorites ({len(favorites)} total)", value="\n".join(favorites_text), inline=False ) - + # Add pagination info if multiple pages if total_pages > 1: embed.set_footer(text=f"Page {page + 1} of {total_pages}") - + embed.set_author(name=guild_name, icon_url=None) - + return embed def create_favorites_list_embed(favorites: List[Dict[str, Any]], guild_name: str = "Server") -> discord.Embed: """ Create a text-only embed listing all favorites (for mobile users) - + Args: favorites: List of favorite data guild_name: Name of the Discord server - + Returns: Discord embed object """ @@ -260,7 +259,7 @@ def create_favorites_list_embed(favorites: List[Dict[str, Any]], guild_name: str color=0x0099ff, description="Complete list of saved radio stations" ) - + if not favorites: embed.add_field( name="No Favorites", @@ -272,35 +271,35 @@ def create_favorites_list_embed(favorites: List[Dict[str, Any]], guild_name: str chunk_size = 10 for i in range(0, len(favorites), chunk_size): chunk = favorites[i:i + chunk_size] - + favorites_text = [] for fav in chunk: favorites_text.append( f"**{fav['favorite_number']}.** {fav['station_name']}\n" f" 🔗 `{fav['stream_url']}`" ) - + field_name = f"Favorites {i + 1}-{min(i + chunk_size, len(favorites))}" embed.add_field( name=field_name, value="\n\n".join(favorites_text), inline=False ) - + embed.set_footer(text=f"Total: {len(favorites)} favorites | Use /play-favorite to play") embed.set_author(name=guild_name, icon_url=None) - + return embed def create_role_setup_embed(role_assignments: List[Dict[str, Any]], available_roles: List[Dict[str, Any]], guild_name: str = "Server") -> discord.Embed: """ Create an embed showing current role assignments and available permission levels - + Args: role_assignments: Current role assignments for the server available_roles: Available permission roles from hierarchy guild_name: Name of the Discord server - + Returns: Discord embed object """ @@ -309,7 +308,7 @@ def create_role_setup_embed(role_assignments: List[Dict[str, Any]], available_ro color=0xffa500, description="Configure which Discord roles can manage favorites" ) - + # Current assignments if role_assignments: assignments_text = [] @@ -322,10 +321,10 @@ def create_role_setup_embed(role_assignments: List[Dict[str, Any]], available_ro permissions.append("Remove") if assignment['can_manage_roles']: permissions.append("Manage") - + perm_text = ", ".join(permissions) if permissions else "None" assignments_text.append(f"{role_mention} → **{assignment['role_name']}** ({perm_text})") - + embed.add_field( name="Current Role Assignments", value="\n".join(assignments_text), @@ -337,7 +336,7 @@ def create_role_setup_embed(role_assignments: List[Dict[str, Any]], available_ro value="No roles assigned yet", inline=False ) - + # Available permission levels levels_text = [] for role in available_roles: @@ -348,22 +347,22 @@ def create_role_setup_embed(role_assignments: List[Dict[str, Any]], available_ro permissions.append("Remove") if role['can_manage_roles']: permissions.append("Manage") - + perm_text = ", ".join(permissions) if permissions else "None" levels_text.append(f"**{role['role_name']}** (Level {role['permission_level']}) - {perm_text}") - + embed.add_field( name="Available Permission Levels", value="\n".join(levels_text), inline=False ) - + embed.add_field( name="Usage", value="Use `/setup-roles @role permission_level` to assign permissions\nExample: `/setup-roles @DJ dj`", inline=False ) - + embed.set_author(name=guild_name, icon_url=None) - + return embed