diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index c73e032..6ed8820 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -17,7 +17,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint + pip install -r requirements-dev.txt + pip install -r requirements.txt - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') diff --git a/gitcord/bot.py b/gitcord/bot.py index 98f2edf..e629d60 100644 --- a/gitcord/bot.py +++ b/gitcord/bot.py @@ -45,19 +45,15 @@ async def setup_hook(self) -> None: async def _load_cogs(self) -> None: """Load all bot cogs.""" - try: - # Load the general cog - await self.load_extension("gitcord.cogs.general") - logger.info("Loaded general cog") + # Load the general cog + await self.load_extension("gitcord.cogs.general") + logger.info("Loaded general cog") - # Add more cogs here as they are created - # await self.load_extension("gitcord.cogs.git") - # await self.load_extension("gitcord.cogs.admin") + # Add more cogs here as they are created + # await self.load_extension("gitcord.cogs.git") + # await self.load_extension("gitcord.cogs.admin") - except Exception as e: - logger.error("Failed to load cogs: %s", e) - - async def on_command_error(self, context, error): + async def on_command_error(self, context, error): # pylint: disable=arguments-differ """Global command error handler.""" logger.error("Global command error: %s", error) if isinstance(error, commands.CommandNotFound): @@ -82,8 +78,6 @@ async def main() -> None: logger.error("Invalid Discord token! Please check your DISCORD_TOKEN in the .env file.") except ValueError as e: logger.error("Configuration error: %s", e) - except Exception as e: - logger.error("Error starting bot: %s", e) def run_bot() -> None: diff --git a/gitcord/cogs/general.py b/gitcord/cogs/general.py index d3dcb13..c46596c 100644 --- a/gitcord/cogs/general.py +++ b/gitcord/cogs/general.py @@ -4,17 +4,16 @@ """ import re -import yaml -import os import discord -from discord.ext import commands -from discord import app_commands import requests +import yaml from bs4 import BeautifulSoup +from discord import app_commands +from discord.ext import commands +from ..utils.helpers import format_latency, create_embed, parse_channel_config from ..utils.logger import main_logger as logger -from ..utils.helpers import format_latency, create_embed class General(commands.Cog): @@ -39,7 +38,7 @@ async def fetchurl_prefix(self, ctx: commands.Context, url: str) -> None: # Fetch the webpage headers = { 'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' - '(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36') + '(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36') } response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() @@ -118,7 +117,7 @@ async def fetchurl(self, interaction: discord.Interaction, url: str) -> None: # Fetch the webpage headers = { 'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' - '(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36') + '(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36') } response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() @@ -221,7 +220,7 @@ async def ping_prefix(self, ctx: commands.Context) -> None: async def createchannel(self, ctx: commands.Context) -> None: """Create a channel based on properties defined in a YAML file.""" yaml_path = "/home/user/Projects/gitcord-template/community/off-topic.yaml" - + try: # Check if the user has permission to manage channels if not ctx.author.guild_permissions.manage_channels: @@ -233,32 +232,17 @@ async def createchannel(self, ctx: commands.Context) -> None: await ctx.send(embed=embed) return - # Check if YAML file exists - if not os.path.exists(yaml_path): + try: + channel_config = parse_channel_config(yaml_path) + except ValueError as e: embed = create_embed( - title="❌ File Not Found", - description=f"YAML file not found at: {yaml_path}", + title="❌ Invalid YAML", + description=str(e), color=discord.Color.red() ) await ctx.send(embed=embed) return - # Read and parse YAML file - with open(yaml_path, 'r', encoding='utf-8') as file: - channel_config = yaml.safe_load(file) - - # Validate required fields - required_fields = ['name', 'type'] - for field in required_fields: - if field not in channel_config: - embed = create_embed( - title="❌ Invalid YAML", - description=f"Missing required field: {field}", - color=discord.Color.red() - ) - await ctx.send(embed=embed) - return - # Prepare channel creation parameters channel_kwargs = { 'name': channel_config['name'], @@ -266,45 +250,42 @@ async def createchannel(self, ctx: commands.Context) -> None: 'nsfw': channel_config.get('nsfw', False) } - # Set channel type + # Set position if specified + if 'position' in channel_config: + channel_kwargs['position'] = channel_config['position'] + + # Create the channel based on type if channel_config['type'].lower() == 'text': - channel_type = discord.ChannelType.text + new_channel = await ctx.guild.create_text_channel(**channel_kwargs) elif channel_config['type'].lower() == 'voice': - channel_type = discord.ChannelType.voice + new_channel = await ctx.guild.create_voice_channel(**channel_kwargs) else: embed = create_embed( title="❌ Invalid Channel Type", - description=f"Channel type '{channel_config['type']}' is not supported. Use 'text' or 'voice'.", + description=f"Channel type '{channel_config['type']}' is not supported. " + "Use 'text' or 'voice'.", color=discord.Color.red() ) await ctx.send(embed=embed) return - # Set position if specified - if 'position' in channel_config: - channel_kwargs['position'] = channel_config['position'] - - # Create the channel based on type - if channel_type == discord.ChannelType.text: - new_channel = await ctx.guild.create_text_channel(**channel_kwargs) - else: # voice channel - new_channel = await ctx.guild.create_voice_channel(**channel_kwargs) - # Create success embed embed = create_embed( title="✅ Channel Created", description=f"Successfully created channel: {new_channel.mention}", color=discord.Color.green() ) - + # Add fields manually - embed.add_field(name="Name", value=channel_config['name'], inline=True) - embed.add_field(name="Type", value=channel_config['type'], inline=True) - embed.add_field(name="NSFW", value=str(channel_config.get('nsfw', False)), inline=True) - embed.add_field(name="Topic", value=channel_config.get('topic', 'No topic set'), inline=False) + embed.add_field(name="Name", value=channel_kwargs['name'], inline=True) + embed.add_field(name="Type", value=channel_kwargs['type'], inline=True) + embed.add_field(name="NSFW", value=channel_kwargs['nsfw'], inline=True) + embed.add_field(name="Topic", value=channel_config.get('topic', 'No topic set'), + inline=False) await ctx.send(embed=embed) - logger.info("Channel '%s' created successfully by %s", channel_config['name'], ctx.author) + logger.info("Channel '%s' created successfully by %s", channel_config['name'], + ctx.author) except yaml.YAMLError as e: embed = create_embed( @@ -340,7 +321,8 @@ async def createchannel(self, ctx: commands.Context) -> None: logger.error("Unexpected error in createchannel command: %s", e) @createchannel.error - async def createchannel_error(self, ctx: commands.Context, error: commands.CommandError) -> None: + async def createchannel_error(self, ctx: commands.Context, + error: commands.CommandError) -> None: """Handle errors for the createchannel command.""" if isinstance(error, commands.MissingPermissions): embed = create_embed( diff --git a/gitcord/utils/helpers.py b/gitcord/utils/helpers.py index e7467a1..158af29 100644 --- a/gitcord/utils/helpers.py +++ b/gitcord/utils/helpers.py @@ -1,11 +1,12 @@ """ Helper utilities for GitCord bot. """ - +import os from datetime import datetime from typing import Optional, Union import discord +import yaml def format_latency(latency: float) -> str: @@ -97,3 +98,20 @@ def format_time_delta(seconds: float) -> str: return f"{minutes:.1f}m" hours = seconds / 3600 return f"{hours:.1f}h" + + +def parse_channel_config(yaml_path: str) -> dict: + """Parse and validate the YAML configuration file.""" + if not os.path.exists(yaml_path): + raise ValueError(f"YAML file not found at: {yaml_path}") + + with open(yaml_path, 'r', encoding='utf-8') as file: + channel_config = yaml.safe_load(file) + + # Validate required fields + required_fields = ['name', 'type'] + for field in required_fields: + if field not in channel_config: + raise ValueError(f"Missing required field: {field}") + + return channel_config diff --git a/gitcord/utils/logger.py b/gitcord/utils/logger.py index 2b2f12e..5424a13 100644 --- a/gitcord/utils/logger.py +++ b/gitcord/utils/logger.py @@ -35,8 +35,8 @@ def setup_logger(name: str = "gitcord", level: int = logging.INFO) -> logging.Lo return logger -def log_error(log_instance: logging.Logger, error: Exception, context: Optional[str] = None) -> None: - +def log_error(log_instance: logging.Logger, error: Exception, + context: Optional[str] = None) -> None: """ Log an error with context. @@ -50,5 +50,6 @@ def log_error(log_instance: logging.Logger, error: Exception, context: Optional[ message = f"{context} - {message}" log_instance.error(message, exc_info=True) # pylint: disable=line-too-long + # Default logger instance main_logger = setup_logger() diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..893a64f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pylint>=3.3.7 +setuptools>=80.9.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 95b71ca..d7d767e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ discord.py>=2.5.2 python-dotenv==1.0.0 requests>=2.31.0 -beautifulsoup4>=4.12.0 \ No newline at end of file +beautifulsoup4>=4.12.0 +PyYAML>=6.0.2 \ No newline at end of file