├── .python-version ├── docs ├── .gitignore ├── src │ ├── api-reference │ │ ├── cogs.md │ │ ├── utils.md │ │ ├── bot.md │ │ └── constants.md │ ├── developer-guide │ │ ├── cogs.md │ │ ├── events.md │ │ ├── utilities.md │ │ ├── architecture.md │ │ └── contributing.md │ ├── templates │ │ ├── channel-templates.md │ │ ├── custom-templates.md │ │ └── category-templates.md │ ├── troubleshooting │ │ ├── debug-mode.md │ │ ├── common-issues.md │ │ └── error-messages.md │ ├── SUMMARY.md │ ├── user-guide │ │ ├── permissions.md │ │ ├── categories.md │ │ ├── channel-management.md │ │ └── commands.md │ ├── introduction.md │ └── getting-started │ │ ├── installation.md │ │ ├── configuration.md │ │ └── quick-start.md ├── book.toml ├── custom.css └── custom.js ├── src ├── gitcord │ ├── constants │ │ ├── __init__.py │ │ ├── permissions.py │ │ ├── paths.py │ │ └── messages.py │ ├── utils │ │ ├── __init__.py │ │ ├── template_metadata.py │ │ ├── logger.py │ │ ├── category_helpers.py │ │ └── helpers.py │ ├── __init__.py │ ├── __main__.py │ ├── cogs │ │ ├── __init__.py │ │ ├── utility.py │ │ ├── base_cog.py │ │ ├── help.py │ │ └── channels.py │ ├── views │ │ ├── __init__.py │ │ ├── channel_views.py │ │ └── base_views.py │ ├── config.py │ ├── bot.py │ └── events.py └── .template_source.json ├── branding ├── logo.png └── logo.xcf ├── .env.example ├── .github ├── workflows │ ├── main.yml │ ├── greetings.yml │ ├── pylint.yml │ ├── dependency-review.yml │ ├── mdbook.yml │ └── codeql.yml ├── PULL_REQUEST_TEMPLATE.md ├── SUPPORT.md ├── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml ├── SECURITY.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── .gitignore ├── pyproject.toml ├── setup_env.py ├── BOT_SETUP.md └── README.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/src/api-reference/cogs.md: -------------------------------------------------------------------------------- 1 | # Cogs 2 | -------------------------------------------------------------------------------- /docs/src/api-reference/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | -------------------------------------------------------------------------------- /docs/src/developer-guide/cogs.md: -------------------------------------------------------------------------------- 1 | # Cogs 2 | -------------------------------------------------------------------------------- /docs/src/api-reference/bot.md: -------------------------------------------------------------------------------- 1 | # Bot Class 2 | -------------------------------------------------------------------------------- /docs/src/developer-guide/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | -------------------------------------------------------------------------------- /docs/src/api-reference/constants.md: -------------------------------------------------------------------------------- 1 | # Constants 2 | -------------------------------------------------------------------------------- /docs/src/developer-guide/utilities.md: -------------------------------------------------------------------------------- 1 | # Utilities 2 | -------------------------------------------------------------------------------- /docs/src/developer-guide/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | -------------------------------------------------------------------------------- /docs/src/developer-guide/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | -------------------------------------------------------------------------------- /docs/src/templates/channel-templates.md: -------------------------------------------------------------------------------- 1 | # Channel Templates 2 | -------------------------------------------------------------------------------- /src/gitcord/constants/__init__.py: -------------------------------------------------------------------------------- 1 | # Make constants a package 2 | -------------------------------------------------------------------------------- /docs/src/templates/custom-templates.md: -------------------------------------------------------------------------------- 1 | # Creating Custom Templates 2 | -------------------------------------------------------------------------------- /src/gitcord/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for GitCord bot. 3 | """ 4 | -------------------------------------------------------------------------------- /branding/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allthingslinux/gitcord/HEAD/branding/logo.png -------------------------------------------------------------------------------- /branding/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allthingslinux/gitcord/HEAD/branding/logo.xcf -------------------------------------------------------------------------------- /docs/src/troubleshooting/debug-mode.md: -------------------------------------------------------------------------------- 1 | # Debug Mode 2 | 3 | As of the time of writing this, there is no debug mode. Contributions are very welcome. -------------------------------------------------------------------------------- /src/gitcord/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | GitCord - A Discord bot for Git integration 3 | """ 4 | 5 | __version__ = "1.0.0" 6 | __author__ = "GitCord Team" 7 | -------------------------------------------------------------------------------- /src/.template_source.json: -------------------------------------------------------------------------------- 1 | {"url": "https://github.com/evolvewithevan/gitcord-template", "branch": "main", "local_path": "/home/user/Projects/gitcord/gitcord/src/.template_repo"} -------------------------------------------------------------------------------- /src/gitcord/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main entry point for GitCord bot. 3 | """ 4 | 5 | from gitcord.bot import run_bot 6 | 7 | 8 | if __name__ == "__main__": 9 | run_bot() 10 | -------------------------------------------------------------------------------- /src/gitcord/cogs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cog modules for GitCord bot. 3 | """ 4 | 5 | from .admin import Admin 6 | from .channels import Channels 7 | from .help import Help 8 | from .utility import Utility 9 | 10 | __all__ = ["Admin", "Channels", "Help", "Utility"] 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Discord Bot Configuration 2 | # Copy this file to .env and fill in your actual values 3 | DISCORD_TOKEN=YOUR_BOT_TOKEN_HERE 4 | 5 | # Optional: Discord Application ID (useful for slash commands) 6 | DISCORD_APPLICATION_ID=YOUR_APPLICATION_ID_HERE 7 | 8 | # Optional: Discord Guild ID (for testing slash commands in a specific server) 9 | DISCORD_GUILD_ID=YOUR_GUILD_ID_HERE -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruff Lint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | ruff-lint: 10 | name: Ruff Check 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Run Ruff 18 | uses: chartboost/ruff-action@v1 19 | with: 20 | args: check . 21 | -------------------------------------------------------------------------------- /src/gitcord/views/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Views module for GitCord bot UI components. 3 | """ 4 | 5 | from .base_views import BaseView, ConfirmationView, ErrorView, LoadingView 6 | from .channel_views import DeleteExtraChannelsView, ConfirmDeleteView 7 | 8 | __all__ = [ 9 | "BaseView", 10 | "ConfirmationView", 11 | "ErrorView", 12 | "LoadingView", 13 | "DeleteExtraChannelsView", 14 | "ConfirmDeleteView", 15 | ] 16 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["GitCord Team"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "GitCord Documentation" 7 | 8 | [output.html] 9 | git-repository-url = "https://github.com/evolvewithevan/gitcord" 10 | git-repository-icon = "fa-github" 11 | site-url = "https://evolvewithevan.github.io/gitcord/" 12 | git-repository-edit-url = "https://github.com/evolvewithevan/gitcord/edit/main/docs/src/" 13 | additional-css = ["custom.css"] 14 | additional-js = ["custom.js"] 15 | 16 | [output.html.fold] 17 | enable = true 18 | level = 1 19 | -------------------------------------------------------------------------------- /src/gitcord/constants/permissions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Permission constants for GitCord bot. 3 | """ 4 | 5 | # Required permissions 6 | PERM_ADMIN = "administrator" 7 | PERM_MANAGE_CHANNELS = "manage_channels" 8 | 9 | # Permission error messages 10 | ERR_ADMIN_REQUIRED = "You need the 'Administrator' permission to use this command." 11 | ERR_MANAGE_CHANNELS_REQUIRED = ( 12 | "You need the 'Manage Channels' permission to use this command." 13 | ) 14 | ERR_BOT_MISSING_PERMS = "I don't have permission to perform this action in this server." 15 | 16 | # Role-based access control (add as needed) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # Virtual environments 28 | .venv/ 29 | venv/ 30 | env/ 31 | ENV/ 32 | env.bak/ 33 | venv.bak/ 34 | 35 | # IDE 36 | .vscode/ 37 | .idea/ 38 | *.swp 39 | *.swo 40 | *~ 41 | 42 | # OS 43 | .DS_Store 44 | Thumbs.db 45 | 46 | # Logs 47 | *.log 48 | logs/ 49 | src/.template_repo 50 | .gitcord_data/ 51 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: "Welcome to the gitcord repository. Thank you for sharing your Issue <3 We will try our best to review and label your issue promptly!" 16 | pr-message: "Welcome to the gitcord repository. Thank you for making a Pull Request <3 We will try our best to review your PR promptly!" 17 | -------------------------------------------------------------------------------- /src/gitcord/constants/paths.py: -------------------------------------------------------------------------------- 1 | """ 2 | File path constants for GitCord bot. 3 | """ 4 | 5 | import os 6 | 7 | # GitCord data directory for storing per-guild template repositories and metadata 8 | GITCORD_DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".gitcord_data") 9 | os.makedirs(GITCORD_DATA_DIR, exist_ok=True) 10 | 11 | def get_template_repo_dir(guild_id): 12 | """Get the template repository directory for a specific guild.""" 13 | return os.path.join(GITCORD_DATA_DIR, "template_repo", str(guild_id)) 14 | 15 | def get_metadata_file(guild_id): 16 | """Get the metadata file path for a specific guild.""" 17 | return os.path.join(GITCORD_DATA_DIR, f"template_source_{guild_id}.json") 18 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10", "3.11", "3.12", "3.13"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install -r requirements-dev.txt 21 | pip install -r requirements.txt 22 | - name: Analysing the code with pylint 23 | run: | 24 | pylint $(git ls-files '*.py') 25 | -------------------------------------------------------------------------------- /src/gitcord/utils/template_metadata.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from ..constants.paths import get_metadata_file 4 | 5 | def save_metadata(guild_id, data): 6 | with open(get_metadata_file(guild_id), "w", encoding="utf-8") as f: 7 | json.dump(data, f) 8 | 9 | def load_metadata(guild_id): 10 | path = get_metadata_file(guild_id) 11 | if not os.path.exists(path): 12 | return None 13 | with open(path, "r", encoding="utf-8") as f: 14 | return json.load(f) 15 | 16 | def update_metadata(guild_id, key, value): 17 | data = load_metadata(guild_id) or {} 18 | data[key] = value 19 | save_metadata(guild_id, data) 20 | 21 | def clear_metadata(guild_id): 22 | path = get_metadata_file(guild_id) 23 | if os.path.exists(path): 24 | os.remove(path) -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Brief description of changes made. 4 | 5 | ## Type of Change 6 | 7 | - [ ] Bug fix (non-breaking change which fixes an issue) 8 | - [ ] New feature (non-breaking change which adds functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 10 | - [ ] Documentation update 11 | - [ ] Refactoring (no functional changes) 12 | 13 | ## Testing 14 | 15 | - [ ] I have tested this change locally 16 | - [ ] I have added tests that prove my fix is effective or that my feature works 17 | - [ ] New and existing unit tests pass locally with my changes 18 | 19 | ## Checklist 20 | 21 | - [ ] My code follows the style guidelines of this project 22 | - [ ] I have performed a self-review of my own code 23 | - [ ] I have commented my code, particularly in hard-to-understand areas 24 | - [ ] I have made corresponding changes to the documentation 25 | - [ ] My changes generate no new warnings 26 | 27 | ## Related Issues 28 | 29 | Closes #(issue) -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "gitcord" 7 | version = "0.6.0" 8 | description = "A Discord bot for GitOps based server structure" 9 | readme = "README.md" 10 | license = "GPL-3.0" 11 | license-files = ["LICENSE"] 12 | requires-python = ">=3.9" 13 | authors = [ 14 | {name = "Evelyn", email = "evolvewithevan@gmail.com"} 15 | ] 16 | dependencies = [ 17 | "discord.py>=2.5.2", 18 | "python-dotenv==1.0.0", 19 | "requests>=2.31.0", 20 | "beautifulsoup4>=4.12.0", 21 | "PyYAML>=6.0.2", 22 | "pylint>=3.3.7", 23 | "setuptools>=80.9.0", 24 | ] 25 | classifiers = [ 26 | "Development Status :: 3 - Alpha", 27 | "Intended Audience :: Developers", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python :: 3", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | ] 35 | 36 | [project.urls] 37 | Homepage = "https://github.com/evolvewithevan/gitcord" 38 | 39 | [project.scripts] 40 | gitcord = "gitcord.bot:run_bot" -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Getting Help 2 | 3 | ## Documentation 4 | 5 | Before asking for help, please check our documentation: 6 | 7 | - [README](../README.md) - Basic setup and usage 8 | - [Wiki Documentation](https://github.com/evolvewithevan/gitcord/wiki) 9 | - [API Reference](../docs/src/api-reference/) 10 | 11 | ## Community Support 12 | 13 | ### GitHub Issues 14 | 15 | For bugs, feature requests, or questions about the project: 16 | - [Open an issue](https://github.com/evolvewithevan/gitcord/issues/new) 17 | - Search existing issues first to avoid duplicates 18 | 19 | ### GitHub Discussions 20 | 21 | For general questions, ideas, or community discussion: 22 | - [Start a discussion](https://github.com/evolvewithevan/gitcord/discussions) 23 | 24 | ## Contact 25 | 26 | ### Project Maintainer 27 | - **Email**: evolvewithevan@gmail.com 28 | - **GitHub**: [@evolvewithevan](https://github.com/evolvewithevan) 29 | 30 | ## Response Times 31 | 32 | - **Issues**: We aim to respond within 48 hours 33 | - **Pull Requests**: We aim to review within 72 hours 34 | - **Security Issues**: We aim to respond within 24 hours 35 | 36 | ## Before You Ask 37 | 38 | Please include: 39 | - GitCord version 40 | - Python version 41 | - Operating system 42 | - Steps to reproduce (for bugs) 43 | - Expected vs actual behavior 44 | - Relevant logs or error messages -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](./introduction.md) 4 | 5 | # Getting Started 6 | 7 | - [Installation](./getting-started/installation.md) 8 | - [Quick Start](./getting-started/quick-start.md) 9 | - [Configuration](./getting-started/configuration.md) 10 | 11 | # User Guide 12 | 13 | - [Commands](./user-guide/commands.md) 14 | - [Channel Management](./user-guide/channel-management.md) 15 | - [Categories](./user-guide/categories.md) 16 | - [Permissions](./user-guide/permissions.md) 17 | 18 | # Developer Guide 19 | 20 | - [Architecture](./developer-guide/architecture.md) 21 | - [Cogs](./developer-guide/cogs.md) 22 | - [Events](./developer-guide/events.md) 23 | - [Utilities](./developer-guide/utilities.md) 24 | - [Contributing](./developer-guide/contributing.md) 25 | 26 | # API Reference 27 | 28 | - [Bot Class](./api-reference/bot.md) 29 | - [Cogs](./api-reference/cogs.md) 30 | - [Utils](./api-reference/utils.md) 31 | - [Constants](./api-reference/constants.md) 32 | 33 | # Templates 34 | 35 | - [Channel Templates](./templates/channel-templates.md) 36 | - [Category Templates](./templates/category-templates.md) 37 | - [Creating Custom Templates](./templates/custom-templates.md) 38 | 39 | # Troubleshooting 40 | 41 | - [Common Issues](./troubleshooting/common-issues.md) 42 | - [Error Messages](./troubleshooting/error-messages.md) 43 | - [Debug Mode](./troubleshooting/debug-mode.md) 44 | -------------------------------------------------------------------------------- /src/gitcord/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration module for GitCord bot. 3 | Handles environment variables and bot settings. 4 | """ 5 | 6 | import os 7 | from typing import Optional 8 | from dotenv import load_dotenv 9 | 10 | 11 | class Config: 12 | """Configuration class for GitCord bot settings.""" 13 | 14 | def __init__(self): 15 | """Initialize configuration by loading environment variables.""" 16 | load_dotenv() 17 | self._token: Optional[str] = None 18 | self._prefix: str = "!" 19 | self._activity_name: str = "!hello" 20 | 21 | @property 22 | def token(self) -> str: 23 | """Get Discord bot token from environment variables.""" 24 | if not self._token: 25 | self._token = os.getenv("DISCORD_TOKEN") 26 | if not self._token: 27 | raise ValueError("DISCORD_TOKEN not found in environment variables!") 28 | return self._token 29 | 30 | @property 31 | def prefix(self) -> str: 32 | """Get command prefix.""" 33 | return self._prefix 34 | 35 | @property 36 | def activity_name(self) -> str: 37 | """Get bot activity name.""" 38 | return self._activity_name 39 | 40 | @activity_name.setter 41 | def activity_name(self, value: str) -> None: 42 | """Set bot activity name.""" 43 | self._activity_name = value 44 | 45 | 46 | # Global configuration instance 47 | config = Config() 48 | -------------------------------------------------------------------------------- /src/gitcord/utils/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logging utilities for GitCord bot. 3 | """ 4 | 5 | import logging 6 | from typing import Optional 7 | 8 | 9 | def setup_logger(name: str = "gitcord", level: int = logging.INFO) -> logging.Logger: 10 | """ 11 | Set up a logger with proper formatting. 12 | 13 | Args: 14 | name: Logger name 15 | level: Logging level 16 | 17 | Returns: 18 | Configured logger instance 19 | """ 20 | logger = logging.getLogger(name) 21 | logger.setLevel(level) 22 | 23 | # Avoid adding handlers if they already exist 24 | if not logger.handlers: 25 | handler = logging.StreamHandler() 26 | formatter = logging.Formatter( 27 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 28 | ) 29 | handler.setFormatter(formatter) 30 | logger.addHandler(handler) 31 | 32 | return logger 33 | 34 | 35 | def log_error( 36 | log_instance: logging.Logger, error: Exception, context: Optional[str] = None 37 | ) -> None: 38 | """ 39 | Log an error with context. 40 | 41 | Args: 42 | log_instance: Logger instance 43 | error: Exception to log 44 | context: Optional context string 45 | """ 46 | message = f"Error: {error}" 47 | if context: 48 | message = f"{context} - {message}" 49 | log_instance.error(message, exc_info=True) # pylint: disable=line-too-long 50 | 51 | 52 | # Default logger instance 53 | main_logger = setup_logger() 54 | -------------------------------------------------------------------------------- /docs/src/user-guide/permissions.md: -------------------------------------------------------------------------------- 1 | # Permissions 2 | 3 | GitCord uses Discord's permission system. You need different permissions for different commands. 4 | 5 | ## Permission Levels 6 | 7 | ### No Permissions Needed 8 | Anyone can use these commands: 9 | - `!hello` - Say hello 10 | - `!ping` / `/slashping` - Check if bot works 11 | - `!help` / `/help` - Show help 12 | 13 | ### Manage Channels 14 | You need "Manage Channels" permission for: 15 | - `!createchannel` - Create channels 16 | - `!createcategory` / `/createcategory` - Create categories 17 | 18 | ### Administrator 19 | You need "Administrator" permission for: 20 | - `!fetchurl` / `/fetchurl` - Get text from websites 21 | - `!synccommands` / `/synccommands` - Update slash commands 22 | 23 | ## Setting Up Permissions 24 | 25 | ### For Server Admins 26 | 1. Create roles for different permission levels 27 | 2. Give "Manage Channels" to moderators 28 | 3. Give "Administrator" only to trusted admins 29 | 4. Test with different user accounts 30 | 31 | ### Recommended Roles 32 | ``` 33 | Server Owner (Administrator) 34 | ├── Bot Admin (Administrator) 35 | ├── Moderator (Manage Channels) 36 | └── Member (No special permissions) 37 | ``` 38 | 39 | ## Security Tips 40 | 41 | - Only give permissions that are needed 42 | - Regularly check who has what permissions 43 | - Remove permissions when people don't need them 44 | - Use roles instead of individual permissions 45 | 46 | ## Common Issues 47 | 48 | - **"Permission denied"**: You need the right permission 49 | - **"Bot can't do that"**: Bot needs the right permissions 50 | - **"Role hierarchy"**: Bot's role must be above roles it manages 51 | -------------------------------------------------------------------------------- /docs/src/user-guide/categories.md: -------------------------------------------------------------------------------- 1 | # Categories 2 | 3 | Categories group related channels together in Discord. GitCord lets you create categories using YAML files. 4 | 5 | ## What Are Categories? 6 | 7 | Categories are folders that hold channels. They help organize your server and make it look cleaner. 8 | 9 | ## Creating Categories 10 | 11 | ### Basic Category 12 | ```yaml 13 | name: Community 14 | position: 0 15 | type: category 16 | 17 | channels: 18 | - general 19 | - memes 20 | - off-topic 21 | ``` 22 | 23 | ### Required Fields 24 | - `name`: Category name 25 | - `position`: Where it appears (0 = top) 26 | - `type`: Must be "category" 27 | - `channels`: List of channel files (without .yaml) 28 | 29 | ## How to Use 30 | 31 | 1. Create a category YAML file 32 | 2. Create channel YAML files for each channel within the category 33 | 3. Run `!createcategory` or `/createcategory` 34 | 35 | ## Examples 36 | 37 | ### Community Category 38 | ```yaml 39 | # community.yaml 40 | name: Community 41 | position: 0 42 | type: category 43 | 44 | channels: 45 | - general 46 | - memes 47 | - off-topic 48 | ``` 49 | 50 | ### Voice Category 51 | ```yaml 52 | # voice.yaml 53 | name: Voice Chats 54 | position: 1 55 | type: category 56 | 57 | channels: 58 | - vc1 59 | - vc2 60 | ``` 61 | 62 | ## Channel Files 63 | 64 | Each channel listed in the category needs its own YAML file: 65 | 66 | ```yaml 67 | # general.yaml 68 | name: general 69 | type: text 70 | position: 0 71 | topic: "General chat for everyone" 72 | ``` 73 | 74 | ## Tips 75 | 76 | - Use clear, simple names 77 | - Position categories logically 78 | - Keep related channels together 79 | - Test your setup first 80 | -------------------------------------------------------------------------------- /src/gitcord/constants/messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | Message constants for GitCord bot. 3 | """ 4 | 5 | # Error messages 6 | ERR_PERMISSION_DENIED = "You do not have the required permission to use this command." 7 | ERR_FILE_NOT_FOUND = "YAML file not found at: {path}" 8 | ERR_INVALID_YAML = "Failed to parse YAML file: {error}" 9 | ERR_INVALID_CONFIG = "Missing required field: {field}" 10 | ERR_INVALID_CHANNEL_TYPE = ( 11 | "Channel type '{type}' is not supported. Use 'text' or 'voice'." 12 | ) 13 | ERR_DISCORD_API = "Discord API error: {error}" 14 | ERR_UNEXPECTED = "An unexpected error occurred: {error}" 15 | 16 | # Success messages 17 | SUCCESS_CATEGORY_CREATED = "Successfully created category: **{name}**" 18 | SUCCESS_CHANNEL_CREATED = "Successfully created channel: {mention}" 19 | SUCCESS_CATEGORY_UPDATED = "Successfully processed category: **{name}**" 20 | SUCCESS_COMMANDS_SYNCED = "Successfully synced **{count}** command(s) to this guild." 21 | 22 | # Help text content (for help command) 23 | HELP_DOCS = ( 24 | "For detailed documentation, tutorials, and guides, visit our " 25 | "[Wiki](https://github.com/evolvewithevan/gitcord/wiki)\n" 26 | "To see current issues, visit our " 27 | "[GitHub Issues](https://github.com/users/evolvewithevan/projects/4/views/1)" 28 | ) 29 | HELP_COMMANDS = ( 30 | "• `!hello` - Get a friendly greeting\n" 31 | "• `!help` - Show help information and link to the wiki (***YOU ARE HERE***)\n" 32 | "• `!ping` - Check bot latency\n" 33 | "• `!createchannel` - Create a channel from YAML (Manage Channels)\n" 34 | "• `!createcategory` - Create a category from YAML (Manage Channels)\n" 35 | "• `!synccommands` - Sync slash commands (Admin only)" 36 | ) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | title: "[Feature]: " 4 | labels: ["enhancement", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for suggesting a new feature! 10 | 11 | - type: textarea 12 | id: problem 13 | attributes: 14 | label: Is your feature request related to a problem? 15 | description: A clear and concise description of what the problem is. 16 | placeholder: I'm always frustrated when... 17 | validations: 18 | required: false 19 | 20 | - type: textarea 21 | id: solution 22 | attributes: 23 | label: Describe the solution you'd like 24 | description: A clear and concise description of what you want to happen. 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: alternatives 30 | attributes: 31 | label: Describe alternatives you've considered 32 | description: A clear and concise description of any alternative solutions or features you've considered. 33 | validations: 34 | required: false 35 | 36 | - type: textarea 37 | id: additional-context 38 | attributes: 39 | label: Additional context 40 | description: Add any other context or screenshots about the feature request here. 41 | validations: 42 | required: false 43 | 44 | - type: checkboxes 45 | id: terms 46 | attributes: 47 | label: Code of Conduct 48 | description: By submitting this issue, you agree to follow our Code of Conduct 49 | options: 50 | - label: I agree to follow this project's Code of Conduct 51 | required: true -------------------------------------------------------------------------------- /setup_env.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Environment setup script for GitCord bot. 4 | This script helps you create the necessary .env file with your Discord bot token. 5 | """ 6 | 7 | import os 8 | 9 | 10 | def create_env_file(): 11 | """Create .env file with user input.""" 12 | print("=== GitCord Bot Setup ===") 13 | print("This script will help you create the .env file with your Discord bot token.") 14 | print() 15 | 16 | # Check if .env already exists 17 | if os.path.exists('.env'): 18 | print("Warning: .env file already exists!") 19 | overwrite = input("Do you want to overwrite it? (y/N): ").lower().strip() 20 | if overwrite != 'y': 21 | print("Setup cancelled.") 22 | return 23 | 24 | # Get Discord token from user 25 | print("Please enter your Discord bot token:") 26 | print("(You can get this from https://discord.com/developers/applications)") 27 | print() 28 | 29 | token = input("Discord Bot Token: ").strip() 30 | 31 | if not token: 32 | print("Error: Token cannot be empty!") 33 | return 34 | 35 | # Create .env file 36 | try: 37 | with open('.env', 'w', encoding='utf-8') as f: 38 | f.write(f"DISCORD_TOKEN={token}\n") 39 | 40 | print() 41 | print("✅ .env file created successfully!") 42 | print("You can now run the bot with: python main.py") 43 | 44 | except OSError as e: 45 | print(f"Error creating .env file: {e}") 46 | except Exception as e: # pylint: disable=broad-except 47 | print(f"Unexpected error: {e}") 48 | 49 | 50 | def main(): 51 | """Main setup function.""" 52 | create_env_file() 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /docs/src/introduction.md: -------------------------------------------------------------------------------- 1 | # GitCord 2 | 3 | A powerful Discord bot for automated channel and category management using YAML templates. 4 | 5 | ## What is GitCord? 6 | 7 | GitCord is a Discord bot that simplifies server management by allowing you to create and manage channels and categories using YAML configuration files. It provides a clean, template-based approach to Discord server organization. 8 | 9 | ## Key Features 10 | 11 | - **Template-based Channel Creation**: Use YAML files to define channel and category structures 12 | - **Automated Management**: Bulk create channels and categories with a single command 13 | - **Flexible Configuration**: Customize channel properties, permissions, and organization 14 | - **Easy Setup**: Simple installation and configuration process 15 | - **Extensible**: Modular cog system for easy customization and extension 16 | 17 | ## Quick Overview 18 | 19 | GitCord allows you to: 20 | 21 | - Create multiple channels and categories from YAML templates 22 | - Manage channel permissions and settings 23 | - Organize your Discord server efficiently 24 | - Automate repetitive server setup tasks 25 | - Maintain consistent channel structures across multiple servers 26 | 27 | ## Getting Started 28 | 29 | Ready to get started? Check out the [Installation Guide](./getting-started/installation.md) to set up GitCord on your Discord server. 30 | 31 | ## Support 32 | 33 | - **GitHub**: [GitCord Repository](https://github.com/evolvewithevan/gitcord) 34 | - **Issues**: Report bugs and request features on GitHub 35 | - **Documentation**: This comprehensive guide covers all aspects of GitCord 36 | 37 | ## License 38 | 39 | GitCord is open source software. See the [LICENSE](../LICENSE) file for details. 40 | 41 | --- 42 | 43 |

44 | Made with ❤️ by the GitCord Team 45 |

⚠️ **Note**: GitCord is in **early development**. Many features are experimental or incomplete. Use only in controlled environments until a stable release is announced. 6 | 7 | --- 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you discover a security vulnerability in GitCord, we strongly encourage responsible disclosure. Please follow the process below: 12 | 13 | ### 🔒 Private Disclosure Process 14 | 15 | - Email the core maintainer directly at **security@allthingslinux.org** 16 | - Do **not** open a public issue. This helps us address vulnerabilities before they're exploited. 17 | - Include: 18 | - A detailed description of the issue 19 | - Steps to reproduce 20 | - The potential impact 21 | - Suggested fixes or mitigation (if any) 22 | 23 | We aim to acknowledge all reports within **72 hours** and resolve verified issues within **7–14 days**, depending on severity. 24 | 25 | ### 🛡 What Happens Next 26 | 27 | - The team will validate the vulnerability. 28 | - If accepted, we’ll prioritize a fix and publish an update. 29 | - Security reporters will be credited in release notes unless anonymity is requested. 30 | - Low-risk or non-critical issues may be queued for a later release. 31 | 32 | --- 33 | 34 | ## Known Insecure Areas 35 | 36 | As of now, the following parts of GitCord are **not secure** and should not be used in production: 37 | - `!fetchurl`: Accepts unvalidated URLs and renders raw HTML 38 | - Commands accessible to any server member 39 | - Lack of permission validation and input sanitization 40 | 41 | Until a secure permission and validation system is implemented, **do not deploy GitCord in public or untrusted servers**. 42 | 43 | When a secure, production-ready built is ready, a large notice will be present in the repo's README 44 | 45 | --- 46 | 47 | Thank you for keeping GitCord safe! 48 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable 6 | # packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 10 | name: 'Dependency review' 11 | on: 12 | pull_request: 13 | branches: [ "main" ] 14 | 15 | # If using a dependency submission action in this workflow this permission will need to be set to: 16 | # 17 | # permissions: 18 | # contents: write 19 | # 20 | # https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api 21 | permissions: 22 | contents: read 23 | # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option 24 | pull-requests: write 25 | 26 | jobs: 27 | dependency-review: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: 'Checkout repository' 31 | uses: actions/checkout@v4 32 | - name: 'Dependency Review' 33 | uses: actions/dependency-review-action@v4 34 | # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. 35 | with: 36 | comment-summary-in-pr: always 37 | # fail-on-severity: moderate 38 | # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later 39 | # retry-on-snapshot-warnings: true 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: What happened? 15 | description: Also tell us, what did you expect to happen? 16 | placeholder: Tell us what you see! 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: reproduce 22 | attributes: 23 | label: Steps to reproduce 24 | description: How can we reproduce this issue? 25 | placeholder: | 26 | 1. Go to '...' 27 | 2. Click on '....' 28 | 3. Scroll down to '....' 29 | 4. See error 30 | validations: 31 | required: true 32 | 33 | - type: dropdown 34 | id: version 35 | attributes: 36 | label: Version 37 | description: What version of GitCord are you running? 38 | options: 39 | - main (latest) 40 | - v0.1.0 41 | - Other (please specify) 42 | validations: 43 | required: true 44 | 45 | - type: input 46 | id: python-version 47 | attributes: 48 | label: Python Version 49 | placeholder: e.g. 3.9.0 50 | validations: 51 | required: true 52 | 53 | - type: dropdown 54 | id: os 55 | attributes: 56 | label: What operating system are you using? 57 | options: 58 | - Linux 59 | - macOS 60 | - Windows 61 | - Other 62 | validations: 63 | required: true 64 | 65 | - type: textarea 66 | id: logs 67 | attributes: 68 | label: Relevant log output 69 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 70 | render: shell -------------------------------------------------------------------------------- /.github/workflows/mdbook.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a mdBook site to GitHub Pages 2 | # 3 | # To get started with mdBook see: https://rust-lang.github.io/mdBook/index.html 4 | # 5 | name: Deploy mdBook site to Pages 6 | 7 | 8 | # Runs on pushes targeting the default branch 9 | on: 10 | push: 11 | branches: ["main"] 12 | paths: 13 | - 'docs/**' 14 | - 'docs/book.toml' 15 | workflow_dispatch: 16 | 17 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 18 | permissions: 19 | contents: read 20 | pages: write 21 | id-token: write 22 | 23 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 24 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 25 | concurrency: 26 | group: "pages" 27 | cancel-in-progress: false 28 | 29 | jobs: 30 | # Build job 31 | build: 32 | runs-on: ubuntu-latest 33 | env: 34 | MDBOOK_VERSION: 0.4.36 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Install mdBook 38 | run: | 39 | curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh 40 | rustup update 41 | cargo install --version ${MDBOOK_VERSION} mdbook 42 | - name: Setup Pages 43 | id: pages 44 | uses: actions/configure-pages@v5 45 | - name: Build with mdBook 46 | run: | 47 | cd docs 48 | mdbook build 49 | - name: Upload artifact 50 | uses: actions/upload-pages-artifact@v3 51 | with: 52 | path: ./docs/book 53 | 54 | # Deployment job 55 | deploy: 56 | environment: 57 | name: github-pages 58 | url: ${{ steps.deployment.outputs.page_url }} 59 | runs-on: ubuntu-latest 60 | needs: build 61 | steps: 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | -------------------------------------------------------------------------------- /src/gitcord/cogs/utility.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic utility commands cog for GitCord bot. 3 | Contains simple utility commands like greetings and latency checks. 4 | """ 5 | 6 | import discord 7 | from discord import app_commands 8 | from discord.ext import commands 9 | 10 | from .base_cog import BaseCog 11 | from ..utils.helpers import create_embed, format_latency 12 | 13 | 14 | class Utility(BaseCog): 15 | """Basic utility commands.""" 16 | 17 | def __init__(self, bot: commands.Bot): 18 | """Initialize the Utility cog.""" 19 | super().__init__(bot) 20 | self.logger.info("Utility cog loaded") 21 | 22 | @commands.command(name="hello") 23 | async def hello(self, ctx: commands.Context) -> None: 24 | """Simple hello world command.""" 25 | embed = create_embed( 26 | title="👋 Welcome!", 27 | description=f"Hello, {ctx.author.mention}! Welcome to GitCord!" 28 | ) 29 | await ctx.send(embed=embed) 30 | 31 | @commands.command(name="ping") 32 | async def ping_prefix(self, ctx: commands.Context) -> None: 33 | """Check bot latency.""" 34 | latency = format_latency(self.bot.latency) 35 | embed = create_embed( 36 | title="🏓 Pong!", 37 | description=f"Latency: **{latency}**", 38 | color=discord.Color.green(), 39 | ) 40 | await ctx.send(embed=embed) 41 | 42 | @app_commands.command(name="slashping", description="Check bot latency (slash)") 43 | async def slashping(self, interaction: discord.Interaction) -> None: 44 | """Slash command to check bot latency.""" 45 | latency = format_latency(self.bot.latency) 46 | embed = create_embed( 47 | title="🏓 Pong!", 48 | description=f"Latency: **{latency}**", 49 | color=discord.Color.green(), 50 | ) 51 | await interaction.response.send_message(embed=embed) 52 | 53 | 54 | async def setup(bot: commands.Bot) -> None: 55 | """Set up the Utility cog.""" 56 | await bot.add_cog(Utility(bot)) 57 | -------------------------------------------------------------------------------- /docs/src/user-guide/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | GitCord has commands for managing your Discord server. You can use `!` commands or `/` slash commands. 4 | 5 | ## Basic Commands 6 | 7 | ### `!hello` 8 | Say hello to the bot. 9 | 10 | **Usage:** `!hello` 11 | 12 | **Permissions:** None needed 13 | 14 | ### `!ping` / `/slashping` 15 | Check if the bot is working. 16 | 17 | **Usage:** 18 | - `!ping` 19 | - `/slashping` 20 | 21 | **Permissions:** None needed 22 | 23 | ## Channel Commands 24 | 25 | ### `!createchannel` 26 | Create one channel from a YAML file. 27 | 28 | **Usage:** `!createchannel` 29 | 30 | **Permissions:** Manage Channels 31 | 32 | **Note:** Now uses dynamic template paths. For servers using the new monolithic template format, this command shows a deprecation warning and suggests using `!git pull` instead. 33 | 34 | ### `!createcategory` / `/createcategory` 35 | Create a category with multiple channels. 36 | 37 | **Usage:** 38 | - `!createcategory` 39 | - `/createcategory [yaml_path]` 40 | 41 | **Permissions:** Manage Channels 42 | 43 | **Note:** `!createcategory` uses a fixed path by default, This will be changed with the release of 1.X. `/createcategory` lets you specify a path. 44 | 45 | ## Admin Commands 46 | 47 | ### `!fetchurl` / `/fetchurl` 48 | Get text from a website. 49 | 50 | **Usage:** 51 | - `!fetchurl ` 52 | - `/fetchurl ` 53 | 54 | **Permissions:** Administrator 55 | 56 | ### `!synccommands` / `/synccommands` 57 | Update slash commands. 58 | 59 | **Usage:** 60 | - `!synccommands` 61 | - `/synccommands` 62 | 63 | **Permissions:** Administrator 64 | 65 | ## Help Commands 66 | 67 | ### `!help` / `/help` 68 | Show help and links. 69 | 70 | **Usage:** 71 | - `!help` 72 | - `/help` 73 | 74 | **Permissions:** None needed 75 | 76 | ## Permissions 77 | 78 | - **None needed:** Anyone can use 79 | - **Manage Channels:** Can create/edit channels 80 | - **Administrator:** Can use admin commands 81 | 82 | ## Common Errors 83 | 84 | - **"Command not found"**: Check spelling or sync commands 85 | - **"Permission denied"**: You need the right permissions 86 | - **"File not found"**: Check your YAML file path 87 | -------------------------------------------------------------------------------- /docs/src/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Set up GitCord on your Discord server. 4 | 5 | ## What You Need 6 | 7 | - Python 3.8 or higher 8 | - A Discord bot token 9 | - Discord server with admin permissions 10 | 11 | ## Step 1: Get the Code 12 | 13 | ```bash 14 | git clone https://github.com/evolvewithevan/gitcord.git 15 | cd gitcord 16 | ``` 17 | 18 | ## Step 2: Install Dependencies 19 | 20 | UV is required 21 | 22 | ### Using uv (Recommended) 23 | 24 | ```bash 25 | uv sync 26 | ``` 27 | 28 | ## Step 3: Set Up Environment 29 | 30 | 1. Copy the example environment file: 31 | ```bash 32 | cp .env.example .env 33 | ``` 34 | 35 | 2. Populate the `.env` file with your bot token: 36 | ```env 37 | DISCORD_TOKEN=your_bot_token_here 38 | ``` 39 | 40 | ## Step 4: Create Discord Bot 41 | 42 | 1. Go to [Discord Developer Portal](https://discord.com/developers/applications) 43 | 2. Click "New Application" and name it 44 | 3. Go to "Bot" section and click "Add Bot" 45 | 4. Copy the bot token and add it to your `.env` file 46 | 5. Enable the following bot permissions: 47 | - Manage Channels 48 | - Manage Roles (Technically not needed until Role Management is implemented) 49 | - Send Messages 50 | - Embed Links 51 | - Use Slash Commands 52 | 53 | ## Step 5: Invite Bot to Server 54 | 55 | Use this URL (replace `YOUR_CLIENT_ID` with your bot's client ID): 56 | 57 | ``` 58 | https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=8&scope=bot%20applications.commands 59 | ``` 60 | 61 | ## Step 6: Run the Bot 62 | 63 | ```bash 64 | python -m gitcord 65 | ``` 66 | 67 | ## Verification 68 | 69 | Once the bot is running, you should see: 70 | - A startup message in the console 71 | - The bot appears online in your Discord server 72 | - Slash commands are available 73 | 74 | ## Next Steps 75 | 76 | - Check out the [Quick Start Guide](./quick-start.md) to create your first channels 77 | - Learn about [Configuration](./configuration.md) options 78 | - Explore available [Commands](../user-guide/commands.md) 79 | 80 | ## Troubleshooting 81 | 82 | If you encounter issues during installation: 83 | 84 | - Ensure Python 3.8+ is installed: `python --version` 85 | - Verify all dependencies are installed: `pip list` 86 | - Check your bot token is correct 87 | - Ensure the bot has proper permissions in your server 88 | 89 | For more help, see the [Troubleshooting](../troubleshooting/common-issues.md) section. -------------------------------------------------------------------------------- /docs/src/getting-started/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Set up GitCord for your needs. 4 | 5 | ## Environment Variables 6 | 7 | Create a `.env` file in the project root: 8 | 9 | ```env 10 | # Required: Your Discord bot token 11 | DISCORD_TOKEN=your_bot_token_here 12 | 13 | # Optional: Bot prefix for commands (default: !) 14 | PREFIX=! 15 | ``` 16 | 17 | ## Bot Permissions 18 | 19 | Your bot needs these permissions: 20 | 21 | - **Manage Channels**: Create, edit, and delete channels 22 | - **Send Messages**: Send responses to commands 23 | - **Embed Links**: Send rich embeds 24 | - **Use Slash Commands**: Register and use slash commands 25 | 26 | ## Discord Intents 27 | 28 | The bot needs these Discord intents: 29 | 30 | - **Guilds**: Access server information 31 | - **Guild Messages**: Read and send messages 32 | - **Message Content**: Read message content for prefix commands 33 | 34 | ## YAML Templates 35 | 36 | Templates use YAML format: 37 | 38 | ```yaml 39 | # Single channel 40 | name: channel-name 41 | type: text|voice|category 42 | topic: Optional channel topic 43 | position: Optional position number 44 | nsfw: Optional boolean for NSFW channels 45 | 46 | # Category with channels 47 | name: category-name 48 | type: category 49 | channels: 50 | - channel-1 51 | - channel-2 52 | ``` 53 | 54 | ## Template Locations 55 | 56 | Templates are available exclusively at: 57 | 58 | - [gitcord-template GitHub Repository](https://github.com/evolvewithevan/gitcord-template) 59 | 60 | ## Logging 61 | 62 | GitCord uses Python's built-in logging. Logs go to the console by default. 63 | 64 | **Note:** Database configuration and advanced options are not currently available. 65 | 66 | ## Security 67 | 68 | - Keep your bot token secure 69 | - Never commit tokens to version control 70 | - Use environment variables for sensitive data 71 | - Regularly rotate your bot token 72 | - Give bot only necessary permissions 73 | 74 | ## Problems? 75 | 76 | ### Common Issues 77 | 78 | 1. **Bot not responding**: Check token and permissions 79 | 2. **Commands not working**: Verify slash commands are registered 80 | 3. **Template errors**: Check YAML syntax 81 | 4. **Permission errors**: Ensure bot has required permissions 82 | 83 | ### Debug Mode 84 | 85 | Enable debug mode for detailed logging: 86 | 87 | ```env 88 | DEBUG=true 89 | ``` 90 | 91 | ## Next Steps 92 | 93 | - Learn about [Available Commands](../user-guide/commands.md) 94 | - Explore [Template Creation](../templates/custom-templates.md) 95 | - Check [Troubleshooting](../troubleshooting/common-issues.md) for help -------------------------------------------------------------------------------- /docs/src/getting-started/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | Get GitCord working in minutes! 4 | 5 | > **Note:** These instructions are for the current version of GitCord. Once GitHub integration is implemented, the setup and usage steps will change dramatically. Please check back for updated instructions in future releases. 6 | 7 | 8 | ## What You Need 9 | 10 | - GitCord bot running (see [Installation](./installation.md)) 11 | - Bot has permissions in your Discord server 12 | - You have admin permissions 13 | 14 | ## Step 1: Create a Channel 15 | 16 | Create a file called `general.yaml`: 17 | 18 | ```yaml 19 | name: general 20 | type: text 21 | topic: General discussion for the server 22 | position: 0 23 | nsfw: false 24 | ``` 25 | 26 | ## Step 2: Create the Channel 27 | 28 | In your Discord server, use: 29 | 30 | ``` 31 | !createchannel 32 | ``` 33 | 34 | **Note:** This command uses a fixed file path. You'll need to put your file in the right place. 35 | 36 | ## Step 3: Create Multiple Channels 37 | 38 | Create a category file: 39 | 40 | ```yaml 41 | # category.yaml 42 | name: community 43 | type: category 44 | position: 0 45 | 46 | channels: 47 | - general 48 | - announcements 49 | - voice-chat 50 | ``` 51 | 52 | Then create channel files for each channel listed. 53 | 54 | ## Step 4: Create the Category 55 | 56 | Use: 57 | 58 | ``` 59 | !createcategory 60 | ``` 61 | 62 | Or with a specific path: 63 | 64 | ``` 65 | /createcategory path/to/category.yaml 66 | ``` 67 | 68 | ## Step 5: Try Basic Commands 69 | 70 | ``` 71 | !hello # Get a greeting 72 | !ping # Check bot latency 73 | !help # Show help information 74 | ``` 75 | 76 | ## Example Templates 77 | 78 | ### Community Category 79 | ```yaml 80 | # category.yaml 81 | name: community 82 | type: category 83 | position: 0 84 | 85 | channels: 86 | - general 87 | - announcements 88 | - memes 89 | - off-topic 90 | ``` 91 | 92 | ### Voice Category 93 | ```yaml 94 | # category.yaml 95 | name: voice-chats 96 | type: category 97 | position: 1 98 | 99 | channels: 100 | - vc1 101 | - vc2 102 | ``` 103 | 104 | ## Next Steps 105 | 106 | - Learn about [Channel Management](../user-guide/channel-management.md) 107 | - Explore [Categories](../user-guide/categories.md) 108 | - Check [Available Commands](../user-guide/commands.md) 109 | - Read about [Permissions](../user-guide/permissions.md) 110 | 111 | ## Tips 112 | 113 | - Use clear, simple names 114 | - Test your YAML files first 115 | - Keep backups of your files 116 | - Use the `!help` command for more options -------------------------------------------------------------------------------- /src/gitcord/cogs/base_cog.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base cog class with common functionality. 3 | """ 4 | 5 | import logging 6 | 7 | import discord 8 | from discord.ext import commands 9 | 10 | from ..utils.helpers import ( 11 | create_error_embed, 12 | create_success_embed, 13 | handle_command_error, 14 | handle_interaction_error, 15 | ) 16 | 17 | 18 | class BaseCog(commands.Cog): 19 | """Base cog class with common functionality.""" 20 | 21 | def __init__(self, bot: commands.Bot): 22 | self.bot = bot 23 | self.logger = logging.getLogger(f"gitcord.{self.__class__.__name__.lower()}") 24 | 25 | async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: 26 | """Handle errors for all commands in this cog.""" 27 | if isinstance(error, commands.CommandError): 28 | await handle_command_error(ctx, error, self.logger) 29 | else: 30 | # Handle non-CommandError exceptions 31 | await self.send_error( 32 | ctx, "❌ Error", f"An unexpected error occurred: {str(error)}" 33 | ) 34 | 35 | async def cog_app_command_error( 36 | self, 37 | interaction: discord.Interaction, 38 | error: discord.app_commands.AppCommandError, 39 | ) -> None: 40 | """Handle errors for all app commands in this cog.""" 41 | await handle_interaction_error(interaction, error, self.logger) 42 | 43 | def create_error_embed(self, title: str, description: str) -> discord.Embed: 44 | """Create a standardized error embed.""" 45 | return create_error_embed(title, description) 46 | 47 | def create_success_embed(self, title: str, description: str) -> discord.Embed: 48 | """Create a standardized success embed.""" 49 | return create_success_embed(title, description) 50 | 51 | async def send_error( 52 | self, ctx: commands.Context, title: str, description: str 53 | ) -> None: 54 | """Send an error embed.""" 55 | embed = self.create_error_embed(title, description) 56 | await ctx.send(embed=embed) 57 | 58 | async def send_success( 59 | self, ctx: commands.Context, title: str, description: str 60 | ) -> None: 61 | """Send a success embed.""" 62 | embed = self.create_success_embed(title, description) 63 | await ctx.send(embed=embed) 64 | 65 | async def send_interaction_error( 66 | self, interaction: discord.Interaction, title: str, description: str 67 | ) -> None: 68 | """Send an error embed via interaction.""" 69 | embed = self.create_error_embed(title, description) 70 | await interaction.followup.send(embed=embed) 71 | 72 | async def send_interaction_success( 73 | self, interaction: discord.Interaction, title: str, description: str 74 | ) -> None: 75 | """Send a success embed via interaction.""" 76 | embed = self.create_success_embed(title, description) 77 | await interaction.followup.send(embed=embed) 78 | -------------------------------------------------------------------------------- /src/gitcord/bot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main GitCord bot class and entry point. 3 | """ 4 | 5 | import asyncio 6 | 7 | import discord 8 | from discord.ext import commands 9 | 10 | from .config import config 11 | from .events import setup_events 12 | from .utils.logger import main_logger as logger 13 | 14 | 15 | class GitCordBot(commands.Bot): 16 | """Main GitCord bot class.""" 17 | 18 | def __init__(self): 19 | """Initialize the GitCord bot.""" 20 | intents = discord.Intents.default() 21 | intents.message_content = True 22 | 23 | super().__init__( 24 | command_prefix=config.prefix, 25 | intents=intents, 26 | help_command=None, # We can implement a custom help command later 27 | ) 28 | 29 | # Set up event handlers 30 | self.event_handler = setup_events(self) 31 | 32 | logger.info("GitCord bot initialized") 33 | 34 | async def setup_hook(self) -> None: 35 | """Setup hook to register slash commands and load cogs.""" 36 | logger.info("Setting up bot...") 37 | 38 | # Load cogs 39 | await self._load_cogs() 40 | 41 | # Command syncing is now done manually using the `/synccommands` slash command. 42 | 43 | logger.info("Bot setup completed") 44 | 45 | async def _load_cogs(self) -> None: 46 | """Load all bot cogs.""" 47 | # Load the modularized cogs 48 | await self.load_extension("gitcord.cogs.admin") 49 | await self.load_extension("gitcord.cogs.channels") 50 | await self.load_extension("gitcord.cogs.utility") 51 | await self.load_extension("gitcord.cogs.help") 52 | logger.info("Loaded all modularized cogs") 53 | 54 | # Add more cogs here as they are created 55 | # await self.load_extension("gitcord.cogs.git") 56 | 57 | async def on_command_error( 58 | self, context, error 59 | ): # pylint: disable=arguments-differ 60 | """Global command error handler.""" 61 | logger.error("Global command error: %s", error) 62 | if isinstance(error, commands.CommandNotFound): 63 | await context.send("Command not found. Try `!help` or `!ping`!") 64 | elif isinstance(error, commands.MissingPermissions): 65 | await context.send("You don't have permission to use this command!") 66 | elif isinstance(error, commands.CommandError): 67 | await context.send(f"A command error occurred: {error}") 68 | else: 69 | raise error 70 | 71 | 72 | async def main() -> None: 73 | """Main function to run the bot.""" 74 | try: 75 | # Create and run the bot 76 | bot = GitCordBot() 77 | logger.info("Starting GitCord bot...") 78 | await bot.start(config.token) 79 | 80 | except discord.LoginFailure: 81 | logger.error( 82 | "Invalid Discord token! Please check your DISCORD_TOKEN in the .env file." 83 | ) 84 | except ValueError as e: 85 | logger.error("Configuration error: %s", e) 86 | 87 | 88 | def run_bot() -> None: 89 | """Entry point to run the bot.""" 90 | asyncio.run(main()) 91 | 92 | 93 | if __name__ == "__main__": 94 | run_bot() 95 | -------------------------------------------------------------------------------- /docs/src/troubleshooting/common-issues.md: -------------------------------------------------------------------------------- 1 | # Common Issues 2 | 3 | Fix common GitCord problems. 4 | 5 | ## Bot Connection Issues 6 | 7 | ### Bot Not Responding 8 | 9 | **Problem:** Bot is online but doesn't respond to commands. 10 | 11 | **Fix:** 12 | 1. Check bot permissions in server settings 13 | 2. Try `!ping` to test basic functionality 14 | 3. Run `/synccommands` to update slash commands 15 | 4. Check your bot token is correct 16 | 17 | ### Bot Won't Start 18 | 19 | **Problem:** Bot fails to start with login errors. 20 | 21 | **Fix:** 22 | 1. Check your bot token in `.env` file 23 | 2. Make sure no extra spaces in token 24 | 3. Regenerate token if needed 25 | 4. Check your internet connection 26 | 27 | ## Channel Issues 28 | 29 | ### Channel Creation Fails 30 | 31 | **Problem:** `!createchannel` or `/createcategory` commands fail. 32 | 33 | **Fix:** 34 | 1. Check YAML file exists and path is correct 35 | 2. Validate YAML syntax (use a validator) 36 | 3. Make sure you have "Manage Channels" permission 37 | 4. Check all required fields are present 38 | 39 | ### Channel Type Errors 40 | 41 | **Problem:** "Channel type 'xyz' is not supported" 42 | 43 | **Fix:** 44 | - Use only "text", "voice", or "category" 45 | - Check spelling in your YAML file 46 | 47 | ## Permission Issues 48 | 49 | ### Permission Denied 50 | 51 | **Problem:** "You don't have permission to use this command!" 52 | 53 | **Fix:** 54 | - Check your role permissions in server 55 | - Make sure you have the right permission level 56 | - Contact a server administrator 57 | 58 | ### Bot Permission Issues 59 | 60 | **Problem:** Bot can't perform actions 61 | 62 | **Fix:** 63 | - Check bot's role permissions in server settings 64 | - Make sure bot's role is above roles it manages 65 | - Give bot "Manage Channels" permission 66 | 67 | ## YAML Issues 68 | 69 | ### Invalid YAML 70 | 71 | **Problem:** "Failed to parse YAML file" 72 | 73 | **Fix:** 74 | - Use a YAML validator to check syntax 75 | - Check indentation (use spaces, not tabs) 76 | - Make sure all quotes match 77 | - Verify all required fields are present 78 | 79 | ### Missing Fields 80 | 81 | **Problem:** "Missing required field" 82 | 83 | **Fix:** 84 | - Check your YAML has all required fields 85 | - Required: `name`, `type`, `position` 86 | - Optional: `topic`, `nsfw`, `user_limit` 87 | 88 | ## Network Issues 89 | 90 | ### URL Fetching Fails 91 | 92 | **Problem:** `!fetchurl` command fails 93 | 94 | **Fix:** 95 | - Check the URL is valid and accessible 96 | - Make sure URL starts with http:// or https:// 97 | - Try a different URL to test 98 | - Check your internet connection 99 | 100 | ## Performance Issues 101 | 102 | ### Bot is Slow 103 | 104 | **Problem:** Commands take a long time to respond 105 | 106 | **Fix:** 107 | - Check your internet connection 108 | - Restart the bot 109 | - Check Discord's status 110 | - Reduce complexity of YAML files 111 | 112 | ## Getting Help 113 | 114 | If you still have problems: 115 | 116 | 1. Check the [Error Messages](./error-messages.md) page 117 | 2. Enable [Debug Mode](./debug-mode.md) for more info 118 | 3. Check the bot logs for error details 119 | 4. Ask for help in the project's GitHub issues 120 | -------------------------------------------------------------------------------- /BOT_SETUP.md: -------------------------------------------------------------------------------- 1 | # GitCord Bot Setup Guide 2 | 3 | This guide will help you set up the GitCord Discord bot on your server. 4 | 5 | ## Prerequisites 6 | 7 | 1. **Python 3.9 or higher** installed on your system 8 | 2. **A Discord bot token** (see [Creating a Discord Bot](#creating-a-discord-bot) below) 9 | 10 | ## Quick Setup 11 | 12 | ### 1. Install uv 13 | 14 | ```bash 15 | $ pip install -r requirements.txt 16 | ``` 17 | 18 | ### 2. Install the project 19 | 20 | ```bash 21 | $ uv build 22 | $ pip install dist/*.whl 23 | ``` 24 | 25 | ### 3. Set Up Environment Variables 26 | 27 | You have two options: 28 | 29 | #### Option A: Use the Setup Script (Recommended) 30 | ```bash 31 | python setup-env.py 32 | ``` 33 | This will guide you through creating the `.env` file with your bot token. 34 | 35 | #### Option B: Manual Setup 36 | Clone the `.env.example` file in the project root and remove `.example` from the extension. Then populate relevant fields: 37 | ``` 38 | DISCORD_TOKEN=your_discord_bot_token_here 39 | ``` 40 | 41 | ### 3. Run the Bot 42 | 43 | ```bash 44 | python3 -m gitcord 45 | ``` 46 | 47 | If everything is set up correctly, you should see: 48 | ``` 49 | Starting GitCord bot... 50 | [Bot Name] has connected to Discord! 51 | Bot is in X guild(s) 52 | ``` 53 | 54 | ## Creating a Discord Bot 55 | 56 | If you don't have a Discord bot yet, follow these steps: 57 | 58 | 1. Go to [Discord Developer Portal](https://discord.com/developers/applications) 59 | 2. Click "New Application" and give it a name 60 | 3. Go to the "Bot" section in the left sidebar 61 | 4. Click "Add Bot" 62 | 5. Under the "Token" section, click "Copy" to copy your bot token. - NOTE, You will only see this token once. 63 | 6. Save this token securely - you'll need it for the `.env` file 64 | 65 | ### Bot Permissions 66 | 67 | Make sure your bot has these permissions: 68 | - Send Messages 69 | - Read Message History 70 | - Use Slash Commands (if you plan to use them later) 71 | 72 | ## Bot Commands 73 | 74 | Once the bot is running, you can use these commands in your Discord server: 75 | 76 | - `!hello` - Get a friendly greeting from the bot 77 | - `!ping` - Check the bot's latency 78 | 79 | ## Troubleshooting 80 | 81 | ### "DISCORD_TOKEN not found in environment variables" 82 | - Make sure you have a `.env` file in the project root 83 | - Check that the file contains `DISCORD_TOKEN=your_token_here` 84 | - Ensure there are no extra spaces or quotes around the token 85 | 86 | ### "Invalid Discord token" 87 | - Double-check your bot token from the Discord Developer Portal 88 | - Make sure you copied the entire token correctly 89 | - Verify the bot is still active in the Developer Portal 90 | 91 | ### Bot not responding to commands 92 | - Ensure the bot has been invited to your server with proper permissions 93 | - Check that the bot is online (green status) 94 | - Verify you're using the correct command prefix (`!`) 95 | 96 | ## Next Steps 97 | 98 | This is a basic hello world bot. For the full GitCord functionality (GitHub integration, server configuration management), check the main [README.md](README.md) for development status and roadmap. 99 | 100 | ## Security Notes 101 | 102 | - Never share your bot token publicly 103 | - The `.env` file is already in `.gitignore` to prevent accidental commits 104 | - If your token is ever compromised, regenerate it in the Discord Developer Portal -------------------------------------------------------------------------------- /src/gitcord/events.py: -------------------------------------------------------------------------------- 1 | """ 2 | Event handlers for GitCord bot. 3 | Handles Discord events like on_ready and command syncing. 4 | """ 5 | 6 | import discord 7 | from discord.ext import commands 8 | 9 | from .utils.logger import main_logger as logger 10 | from .config import config 11 | 12 | 13 | class EventHandler: # pylint: disable=too-few-public-methods 14 | """Handles Discord bot events.""" 15 | 16 | def __init__(self, bot: commands.Bot): 17 | """Initialize the event handler.""" 18 | self.bot = bot 19 | 20 | async def on_ready(self) -> None: 21 | """Event triggered when the bot is ready and connected to Discord.""" 22 | logger.info("%s has connected to Discord!", self.bot.user) 23 | logger.info("Bot is in %d guild(s)", len(self.bot.guilds)) 24 | 25 | # Set bot status 26 | await self.bot.change_presence(activity=discord.Game(name=config.activity_name)) 27 | 28 | # Send restart message to guilds 29 | await self._send_restart_messages() 30 | 31 | # Slash commands are no longer automatically synced here. 32 | # Use the `/synccommands` command to sync manually if needed. 33 | 34 | async def _send_restart_messages(self) -> None: 35 | """Send restart messages to available text channels.""" 36 | for guild in self.bot.guilds: 37 | logger.info("Connected to guild: %s (ID: %s)", guild.name, guild.id) 38 | 39 | for channel in guild.text_channels: 40 | if channel.permissions_for(guild.me).send_messages: 41 | try: 42 | await channel.send("Bot has restarted successfully!") 43 | logger.info( 44 | "Sent restart message to %s in %s", channel.name, guild.name 45 | ) 46 | except discord.DiscordException as e: 47 | logger.error( 48 | "Failed to send message to %s in %s: %s", 49 | channel.name, 50 | guild.name, 51 | e, 52 | ) 53 | except Exception as e: # pylint: disable=broad-except 54 | logger.error( 55 | "Failed to send message to %s in %s: %s", 56 | channel.name, 57 | guild.name, 58 | e, 59 | ) 60 | break # Only send to the first available channel 61 | 62 | async def _sync_commands(self) -> None: 63 | """Sync slash commands to all guilds and globally.""" 64 | try: 65 | logger.info("Syncing slash commands...") 66 | 67 | # Sync to all guilds the bot is in 68 | for guild in self.bot.guilds: 69 | logger.info("Syncing commands to guild: %s", guild.name) 70 | synced = await self.bot.tree.sync(guild=guild) 71 | logger.info("Synced %d command(s) to %s", len(synced), guild.name) 72 | 73 | # Also sync globally (takes up to 1 hour to propagate) 74 | synced_global = await self.bot.tree.sync() 75 | logger.info("Synced %d command(s) globally", len(synced_global)) 76 | 77 | except discord.DiscordException as e: 78 | logger.error("Failed to sync commands: %s", e) 79 | except Exception as e: # pylint: disable=broad-except 80 | logger.error("Failed to sync commands: %s", e) 81 | 82 | 83 | def setup_events(bot: commands.Bot) -> EventHandler: 84 | """ 85 | Set up event handlers for the bot. 86 | 87 | Args: 88 | bot: The Discord bot instance 89 | 90 | Returns: 91 | EventHandler instance 92 | """ 93 | event_handler = EventHandler(bot) 94 | 95 | # Register event handlers 96 | bot.add_listener(event_handler.on_ready, "on_ready") 97 | 98 | return event_handler 99 | -------------------------------------------------------------------------------- /docs/src/troubleshooting/error-messages.md: -------------------------------------------------------------------------------- 1 | # Error Messages 2 | 3 | Common GitCord error messages and how to fix them. 4 | 5 | ## Authentication Errors 6 | 7 | ### Invalid Discord Token 8 | 9 | **Error:** 10 | ``` 11 | Invalid Discord token! Please check your DISCORD_TOKEN in the .env file. 12 | ``` 13 | 14 | **Fix:** 15 | 1. Go to [Discord Developer Portal](https://discord.com/developers/applications) 16 | 2. Select your application 17 | 3. Go to "Bot" section 18 | 4. Click "Reset Token" to get a new token 19 | 5. Update your `.env` file 20 | 21 | ### Login Failure 22 | 23 | **Error:** 24 | ``` 25 | discord.errors.LoginFailure: 401 Unauthorized 26 | ``` 27 | 28 | **Fix:** 29 | - Check your bot token is correct 30 | - Make sure bot application exists and is active 31 | 32 | ## Permission Errors 33 | 34 | ### Missing Permissions 35 | 36 | **Error:** 37 | ``` 38 | You don't have permission to use this command! 39 | ``` 40 | 41 | **Fix:** 42 | - Check your role permissions in server 43 | - Make sure you have the right permission level 44 | - Contact a server administrator 45 | 46 | ### Bot Permission Issues 47 | 48 | **Error:** 49 | ``` 50 | discord.errors.Forbidden: 403 Forbidden (error code: 50013): Missing Permissions 51 | ``` 52 | 53 | **Fix:** 54 | - Go to Server Settings → Roles 55 | - Make sure bot's role has necessary permissions 56 | - Move bot's role higher in the role list 57 | 58 | ## Command Errors 59 | 60 | ### Command Not Found 61 | 62 | **Error:** 63 | ``` 64 | Command not found. Try `!help` for available commands. 65 | ``` 66 | 67 | **Fix:** 68 | - Check command spelling 69 | - Run `/synccommands` to update slash commands 70 | - Try `!help` to see available commands 71 | 72 | ## YAML Errors 73 | 74 | ### File Not Found 75 | 76 | **Error:** 77 | ``` 78 | YAML file not found at: {path} 79 | ``` 80 | 81 | **Fix:** 82 | - Check the file path is correct 83 | - Make sure the file exists 84 | - Use the right path format 85 | 86 | ### Invalid YAML 87 | 88 | **Error:** 89 | ``` 90 | Failed to parse YAML file: {error} 91 | ``` 92 | 93 | **Fix:** 94 | - Use a YAML validator to check syntax 95 | - Check indentation (use spaces, not tabs) 96 | - Make sure all quotes match 97 | 98 | ### Missing Required Field 99 | 100 | **Error:** 101 | ``` 102 | Missing required field: {field} 103 | ``` 104 | 105 | **Fix:** 106 | - Add the missing field to your YAML 107 | - Required fields: `name`, `type`, `position` 108 | - Check field spelling 109 | 110 | ### Invalid Channel Type 111 | 112 | **Error:** 113 | ``` 114 | Channel type '{type}' is not supported. Use 'text' or 'voice'. 115 | ``` 116 | 117 | **Fix:** 118 | - Use only "text", "voice", or "category" 119 | - Check spelling in your YAML file 120 | 121 | ## Network Errors 122 | 123 | ### Fetch Error 124 | 125 | **Error:** 126 | ``` 127 | Failed to fetch content from the URL: {error} 128 | ``` 129 | 130 | **Fix:** 131 | - Check the URL is valid and accessible 132 | - Make sure URL starts with http:// or https:// 133 | - Try a different URL to test 134 | 135 | ### No Content Found 136 | 137 | **Error:** 138 | ``` 139 | No readable text content was found on the provided URL. 140 | ``` 141 | 142 | **Fix:** 143 | - Try a different website 144 | - Check if the website has text content 145 | - Some websites block content fetching 146 | 147 | ## Discord API Errors 148 | 149 | ### Discord API Error 150 | 151 | **Error:** 152 | ``` 153 | Discord API error: {error} 154 | ``` 155 | 156 | **Fix:** 157 | - Check Discord's status 158 | - Wait a few minutes and try again 159 | - Check your internet connection 160 | 161 | ### Unexpected Error 162 | 163 | **Error:** 164 | ``` 165 | An unexpected error occurred: {error} 166 | ``` 167 | 168 | **Fix:** 169 | - Restart the bot 170 | - Check the bot logs for more details 171 | - Report the issue with error details 172 | 173 | ## Getting Help 174 | 175 | If you can't fix the error: 176 | 177 | 1. Copy the exact error message 178 | 2. Check the [Common Issues](./common-issues.md) page 179 | 3. Enable [Debug Mode](./debug-mode.md) for more info 180 | 4. Ask for help in the project's GitHub issues 181 | -------------------------------------------------------------------------------- /docs/custom.css: -------------------------------------------------------------------------------- 1 | /* Custom styles for GitCord documentation */ 2 | 3 | :root { 4 | --primary-color: #5865f2; 5 | --secondary-color: #7289da; 6 | --accent-color: #99aab5; 7 | --background-color: #ffffff; 8 | --text-color: #2c2f33; 9 | --code-background: #f6f8fa; 10 | --border-color: #e1e4e8; 11 | } 12 | 13 | /* Header styling */ 14 | .book-header { 15 | background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); 16 | border-bottom: 3px solid var(--accent-color); 17 | } 18 | 19 | .book-header h1 { 20 | color: white; 21 | text-shadow: 0 2px 4px rgba(0,0,0,0.3); 22 | } 23 | 24 | /* Navigation styling */ 25 | .book-summary { 26 | background-color: #f8f9fa; 27 | border-right: 1px solid var(--border-color); 28 | } 29 | 30 | .book-summary ul.summary li a { 31 | color: var(--text-color); 32 | border-left: 3px solid transparent; 33 | transition: all 0.3s ease; 34 | } 35 | 36 | .book-summary ul.summary li a:hover { 37 | background-color: var(--primary-color); 38 | color: white; 39 | border-left-color: var(--accent-color); 40 | } 41 | 42 | .book-summary ul.summary li.active > a { 43 | background-color: var(--primary-color); 44 | color: white; 45 | border-left-color: var(--accent-color); 46 | } 47 | 48 | /* Code blocks */ 49 | .markdown pre { 50 | background-color: var(--code-background); 51 | border: 1px solid var(--border-color); 52 | border-radius: 6px; 53 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 54 | } 55 | 56 | .markdown code { 57 | background-color: var(--code-background); 58 | color: #e83e8c; 59 | padding: 2px 4px; 60 | border-radius: 3px; 61 | font-size: 0.9em; 62 | } 63 | 64 | /* YAML syntax highlighting */ 65 | .hljs-string { 66 | color: #28a745; 67 | } 68 | 69 | .hljs-keyword { 70 | color: #007bff; 71 | } 72 | 73 | .hljs-number { 74 | color: #fd7e14; 75 | } 76 | 77 | /* Buttons and interactive elements */ 78 | .btn { 79 | background-color: var(--primary-color); 80 | border: none; 81 | color: white; 82 | padding: 8px 16px; 83 | border-radius: 4px; 84 | text-decoration: none; 85 | display: inline-block; 86 | transition: background-color 0.3s ease; 87 | } 88 | 89 | .btn:hover { 90 | background-color: var(--secondary-color); 91 | color: white; 92 | text-decoration: none; 93 | } 94 | 95 | /* Alerts and notices */ 96 | .alert { 97 | padding: 12px 16px; 98 | margin: 16px 0; 99 | border-radius: 6px; 100 | border-left: 4px solid; 101 | } 102 | 103 | .alert-info { 104 | background-color: #d1ecf1; 105 | border-color: #17a2b8; 106 | color: #0c5460; 107 | } 108 | 109 | .alert-warning { 110 | background-color: #fff3cd; 111 | border-color: #ffc107; 112 | color: #856404; 113 | } 114 | 115 | .alert-success { 116 | background-color: #d4edda; 117 | border-color: #28a745; 118 | color: #155724; 119 | } 120 | 121 | .alert-danger { 122 | background-color: #f8d7da; 123 | border-color: #dc3545; 124 | color: #721c24; 125 | } 126 | 127 | /* Tables */ 128 | .markdown table { 129 | border-collapse: collapse; 130 | width: 100%; 131 | margin: 16px 0; 132 | } 133 | 134 | .markdown table th, 135 | .markdown table td { 136 | border: 1px solid var(--border-color); 137 | padding: 8px 12px; 138 | text-align: left; 139 | } 140 | 141 | .markdown table th { 142 | background-color: var(--code-background); 143 | font-weight: 600; 144 | } 145 | 146 | .markdown table tr:nth-child(even) { 147 | background-color: #f8f9fa; 148 | } 149 | 150 | /* Responsive design */ 151 | @media (max-width: 768px) { 152 | .book-summary { 153 | position: fixed; 154 | top: 0; 155 | left: -100%; 156 | width: 80%; 157 | height: 100%; 158 | z-index: 1000; 159 | transition: left 0.3s ease; 160 | } 161 | 162 | .book-summary.open { 163 | left: 0; 164 | } 165 | } 166 | 167 | /* Print styles */ 168 | @media print { 169 | .book-summary, 170 | .book-header { 171 | display: none; 172 | } 173 | 174 | .markdown { 175 | margin: 0; 176 | padding: 0; 177 | } 178 | } -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '17 23 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: python 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Add any setup steps before running the `github/codeql-action/init` action. 61 | # This includes steps like installing compilers or runtimes (`actions/setup-node` 62 | # or others). This is typically only required for manual builds. 63 | # - name: Setup runtime (example) 64 | # uses: actions/setup-example@v1 65 | 66 | # Initializes the CodeQL tools for scanning. 67 | - name: Initialize CodeQL 68 | uses: github/codeql-action/init@v3 69 | with: 70 | languages: ${{ matrix.language }} 71 | build-mode: ${{ matrix.build-mode }} 72 | # If you wish to specify custom queries, you can do so here or in a config file. 73 | # By default, queries listed here will override any specified in a config file. 74 | # Prefix the list here with "+" to use these queries and those in the config file. 75 | 76 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 77 | # queries: security-extended,security-and-quality 78 | 79 | # If the analyze step fails for one of the languages you are analyzing with 80 | # "We were unable to automatically build your code", modify the matrix above 81 | # to set the build mode to "manual" for that language. Then modify this step 82 | # to build your code. 83 | # ℹ️ Command-line programs to run using the OS shell. 84 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 85 | - if: matrix.build-mode == 'manual' 86 | shell: bash 87 | run: | 88 | echo 'If you are using a "manual" build mode for one or more of the' \ 89 | 'languages you are analyzing, replace this with the commands to build' \ 90 | 'your code, for example:' 91 | echo ' make bootstrap' 92 | echo ' make release' 93 | exit 1 94 | 95 | - name: Perform CodeQL Analysis 96 | uses: github/codeql-action/analyze@v3 97 | with: 98 | category: "/language:${{matrix.language}}" 99 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | - Demonstrating empathy and kindness toward other people 22 | - Being respectful of differing opinions, viewpoints, and experiences 23 | - Giving and gracefully accepting constructive feedback 24 | - Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | - Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | - The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | - Trolling, insulting or derogatory comments, and personal or political attacks 34 | - Public or private harassment 35 | - Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | - Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at evolvewithevan@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the 118 | [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, 119 | available at 120 | . 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/inclusion). 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | . Translations are available at 127 | . 128 | 129 | -------------------------------------------------------------------------------- /src/gitcord/views/channel_views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Channel management UI components for GitCord bot. 3 | """ 4 | 5 | import discord 6 | from discord.ui import Button, View 7 | 8 | from ..utils.helpers import create_embed 9 | from ..utils.logger import main_logger as logger 10 | 11 | 12 | class DeleteExtraChannelsView(View): 13 | """View for confirming deletion of extra channels.""" 14 | 15 | def __init__(self, extra_channels, category_name, timeout=60): 16 | super().__init__(timeout=timeout) 17 | self.extra_channels = extra_channels 18 | self.category_name = category_name 19 | 20 | # Add delete button 21 | delete_button = Button( 22 | label="🗑️ Delete Extra Channels", 23 | style=discord.ButtonStyle.danger, 24 | custom_id="delete_extra_channels", 25 | ) 26 | delete_button.callback = self.delete_callback 27 | self.add_item(delete_button) 28 | 29 | async def delete_callback(self, interaction: discord.Interaction): 30 | """Handle delete button click.""" 31 | # Check if user has manage channels permission 32 | if not interaction.user.guild_permissions.manage_channels: # type: ignore 33 | embed = create_embed( 34 | title="❌ Permission Denied", 35 | description="You need the 'Manage Channels' permission to delete channels.", 36 | color=discord.Color.red(), 37 | ) 38 | await interaction.response.send_message(embed=embed, ephemeral=True) 39 | return 40 | 41 | # Create confirmation embed 42 | channel_list = "\n".join( 43 | [f"• {channel.mention}" for channel in self.extra_channels] 44 | ) 45 | embed = create_embed( 46 | title="⚠️ Confirm Deletion", 47 | description=( 48 | f"Are you sure you want to delete the following channels from " 49 | f"**{self.category_name}**?\n\n{channel_list}\n\n**This action is irreversible!**" 50 | ), 51 | color=discord.Color.orange(), 52 | ) 53 | 54 | # Create confirmation view 55 | confirm_view = ConfirmDeleteView(self.extra_channels, self.category_name) 56 | await interaction.response.send_message( 57 | embed=embed, view=confirm_view, ephemeral=True 58 | ) 59 | 60 | 61 | class ConfirmDeleteView(View): 62 | """View for final confirmation of channel deletion.""" 63 | 64 | def __init__(self, extra_channels, category_name, timeout=60): 65 | super().__init__(timeout=timeout) 66 | self.extra_channels = extra_channels 67 | self.category_name = category_name 68 | 69 | # Add confirm and cancel buttons 70 | confirm_button = Button( 71 | label="✅ Yes, Delete All", 72 | style=discord.ButtonStyle.danger, 73 | custom_id="confirm_delete", 74 | ) 75 | confirm_button.callback = self.confirm_callback 76 | 77 | cancel_button = Button( 78 | label="❌ Cancel", 79 | style=discord.ButtonStyle.secondary, 80 | custom_id="cancel_delete", 81 | ) 82 | cancel_button.callback = self.cancel_callback 83 | 84 | self.add_item(confirm_button) 85 | self.add_item(cancel_button) 86 | 87 | async def confirm_callback(self, interaction: discord.Interaction): 88 | """Handle confirm button click.""" 89 | deleted_channels = [] 90 | failed_channels = [] 91 | 92 | # Delete each channel 93 | for channel in self.extra_channels: 94 | try: 95 | channel_name = channel.name 96 | await channel.delete() 97 | deleted_channels.append(channel_name) 98 | logger.info( 99 | "Deleted extra channel '%s' from category '%s'", 100 | channel_name, 101 | self.category_name, 102 | ) 103 | except (discord.Forbidden, discord.HTTPException, OSError) as e: 104 | failed_channels.append(channel.name) 105 | logger.error("Failed to delete channel '%s': %s", channel.name, e) 106 | 107 | # Create result embed 108 | if deleted_channels: 109 | embed = create_embed( 110 | title="✅ Channels Deleted", 111 | description=( 112 | f"Successfully deleted {len(deleted_channels)} extra channels " 113 | f"from **{self.category_name}**" 114 | ), 115 | color=discord.Color.green(), 116 | ) 117 | 118 | if deleted_channels: 119 | deleted_list = "\n".join([f"• #{name}" for name in deleted_channels]) 120 | embed.add_field( 121 | name="Deleted Channels", value=deleted_list, inline=False 122 | ) 123 | 124 | if failed_channels: 125 | failed_list = "\n".join([f"• #{name}" for name in failed_channels]) 126 | embed.add_field( 127 | name="Failed to Delete", value=failed_list, inline=False 128 | ) 129 | else: 130 | embed = create_embed( 131 | title="❌ Deletion Failed", 132 | description=( 133 | "Failed to delete any channels. Please check permissions and try again." 134 | ), 135 | color=discord.Color.red(), 136 | ) 137 | 138 | await interaction.response.edit_message(embed=embed, view=None) 139 | 140 | async def cancel_callback(self, interaction: discord.Interaction): 141 | """Handle cancel button click.""" 142 | embed = create_embed( 143 | title="❌ Deletion Cancelled", 144 | description="Channel deletion was cancelled." 145 | ) 146 | await interaction.response.edit_message(embed=embed, view=None) 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitCord 2 | 3 | A Discord bot for GitOps-based Discord server management. Manage your server's channels and categories using YAML templates, version control, and GitHub integration. 4 | 5 | [![Documentation](https://img.shields.io/badge/docs-mdBook-blue)](https://evolvewithevan.github.io/gitcord/) 6 | [![License: GPL v3](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE) 7 | 8 | --- 9 | 10 | ## 🚀 Getting Started 11 | 12 | ### Option 1: Use Pre-Hosted Bot (Recommended) 13 | 14 | The easiest way to get started is to invite the pre-hosted GitCord bot to your Discord server: 15 | 16 | **[📥 Invite GitCord Bot](https://discord.com/oauth2/authorize?client_id=1391153955936927824)** 17 | 18 | After inviting the bot, you can start using commands immediately. See the [Quick Start Guide](https://evolvewithevan.github.io/gitcord/getting-started/quick-start.html) for usage examples. 19 | 20 | ### Option 2: Self-Host (Advanced Users) 21 | 22 | If you prefer to run your own instance of GitCord: 23 | 24 | 1. **Clone the repository:** 25 | ```bash 26 | git clone https://github.com/evolvewithevan/gitcord.git 27 | cd gitcord 28 | ``` 29 | 2. **Install dependencies (requires Python 3.9+ and [uv](https://github.com/astral-sh/uv))** 30 | ```bash 31 | uv sync 32 | ``` 33 | 3. **Set up environment variables:** 34 | ```bash 35 | cp .env.example .env 36 | # Edit .env and add your Discord bot token 37 | ``` 38 | 4. **Run the bot:** 39 | ```bash 40 | python -m gitcord 41 | ``` 42 | 43 | For detailed self-hosting setup, see the [Installation Guide](https://evolvewithevan.github.io/gitcord/getting-started/installation.html). 44 | 45 | --- 46 | 47 | ## 📝 What is GitCord? 48 | 49 | GitCord is a Discord bot that lets you manage your server's structure using YAML configuration files, stored in Git and optionally synced with GitHub. It enables: 50 | - **Version-controlled server configuration** 51 | - **Automated and manual sync of categories/channels** 52 | - **Bulk creation of channels/categories from templates** 53 | - **Easy server setup and reproducibility** 54 | 55 | --- 56 | 57 | ## ✨ Features 58 | - **Template-based Channel & Category Creation**: Use YAML files to define your server structure 59 | - **Manual & Planned Automatic Sync**: Pull changes from a GitHub repo or local files 60 | - **Slash & Prefix Commands**: Use `/createcategory`, `/createchannel`, `!createcategory`, etc. 61 | - **Permission Management**: Follows Discord's permission system 62 | - **Extensible**: Modular cog system for easy extension 63 | - **Open Source**: GPL-3.0 License 64 | 65 | See the [full feature list](https://evolvewithevan.github.io/gitcord/introduction.html#key-features). 66 | 67 | --- 68 | 69 | ## 🛠️ Example Usage 70 | 71 | - **Create a channel from YAML:** 72 | ```yaml 73 | # general.yaml 74 | name: general 75 | type: text 76 | topic: General discussion 77 | position: 0 78 | nsfw: false 79 | ``` 80 | Use: `!createchannel` 81 | 82 | - **Create a category with channels:** 83 | ```yaml 84 | # community.yaml 85 | name: Community 86 | type: category 87 | position: 0 88 | channels: 89 | - general 90 | - memes 91 | - off-topic 92 | ``` 93 | Use: `!createcategory` or `/createcategory` 94 | 95 | See [Quick Start](https://evolvewithevan.github.io/gitcord/getting-started/quick-start.html) and [Templates Guide](https://evolvewithevan.github.io/gitcord/templates/category-templates.html). 96 | 97 | --- 98 | 99 | ## 🧩 Main Commands 100 | 101 | - `!hello` / `/hello` — Greet the bot 102 | - `!ping` / `/slashping` — Check bot latency 103 | - `!createchannel` — Create a channel from YAML 104 | - `!createcategory` / `/createcategory [yaml_path]` — Create a category with channels 105 | - `!fetchurl ` / `/fetchurl ` — Fetch text from a website (admin) 106 | - `!synccommands` / `/synccommands` — Update slash commands (admin) 107 | - `!help` / `/help` — Show help and links 108 | 109 | See [Commands Reference](https://evolvewithevan.github.io/gitcord/user-guide/commands.html). 110 | 111 | --- 112 | 113 | ## 📁 Project Structure 114 | 115 | ``` 116 | gitcord/ 117 | ├── src/gitcord/ # Main source code 118 | │ ├── bot.py # Main bot entry point 119 | │ ├── config.py # Configuration management 120 | │ ├── events.py # Discord event handlers 121 | │ ├── cogs/ # Discord.py cogs (command modules) 122 | │ ├── utils/ # Utility functions 123 | │ ├── views/ # Discord UI components 124 | │ └── constants/ # Constants and messages 125 | ├── gitcord-template/ # Example template repository 126 | ├── docs/ # Documentation (mdBook) 127 | ├── requirements.txt # Python dependencies 128 | ├── pyproject.toml # Project metadata 129 | └── README.md # Project documentation 130 | ``` 131 | 132 | --- 133 | 134 | ## 📈 Project Status & Roadmap 135 | 136 | - **Alpha**: Core features implemented, active development 137 | - See the [Roadmap](https://github.com/users/evolvewithevan/projects/4) for planned features and progress 138 | - [Planned Features](https://evolvewithevan.github.io/gitcord/templates/category-templates.html#future-enhancements): 139 | - Webhook-based automatic sync 140 | - Advanced template features (inheritance, variables) 141 | - More admin tools 142 | 143 | --- 144 | 145 | ## 🤝 Contributing 146 | 147 | We welcome contributions! Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) for: 148 | - Coding standards (PEP8, type hints, docstrings) 149 | - How to set up your dev environment 150 | - Testing and documentation guidelines 151 | - Pull request process 152 | 153 | --- 154 | 155 | ## 🆘 Support & Troubleshooting 156 | 157 | - [Common Issues](https://evolvewithevan.github.io/gitcord/troubleshooting/common-issues.html) 158 | - [Error Messages](https://evolvewithevan.github.io/gitcord/troubleshooting/error-messages.html) 159 | - [GitHub Issues](https://github.com/evolvewithevan/gitcord/issues) 160 | - [Discussions](https://github.com/evolvewithevan/gitcord/discussions) 161 | 162 | --- 163 | 164 | ## 📜 License 165 | 166 | This project is licensed under the GNU General Public License v3.0. See [LICENSE](LICENSE). 167 | 168 | --- 169 | 170 | Made with ❤️ by the GitCord Team. [Full Documentation](https://evolvewithevan.github.io/gitcord/) 171 | -------------------------------------------------------------------------------- /src/gitcord/cogs/help.py: -------------------------------------------------------------------------------- 1 | """ 2 | Help system cog for GitCord bot. 3 | Contains help commands and documentation links. 4 | """ 5 | 6 | import discord 7 | from discord import app_commands 8 | from discord.ext import commands 9 | 10 | from ..utils.helpers import create_embed 11 | from ..utils.logger import main_logger as logger 12 | 13 | 14 | class Help(commands.Cog): 15 | """Help system commands.""" 16 | 17 | def __init__(self, bot: commands.Bot): 18 | """Initialize the Help cog.""" 19 | self.bot = bot 20 | logger.info("Help cog loaded") 21 | 22 | @commands.command(name="help") 23 | async def help_prefix(self, ctx: commands.Context) -> None: 24 | """Prefix command to show help information and link to the wiki.""" 25 | embed = create_embed( 26 | title="🤖 GitCord Help", 27 | description="Welcome to GitCord! Here's how to get help and learn more about the bot." 28 | ) 29 | 30 | embed.add_field( 31 | name="📚 Documentation", 32 | value=( 33 | "For detailed documentation, tutorials, and guides, visit our " 34 | "[Wiki](https://github.com/evolvewithevan/gitcord/wiki)\n" 35 | "To see current issues, visit our " 36 | "[GitHub Issues](https://github.com/users/evolvewithevan/projects/4/views/1)" 37 | ), 38 | inline=False, 39 | ) 40 | 41 | embed.add_field( 42 | name="🔧 Available Commands", 43 | value="• `!hello` - Get a friendly greeting\n" 44 | "• `!help` - Show help information and link to the wiki (***YOU ARE HERE***)\n" 45 | "• `!ping` - Check bot latency\n" 46 | "• `!createchannel` - Create a channel from YAML (Requires Manage Channels)\n" 47 | "• `!createcategory` - Create a category from YAML (Requires Manage Channels)\n" 48 | "• `!git clone [-b branch]` - Clone a template repo for this server (Admin only)\n" 49 | "• `!git pull` - Pull latest changes and apply template (Admin only)\n" 50 | "• `!synccommands` - Sync slash commands (Admin only)\n" 51 | "• `!applytemplate` - (Deprecated) Use !git clone and !git pull instead", 52 | inline=False, 53 | ) 54 | 55 | embed.add_field( 56 | name="⚡ Slash Commands", 57 | value="• `/slashping` - Check bot latency\n" 58 | "• `/createcategory [yaml_path]` - Create category from YAML (Requires Manage Channels)\n" 59 | "• `/synccommands` - Sync slash commands (Admin only)\n" 60 | "• `/applytemplate` - (Deprecated) Use !git clone and !git pull instead", 61 | inline=False, 62 | ) 63 | 64 | embed.add_field( 65 | name="🔗 Quick Links", 66 | value="• [GitHub Repository](https://github.com/evolvewithevan/gitcord)\n" 67 | "• [Github Project](https://github.com/users/evolvewithevan/projects/4/views/1)\n" 68 | "• [Roadmap](https://github.com/users/evolvewithevan/projects/4/views/3)\n" 69 | "• [Wiki Documentation](https://github.com/evolvewithevan/gitcord/wiki)\n" 70 | "• [Security Policy](https://github.com/evolvewithevan/gitcord/blob/main/.github/SECURITY.md)", 71 | inline=False, 72 | ) 73 | 74 | embed.set_footer(text="GitCord - Discord bot for GitOps-based server structure. For more, see the Wiki.") 75 | 76 | await ctx.send(embed=embed) 77 | 78 | @app_commands.command( 79 | name="help", description="Show help information and link to the wiki" 80 | ) 81 | async def help_slash(self, interaction: discord.Interaction) -> None: 82 | """Slash command to show help information and link to the wiki.""" 83 | embed = create_embed( 84 | title="🤖 GitCord Help", 85 | description="Welcome to GitCord! Here's how to get help and learn more about the bot." 86 | ) 87 | 88 | embed.add_field( 89 | name="📚 Documentation", 90 | value=( 91 | "For detailed documentation, tutorials, and guides, visit our " 92 | "[Wiki](https://github.com/evolvewithevan/gitcord/wiki)\n" 93 | "To see current issues, visit our " 94 | "[GitHub Issues](https://github.com/users/evolvewithevan/projects/4/views/1)" 95 | ), 96 | inline=False, 97 | ) 98 | 99 | embed.add_field( 100 | name="🔧 Available Commands", 101 | value="• `!hello` - Get a friendly greeting\n" 102 | "• `!help` - Show help information and link to the wiki (***YOU ARE HERE***)\n" 103 | "• `!ping` - Check bot latency\n" 104 | "• `!createchannel` - Create a channel from YAML (Requires Manage Channels)\n" 105 | "• `!createcategory` - Create a category from YAML (Requires Manage Channels)\n" 106 | "• `!git clone [-b branch]` - Clone a template repo for this server (Admin only)\n" 107 | "• `!git pull` - Pull latest changes and apply template (Admin only)\n" 108 | "• `!synccommands` - Sync slash commands (Admin only)\n" 109 | "• `!applytemplate` - (Deprecated) Use !git clone and !git pull instead", 110 | inline=False, 111 | ) 112 | 113 | embed.add_field( 114 | name="⚡ Slash Commands", 115 | value="• `/slashping` - Check bot latency\n" 116 | "• `/createcategory [yaml_path]` - Create category from YAML (Requires Manage Channels)\n" 117 | "• `/synccommands` - Sync slash commands (Admin only)\n" 118 | "• `/applytemplate` - (Deprecated) Use !git clone and !git pull instead", 119 | inline=False, 120 | ) 121 | 122 | embed.add_field( 123 | name="🔗 Quick Links", 124 | value="• [GitHub Repository](https://github.com/evolvewithevan/gitcord)\n" 125 | "• [Github Project](https://github.com/users/evolvewithevan/projects/4/views/1)\n" 126 | "• [Roadmap](https://github.com/users/evolvewithevan/projects/4/views/3)\n" 127 | "• [Wiki Documentation](https://github.com/evolvewithevan/gitcord/wiki)\n" 128 | "• [Security Policy](https://github.com/evolvewithevan/gitcord/blob/main/.github/SECURITY.md)", 129 | inline=False, 130 | ) 131 | 132 | embed.set_footer(text="GitCord - Discord bot for GitOps-based server structure. For more, see the Wiki.") 133 | 134 | await interaction.response.send_message(embed=embed) 135 | 136 | 137 | async def setup(bot: commands.Bot) -> None: 138 | """Set up the Help cog.""" 139 | await bot.add_cog(Help(bot)) 140 | -------------------------------------------------------------------------------- /src/gitcord/views/base_views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base UI components and utilities for GitCord bot views. 3 | """ 4 | 5 | import discord 6 | from discord.ui import Button, View 7 | 8 | from ..utils.helpers import create_embed 9 | 10 | 11 | class BaseView(View): 12 | """Base view class with common functionality.""" 13 | 14 | def __init__(self, timeout=60): 15 | super().__init__(timeout=timeout) 16 | 17 | async def on_timeout(self): 18 | """Handle view timeout.""" 19 | # Disable all buttons when view times out 20 | for child in self.children: 21 | if isinstance(child, Button): 22 | child.disabled = True 23 | 24 | def disable_all_buttons(self): 25 | """Disable all buttons in the view.""" 26 | for child in self.children: 27 | if isinstance(child, Button): 28 | child.disabled = True 29 | 30 | 31 | class ConfirmationView(BaseView): 32 | """Base confirmation view with yes/no buttons.""" 33 | 34 | def __init__(self, title: str, description: str, timeout=60): 35 | super().__init__(timeout=timeout) 36 | self.title = title 37 | self.description = description 38 | self.result: bool | None = None 39 | 40 | # Add confirm and cancel buttons 41 | confirm_button: Button = Button( 42 | label="✅ Confirm", style=discord.ButtonStyle.success, custom_id="confirm" 43 | ) 44 | confirm_button.callback = self.confirm_callback 45 | 46 | cancel_button: Button = Button( 47 | label="❌ Cancel", style=discord.ButtonStyle.secondary, custom_id="cancel" 48 | ) 49 | cancel_button.callback = self.cancel_callback 50 | 51 | self.add_item(confirm_button) 52 | self.add_item(cancel_button) 53 | 54 | async def confirm_callback(self, interaction: discord.Interaction): 55 | """Handle confirm button click.""" 56 | self.result = True 57 | embed = create_embed( 58 | title="✅ Confirmed", 59 | description=self.description, 60 | color=discord.Color.green(), 61 | ) 62 | await interaction.response.edit_message(embed=embed, view=None) 63 | 64 | async def cancel_callback(self, interaction: discord.Interaction): 65 | """Handle cancel button click.""" 66 | self.result = False 67 | embed = create_embed( 68 | title="❌ Cancelled", 69 | description="Action was cancelled." 70 | ) 71 | await interaction.response.edit_message(embed=embed, view=None) 72 | 73 | 74 | class ErrorView(BaseView): 75 | """View for displaying errors with a close button.""" 76 | 77 | def __init__(self, title: str, description: str, timeout=30): 78 | super().__init__(timeout=timeout) 79 | self.title = title 80 | self.description = description 81 | 82 | close_button: Button = Button( 83 | label="❌ Close", style=discord.ButtonStyle.secondary, custom_id="close" 84 | ) 85 | close_button.callback = self.close_callback 86 | self.add_item(close_button) 87 | 88 | async def close_callback(self, interaction: discord.Interaction): 89 | """Handle close button click.""" 90 | if interaction.message: 91 | await interaction.message.delete() 92 | 93 | 94 | class LoadingView(BaseView): 95 | """View for showing loading state.""" 96 | 97 | def __init__(self, message: str = "Loading...", timeout=30): 98 | super().__init__(timeout=timeout) 99 | 100 | # Create a disabled button to show loading state 101 | loading_button: Button = Button( 102 | label="⏳ " + message, 103 | style=discord.ButtonStyle.secondary, 104 | disabled=True, 105 | custom_id="loading", 106 | ) 107 | self.add_item(loading_button) 108 | 109 | 110 | class DeleteExtraObjectsView(View): 111 | """ 112 | Generic view for confirming deletion of extra Discord objects (channels, categories, etc.). 113 | Objects must have .name and .delete(). 114 | """ 115 | 116 | def __init__(self, extra_objects, object_type_label, timeout=60): 117 | super().__init__(timeout=timeout) 118 | self.extra_objects = extra_objects 119 | self.object_type_label = object_type_label 120 | delete_button = Button( 121 | label=f"🗑️ Delete Extra {object_type_label.title()}s", 122 | style=discord.ButtonStyle.danger, 123 | custom_id=f"delete_extra_{object_type_label}s", 124 | ) 125 | delete_button.callback = self.delete_callback 126 | self.add_item(delete_button) 127 | 128 | async def delete_callback(self, interaction: discord.Interaction): 129 | # Check permissions (manage_channels for channels, manage_channels for categories) 130 | if not interaction.user.guild_permissions.manage_channels: # type: ignore 131 | from ..utils.helpers import create_embed 132 | 133 | embed = create_embed( 134 | title="❌ Permission Denied", 135 | description=f"You need the 'Manage Channels' permission to delete {self.object_type_label}s.", 136 | color=discord.Color.red(), 137 | ) 138 | await interaction.response.send_message(embed=embed, ephemeral=True) 139 | return 140 | # Create confirmation embed 141 | object_list = "\n".join( 142 | [ 143 | f"• {getattr(obj, 'mention', '#' + obj.name)}" 144 | for obj in self.extra_objects 145 | ] 146 | ) 147 | from ..utils.helpers import create_embed 148 | 149 | embed = create_embed( 150 | title="⚠️ Confirm Deletion", 151 | description=( 152 | f"Are you sure you want to delete the following {self.object_type_label}s?\n\n{object_list}\n\n**This action is irreversible!**" 153 | ), 154 | color=discord.Color.orange(), 155 | ) 156 | confirm_view = ConfirmDeleteObjectsView( 157 | self.extra_objects, self.object_type_label 158 | ) 159 | await interaction.response.send_message( 160 | embed=embed, view=confirm_view, ephemeral=True 161 | ) 162 | 163 | 164 | class ConfirmDeleteObjectsView(View): 165 | """ 166 | View for final confirmation of object deletion. 167 | Objects must have .name and .delete(). 168 | """ 169 | 170 | def __init__(self, extra_objects, object_type_label, timeout=60): 171 | super().__init__(timeout=timeout) 172 | self.extra_objects = extra_objects 173 | self.object_type_label = object_type_label 174 | confirm_button = Button( 175 | label="✅ Yes, Delete All", 176 | style=discord.ButtonStyle.danger, 177 | custom_id="confirm_delete", 178 | ) 179 | confirm_button.callback = self.confirm_callback 180 | cancel_button = Button( 181 | label="❌ Cancel", 182 | style=discord.ButtonStyle.secondary, 183 | custom_id="cancel_delete", 184 | ) 185 | cancel_button.callback = self.cancel_callback 186 | self.add_item(confirm_button) 187 | self.add_item(cancel_button) 188 | 189 | async def confirm_callback(self, interaction: discord.Interaction): 190 | deleted = [] 191 | failed = [] 192 | for obj in self.extra_objects: 193 | try: 194 | await obj.delete() 195 | deleted.append(obj.name) 196 | except Exception: 197 | failed.append(obj.name) 198 | from ..utils.helpers import create_embed 199 | 200 | if deleted: 201 | embed = create_embed( 202 | title=f"✅ {self.object_type_label.title()}s Deleted", 203 | description=f"Successfully deleted {len(deleted)} {self.object_type_label}s.", 204 | color=discord.Color.green(), 205 | ) 206 | embed.add_field( 207 | name="Deleted", 208 | value="\n".join([f"• {name}" for name in deleted]), 209 | inline=False, 210 | ) 211 | if failed: 212 | embed.add_field( 213 | name="Failed", 214 | value="\n".join([f"• {name}" for name in failed]), 215 | inline=False, 216 | ) 217 | else: 218 | embed = create_embed( 219 | title="❌ Deletion Failed", 220 | description="Failed to delete any objects. Please check permissions and try again.", 221 | color=discord.Color.red(), 222 | ) 223 | await interaction.response.edit_message(embed=embed, view=None) 224 | 225 | async def cancel_callback(self, interaction: discord.Interaction): 226 | from ..utils.helpers import create_embed 227 | 228 | embed = create_embed( 229 | title="❌ Deletion Cancelled", 230 | description=f"{self.object_type_label.title()} deletion was cancelled." 231 | ) 232 | await interaction.response.edit_message(embed=embed, view=None) 233 | -------------------------------------------------------------------------------- /docs/custom.js: -------------------------------------------------------------------------------- 1 | // Custom JavaScript for GitCord documentation 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | // Add copy button to code blocks 7 | function addCopyButtons() { 8 | const codeBlocks = document.querySelectorAll('pre code'); 9 | 10 | codeBlocks.forEach((block, index) => { 11 | const button = document.createElement('button'); 12 | button.className = 'copy-btn'; 13 | button.textContent = 'Copy'; 14 | button.style.cssText = ` 15 | position: absolute; 16 | top: 8px; 17 | right: 8px; 18 | background: #5865f2; 19 | color: white; 20 | border: none; 21 | border-radius: 4px; 22 | padding: 4px 8px; 23 | font-size: 12px; 24 | cursor: pointer; 25 | opacity: 0; 26 | transition: opacity 0.3s ease; 27 | `; 28 | 29 | const pre = block.parentElement; 30 | pre.style.position = 'relative'; 31 | 32 | pre.addEventListener('mouseenter', () => { 33 | button.style.opacity = '1'; 34 | }); 35 | 36 | pre.addEventListener('mouseleave', () => { 37 | button.style.opacity = '0'; 38 | }); 39 | 40 | button.addEventListener('click', async () => { 41 | try { 42 | await navigator.clipboard.writeText(block.textContent); 43 | button.textContent = 'Copied!'; 44 | setTimeout(() => { 45 | button.textContent = 'Copy'; 46 | }, 2000); 47 | } catch (err) { 48 | console.error('Failed to copy: ', err); 49 | button.textContent = 'Failed'; 50 | setTimeout(() => { 51 | button.textContent = 'Copy'; 52 | }, 2000); 53 | } 54 | }); 55 | 56 | pre.appendChild(button); 57 | }); 58 | } 59 | 60 | // Add syntax highlighting for YAML 61 | function highlightYAML() { 62 | const yamlBlocks = document.querySelectorAll('pre code.language-yaml, pre code.language-yml'); 63 | 64 | yamlBlocks.forEach(block => { 65 | const text = block.textContent; 66 | const highlighted = text 67 | .replace(/(\w+):/g, '$1:') 68 | .replace(/(\d+)/g, '$1') 69 | .replace(/"([^"]*)"/g, '"$1"') 70 | .replace(/'([^']*)'/g, '\'$1\''); 71 | 72 | block.innerHTML = highlighted; 73 | }); 74 | } 75 | 76 | // Add search functionality 77 | function addSearch() { 78 | const searchBox = document.createElement('div'); 79 | searchBox.innerHTML = ` 80 |
81 | 83 |
84 |
85 | `; 86 | 87 | const content = document.querySelector('.markdown'); 88 | if (content) { 89 | content.insertBefore(searchBox, content.firstChild); 90 | 91 | const searchInput = document.getElementById('search-input'); 92 | const resultsDiv = document.getElementById('search-results'); 93 | 94 | searchInput.addEventListener('input', (e) => { 95 | const query = e.target.value.toLowerCase(); 96 | if (query.length < 2) { 97 | resultsDiv.innerHTML = ''; 98 | return; 99 | } 100 | 101 | const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); 102 | const results = []; 103 | 104 | headings.forEach(heading => { 105 | const text = heading.textContent.toLowerCase(); 106 | if (text.includes(query)) { 107 | results.push({ 108 | text: heading.textContent, 109 | level: parseInt(heading.tagName.charAt(1)), 110 | element: heading 111 | }); 112 | } 113 | }); 114 | 115 | if (results.length > 0) { 116 | resultsDiv.innerHTML = ` 117 |

Search Results:

118 | 127 | `; 128 | } else { 129 | resultsDiv.innerHTML = '

No results found.

'; 130 | } 131 | }); 132 | } 133 | } 134 | 135 | // Add table of contents 136 | function addTableOfContents() { 137 | const headings = document.querySelectorAll('h1, h2, h3'); 138 | if (headings.length < 3) return; 139 | 140 | const toc = document.createElement('div'); 141 | toc.innerHTML = ` 142 |
143 |

Table of Contents

144 |
    145 |
    146 | `; 147 | 148 | const content = document.querySelector('.markdown'); 149 | if (content) { 150 | content.insertBefore(toc, content.firstChild); 151 | 152 | const tocList = document.getElementById('toc-list'); 153 | 154 | headings.forEach((heading, index) => { 155 | if (!heading.id) { 156 | heading.id = `heading-${index}`; 157 | } 158 | 159 | const li = document.createElement('li'); 160 | const indent = (parseInt(heading.tagName.charAt(1)) - 1) * 20; 161 | li.style.marginLeft = `${indent}px`; 162 | li.style.marginBottom = '4px'; 163 | 164 | const link = document.createElement('a'); 165 | link.href = `#${heading.id}`; 166 | link.textContent = heading.textContent; 167 | link.style.color = '#5865f2'; 168 | link.style.textDecoration = 'none'; 169 | 170 | li.appendChild(link); 171 | tocList.appendChild(li); 172 | }); 173 | } 174 | } 175 | 176 | // Add dark mode toggle 177 | function addDarkModeToggle() { 178 | const toggle = document.createElement('button'); 179 | toggle.innerHTML = '🌙'; 180 | toggle.style.cssText = ` 181 | position: fixed; 182 | top: 20px; 183 | right: 20px; 184 | background: #5865f2; 185 | color: white; 186 | border: none; 187 | border-radius: 50%; 188 | width: 40px; 189 | height: 40px; 190 | cursor: pointer; 191 | z-index: 1000; 192 | font-size: 16px; 193 | `; 194 | 195 | document.body.appendChild(toggle); 196 | 197 | let isDark = localStorage.getItem('darkMode') === 'true'; 198 | if (isDark) { 199 | document.body.classList.add('dark-mode'); 200 | toggle.innerHTML = '☀️'; 201 | } 202 | 203 | toggle.addEventListener('click', () => { 204 | isDark = !isDark; 205 | document.body.classList.toggle('dark-mode', isDark); 206 | toggle.innerHTML = isDark ? '☀️' : '🌙'; 207 | localStorage.setItem('darkMode', isDark); 208 | }); 209 | } 210 | 211 | // Add dark mode styles 212 | function addDarkModeStyles() { 213 | const style = document.createElement('style'); 214 | style.textContent = ` 215 | .dark-mode { 216 | background-color: #1a1a1a !important; 217 | color: #ffffff !important; 218 | } 219 | 220 | .dark-mode .markdown { 221 | background-color: #1a1a1a !important; 222 | color: #ffffff !important; 223 | } 224 | 225 | .dark-mode .book-summary { 226 | background-color: #2d2d2d !important; 227 | color: #ffffff !important; 228 | } 229 | 230 | .dark-mode pre { 231 | background-color: #2d2d2d !important; 232 | color: #ffffff !important; 233 | } 234 | 235 | .dark-mode code { 236 | background-color: #2d2d2d !important; 237 | color: #ff6b6b !important; 238 | } 239 | `; 240 | document.head.appendChild(style); 241 | } 242 | 243 | // Initialize all features 244 | function init() { 245 | console.log('GitCord documentation loaded'); 246 | 247 | // Wait for DOM to be ready 248 | if (document.readyState === 'loading') { 249 | document.addEventListener('DOMContentLoaded', init); 250 | return; 251 | } 252 | 253 | addCopyButtons(); 254 | highlightYAML(); 255 | addSearch(); 256 | addTableOfContents(); 257 | addDarkModeStyles(); 258 | addDarkModeToggle(); 259 | 260 | // Add smooth scrolling for anchor links 261 | document.querySelectorAll('a[href^="#"]').forEach(anchor => { 262 | anchor.addEventListener('click', function (e) { 263 | e.preventDefault(); 264 | const target = document.querySelector(this.getAttribute('href')); 265 | if (target) { 266 | target.scrollIntoView({ 267 | behavior: 'smooth', 268 | block: 'start' 269 | }); 270 | } 271 | }); 272 | }); 273 | } 274 | 275 | // Start initialization 276 | init(); 277 | })(); -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GitCord 2 | 3 | Thank you for your interest in contributing to GitCord! This document provides guidelines and information for contributors. 4 | 5 | ## Table of Contents 6 | 7 | - [Code of Conduct](#code-of-conduct) 8 | - [Project Overview](#project-overview) 9 | - [Development Setup](#development-setup) 10 | - [Contribution Workflow](#contribution-workflow) 11 | - [Coding Standards](#coding-standards) 12 | - [Testing](#testing) 13 | - [Documentation](#documentation) 14 | - [Issue Guidelines](#issue-guidelines) 15 | - [Pull Request Guidelines](#pull-request-guidelines) 16 | - [Release Process](#release-process) 17 | 18 | ## Code of Conduct 19 | 20 | This project is governed by the GNU General Public License v3.0. By participating, you are expected to uphold this license and contribute to a respectful, inclusive community. 21 | 22 | ## Project Overview 23 | 24 | GitCord is a Discord bot that integrates with GitHub to manage Discord server configurations through version-controlled configuration files. The project is currently in early development (Alpha stage) and uses: 25 | 26 | - **Python 3.9+** with Discord.py 27 | - **YAML** for configuration files 28 | - **GitOps** approach for server management 29 | - **GPL-3.0** license 30 | 31 | ### Current Status 32 | 33 | ⚠️ **Important**: This project is currently in early development. The bot exists as a basic framework with limited functionality 34 | 35 | **Do not run this on servers with untrusted members present. It is not secure yet.** 36 | 37 | ## Development Setup 38 | 39 | ### Prerequisites 40 | 41 | - Python 3.9 or higher 42 | - UV installed for python 43 | - Git 44 | - A Discord bot token (for testing) 45 | - GitHub account (for contributing) 46 | 47 | ### Local Development Setup 48 | 49 | 1. **Clone the repository** 50 | ```bash 51 | git clone https://github.com/evolvewithevan/gitcord.git 52 | cd gitcord 53 | ``` 54 | 55 | 2. **Set up a virtual environment** 56 | ```bash 57 | python -m venv venv 58 | source venv/bin/activate # On Windows: venv\Scripts\activate 59 | ``` 60 | 61 | 3. **Install dependencies** 62 | ```bash 63 | pip install -r requirements.txt 64 | # Or using uv (recommended) 65 | uv sync 66 | ``` 67 | 68 | 4. **Set up environment variables** 69 | ```bash 70 | cp .env.example .env # If .env.example exists 71 | # Edit .env with your Discord bot token and other settings 72 | ``` 73 | 74 | 5. **Run the bot** 75 | ```bash 76 | python -m gitcord 77 | # Or using the installed script 78 | gitcord 79 | ``` 80 | 81 | ### Project Structure 82 | 83 | ``` 84 | gitcord/ 85 | ├── src/gitcord/ # Main source code 86 | │ ├── bot.py # Main bot entry point 87 | │ ├── config.py # Configuration management 88 | │ ├── events.py # Discord event handlers 89 | │ ├── cogs/ # Discord.py cogs (command modules) 90 | │ ├── utils/ # Utility functions 91 | │ ├── views/ # Discord UI components 92 | │ └── constants/ # Constants and messages 93 | ├── gitcord-template/ # Template repository structure 94 | ├── requirements.txt # Python dependencies 95 | ├── pyproject.toml # Project metadata and build config 96 | └── README.md # Project documentation 97 | ``` 98 | 99 | ## Contribution Workflow 100 | 101 | ### 1. Fork and Clone 102 | 103 | 1. Fork the repository on GitHub 104 | 2. Clone your fork locally 105 | 3. Add the upstream repository as a remote: 106 | ```bash 107 | git remote add upstream https://github.com/evolvewithevan/gitcord.git 108 | ``` 109 | 110 | ### 2. Create a Feature Branch 111 | 112 | ```bash 113 | git checkout -b feature/your-feature-name 114 | # or 115 | git checkout -b fix/your-bug-fix 116 | ``` 117 | 118 | ### 3. Make Your Changes 119 | 120 | - Follow the [coding standards](#coding-standards) 121 | - Write tests for new functionality 122 | - Update documentation as needed 123 | - Keep commits atomic and well-described 124 | 125 | ### 4. Test Your Changes 126 | 127 | ```bash 128 | # Run linting 129 | pylint src/gitcord/ 130 | 131 | # Run tests (when available) 132 | python -m pytest 133 | 134 | # Test the bot locally 135 | python -m gitcord 136 | ``` 137 | 138 | ### 5. Commit Your Changes 139 | 140 | ```bash 141 | git add . 142 | git commit -m "feat: add new feature description" 143 | ``` 144 | 145 | Use conventional commit messages: 146 | - `feat:` for new features 147 | - `fix:` for bug fixes 148 | - `docs:` for documentation changes 149 | - `style:` for formatting changes 150 | - `refactor:` for code refactoring 151 | - `test:` for adding tests 152 | - `chore:` for maintenance tasks 153 | 154 | ### 6. Push and Create Pull Request 155 | 156 | ```bash 157 | git push origin feature/your-feature-name 158 | ``` 159 | 160 | Then create a Pull Request on GitHub with a clear description of your changes. 161 | 162 | ## Coding Standards 163 | 164 | ### Python Style Guide 165 | 166 | - Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guidelines 167 | - Use type hints where appropriate 168 | - Maximum line length: 88 characters (Black formatter default) 169 | - Use meaningful variable and function names 170 | 171 | ### Discord.py Best Practices 172 | 173 | - Use Discord.py 2.5.2+ features 174 | - Implement proper error handling for Discord API calls 175 | - Use slash commands where possible 176 | - Follow Discord's rate limiting guidelines 177 | 178 | ### Code Organization 179 | 180 | - Keep cogs focused on specific functionality 181 | - Use utility functions for reusable code 182 | - Maintain separation of concerns 183 | - Document complex functions and classes 184 | 185 | ### Example Code Structure 186 | 187 | ```python 188 | """ 189 | Module docstring explaining the purpose. 190 | """ 191 | 192 | from typing import Optional 193 | import discord 194 | from discord.ext import commands 195 | 196 | 197 | class ExampleCog(commands.Cog): 198 | """Example cog demonstrating coding standards.""" 199 | 200 | def __init__(self, bot: commands.Bot) -> None: 201 | self.bot = bot 202 | 203 | @commands.slash_command(name="example") 204 | async def example_command( 205 | self, 206 | ctx: discord.ApplicationContext, 207 | parameter: str 208 | ) -> None: 209 | """ 210 | Example slash command. 211 | 212 | Args: 213 | ctx: Discord application context 214 | parameter: Example parameter 215 | """ 216 | try: 217 | # Command logic here 218 | await ctx.respond(f"Example response: {parameter}") 219 | except Exception as e: 220 | await ctx.respond(f"Error: {e}", ephemeral=True) 221 | 222 | 223 | def setup(bot: commands.Bot) -> None: 224 | """Add the cog to the bot.""" 225 | bot.add_cog(ExampleCog(bot)) 226 | ``` 227 | 228 | ## Testing 229 | 230 | ### Running Tests 231 | 232 | ```bash 233 | # Run all tests 234 | python -m pytest 235 | 236 | # Run tests with coverage 237 | python -m pytest --cov=gitcord 238 | 239 | # Run specific test file 240 | python -m pytest tests/test_specific_module.py 241 | ``` 242 | 243 | ### Writing Tests 244 | 245 | - Write tests for new functionality 246 | - Use descriptive test names 247 | - Mock external dependencies (Discord API, GitHub API) 248 | - Aim for good test coverage 249 | 250 | ### Example Test 251 | 252 | ```python 253 | """Test example cog functionality.""" 254 | 255 | import pytest 256 | from unittest.mock import AsyncMock, MagicMock 257 | import discord 258 | from gitcord.cogs.example import ExampleCog 259 | 260 | 261 | @pytest.fixture 262 | def mock_context(): 263 | """Create a mock Discord context.""" 264 | context = MagicMock(spec=discord.ApplicationContext) 265 | context.respond = AsyncMock() 266 | return context 267 | 268 | 269 | @pytest.mark.asyncio 270 | async def test_example_command(mock_context): 271 | """Test the example command.""" 272 | bot = MagicMock() 273 | cog = ExampleCog(bot) 274 | 275 | await cog.example_command(mock_context, "test_parameter") 276 | 277 | mock_context.respond.assert_called_once_with( 278 | "Example response: test_parameter" 279 | ) 280 | ``` 281 | 282 | ## Documentation 283 | 284 | ### Code Documentation 285 | 286 | - Use docstrings for all public functions and classes 287 | - Follow Google or NumPy docstring format 288 | - Include type hints 289 | - Document exceptions that may be raised 290 | 291 | ### README Updates 292 | 293 | - Update README.md for significant changes 294 | - Keep installation instructions current 295 | - Document new features and configuration options 296 | 297 | ### API Documentation 298 | 299 | - Document Discord bot commands and their usage 300 | - Explain configuration file formats 301 | - Provide examples for common use cases 302 | 303 | ## Issue Guidelines 304 | 305 | ### Before Creating an Issue 306 | 307 | 1. Check existing issues for duplicates 308 | 2. Search the documentation for solutions 309 | 3. Try to reproduce the issue locally 310 | 311 | ### Issue Template 312 | 313 | When creating an issue, include: 314 | 315 | - **Title**: Clear, descriptive title 316 | - **Description**: Detailed description of the problem 317 | - **Steps to Reproduce**: Step-by-step instructions 318 | - **Expected vs Actual Behavior**: What you expected vs what happened 319 | - **Environment**: Python version, OS, Discord.py version 320 | - **Additional Context**: Screenshots, logs, etc. 321 | 322 | ### Issue Labels 323 | 324 | - `bug`: Something isn't working 325 | - `enhancement`: New feature or request 326 | - `documentation`: Improvements or additions to documentation 327 | - `good first issue`: Good for newcomers 328 | - `help wanted`: Extra attention is needed 329 | 330 | ## Pull Request Guidelines 331 | 332 | ### Before Submitting 333 | 334 | 1. Ensure all tests pass 335 | 2. Update documentation if needed 336 | 3. Follow the coding standards 337 | 4. Test your changes thoroughly 338 | 339 | ### PR Template 340 | 341 | - **Title**: Clear, descriptive title 342 | - **Description**: Explain what the PR does and why 343 | - **Related Issues**: Link to related issues 344 | - **Type of Change**: Bug fix, feature, documentation, etc. 345 | - **Testing**: How you tested your changes 346 | - **Checklist**: Ensure all requirements are met 347 | 348 | ### PR Review Process 349 | 350 | 1. Automated checks must pass (linting, tests) 351 | 2. Code review by maintainers 352 | 3. Address feedback and make requested changes 353 | 4. Maintainers will merge when approved 354 | 355 | ## Release Process 356 | 357 | ### Versioning 358 | 359 | This project follows [Semantic Versioning](https://semver.org/): 360 | - `MAJOR.MINOR.PATCH` 361 | - Current version: `0.6.0` (Alpha) 362 | 363 | ### Release Checklist 364 | 365 | - [ ] All tests pass 366 | - [ ] Documentation is updated 367 | - [ ] CHANGELOG.md is updated 368 | - [ ] Version is bumped in `pyproject.toml` 369 | - [ ] Release notes are prepared 370 | 371 | ## Getting Help 372 | 373 | ### Communication Channels 374 | 375 | - **GitHub Issues**: For bug reports and feature requests 376 | - **GitHub Discussions**: For questions and general discussion 377 | - **Pull Requests**: For code contributions 378 | 379 | ### Resources 380 | 381 | - [Discord.py Documentation](https://discordpy.readthedocs.io/) 382 | - [Discord Developer Portal](https://discord.com/developers/docs) 383 | - [GitHub API Documentation](https://docs.github.com/en/rest) 384 | 385 | ## License 386 | 387 | By contributing to GitCord, you agree that your contributions will be licensed under the GNU General Public License v3.0. 388 | 389 | --- 390 | 391 | Thank you for contributing to GitCord! Your contributions help make this project better for everyone. -------------------------------------------------------------------------------- /src/gitcord/utils/category_helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Category management utilities for GitCord bot. 3 | """ 4 | 5 | from dataclasses import dataclass 6 | from typing import List, Optional 7 | 8 | import discord 9 | 10 | from .helpers import create_embed, parse_channel_config 11 | from .logger import main_logger as logger 12 | 13 | import requests 14 | 15 | 16 | @dataclass 17 | class CategoryResult: 18 | """Result of category processing operations.""" 19 | 20 | created_channels: List[discord.abc.GuildChannel] 21 | updated_channels: List[discord.abc.GuildChannel] 22 | skipped_channels: List[str] 23 | extra_channels: List[discord.abc.GuildChannel] 24 | category_updated: bool 25 | 26 | 27 | async def process_existing_category( 28 | guild: discord.Guild, 29 | category_config: dict, 30 | existing_category: discord.CategoryChannel, 31 | category_position: int = None, 32 | ) -> CategoryResult: 33 | """ 34 | Process an existing category and update/create channels as needed. 35 | 36 | Args: 37 | guild: The Discord guild 38 | category_config: The category configuration from YAML 39 | existing_category: The existing category channel 40 | category_position: The desired position of the category based on YAML order 41 | 42 | Returns: 43 | CategoryResult with processing results 44 | """ 45 | yaml_channel_names = _get_yaml_channel_names(category_config) 46 | extra_channels = _find_extra_channels( 47 | existing_category, yaml_channel_names, category_config["name"] 48 | ) 49 | 50 | # Update category position if needed (smart positioning) 51 | category_updated = False 52 | if category_position is not None: 53 | # Get all categories in guild sorted by position 54 | guild_categories = sorted(guild.categories, key=lambda cat: cat.position) 55 | current_relative_index = guild_categories.index(existing_category) 56 | 57 | # Only move if the category is not at the correct relative position 58 | if current_relative_index != category_position: 59 | try: 60 | await existing_category.edit(position=category_position) 61 | logger.info( 62 | "Updated category '%s' position to %d", 63 | category_config["name"], 64 | category_position, 65 | ) 66 | category_updated = True 67 | except (discord.Forbidden, discord.HTTPException) as e: 68 | logger.warning("Failed to update category position: %s", e) 69 | 70 | # Process channels in the category 71 | created_channels, updated_channels, skipped_channels = ( 72 | await _process_category_channels(guild, existing_category, category_config) 73 | ) 74 | 75 | return CategoryResult( 76 | created_channels=created_channels, 77 | updated_channels=updated_channels, 78 | skipped_channels=skipped_channels, 79 | extra_channels=extra_channels, 80 | category_updated=category_updated, 81 | ) 82 | 83 | 84 | def _get_yaml_channel_names(category_config: dict) -> set: 85 | """Get set of channel names from YAML configuration.""" 86 | yaml_channel_names = set() 87 | for channel_name in category_config["channels"]: 88 | try: 89 | channel_yaml_path = f"{channel_name}.yaml" 90 | channel_config = parse_channel_config(channel_yaml_path) 91 | yaml_channel_names.add(channel_config["name"]) 92 | except (ValueError, FileNotFoundError) as e: 93 | logger.error( 94 | "Failed to parse channel '%s' from YAML: %s", 95 | channel_name, 96 | e, 97 | ) 98 | return yaml_channel_names 99 | 100 | 101 | def _find_extra_channels( 102 | existing_category: discord.CategoryChannel, 103 | yaml_channel_names: set, 104 | category_name: str, 105 | ) -> List[discord.abc.GuildChannel]: 106 | """Find channels that exist in Discord but not in YAML.""" 107 | extra_channels = [] 108 | for existing_channel in existing_category.channels: 109 | if existing_channel.name not in yaml_channel_names: 110 | extra_channels.append(existing_channel) 111 | logger.info( 112 | "Found extra channel '%s' in category '%s' (not in YAML)", 113 | existing_channel.name, 114 | category_name, 115 | ) 116 | return extra_channels 117 | 118 | 119 | async def _process_category_channels( 120 | guild: discord.Guild, 121 | existing_category: discord.CategoryChannel, 122 | category_config: dict, 123 | ) -> tuple[ 124 | List[discord.abc.GuildChannel], List[discord.abc.GuildChannel], List[str] 125 | ]: 126 | """Process channels in the category.""" 127 | created_channels = [] 128 | updated_channels = [] 129 | skipped_channels = [] 130 | 131 | channels = category_config.get("channels", []) 132 | for channel_index, channel_name in enumerate(channels): 133 | try: 134 | channel_yaml_path = f"{channel_name}.yaml" 135 | channel_config = parse_channel_config(channel_yaml_path) 136 | 137 | existing_channel = discord.utils.get( 138 | existing_category.channels, name=channel_config["name"] 139 | ) 140 | 141 | if existing_channel: 142 | channel_updated = await _update_existing_channel( 143 | existing_channel, channel_config, category_config["name"], channel_index 144 | ) 145 | if channel_updated: 146 | updated_channels.append(existing_channel) 147 | else: 148 | skipped_channels.append(channel_config["name"]) 149 | else: 150 | new_channel = await _create_channel_in_category( 151 | guild, channel_config, existing_category, category_config["name"], channel_index 152 | ) 153 | if new_channel: 154 | created_channels.append(new_channel) 155 | 156 | except (ValueError, FileNotFoundError) as e: 157 | logger.error( 158 | "Failed to parse channel '%s' from YAML: %s", 159 | channel_name, 160 | e, 161 | ) 162 | skipped_channels.append(channel_name) 163 | 164 | return created_channels, updated_channels, skipped_channels 165 | 166 | 167 | async def _update_existing_channel( 168 | existing_channel: discord.abc.GuildChannel, 169 | channel_config: dict, 170 | category_name: str, 171 | channel_position: int = None, 172 | ) -> bool: 173 | """Update an existing channel with new configuration.""" 174 | channel_updated = False 175 | update_kwargs = {} 176 | 177 | # Check if topic needs updating (text channels only) 178 | if ( 179 | channel_config["type"].lower() == "text" 180 | and hasattr(existing_channel, "topic") 181 | and existing_channel.topic != channel_config.get("topic", "") 182 | ): 183 | update_kwargs["topic"] = channel_config.get("topic", "") 184 | channel_updated = True 185 | 186 | # Check if NSFW setting needs updating 187 | if ( 188 | hasattr(existing_channel, "nsfw") 189 | and existing_channel.nsfw != channel_config.get("nsfw", False) 190 | ): 191 | update_kwargs["nsfw"] = channel_config.get("nsfw", False) 192 | channel_updated = True 193 | 194 | # Check if position needs updating based on YAML order (smart positioning) 195 | if ( 196 | channel_position is not None 197 | and hasattr(existing_channel, "position") 198 | ): 199 | # Get all channels in category sorted by position 200 | category_channels = sorted(existing_channel.category.channels, key=lambda ch: ch.position) 201 | current_relative_index = category_channels.index(existing_channel) 202 | 203 | # Only move if the channel is not at the correct relative position 204 | if current_relative_index != channel_position: 205 | update_kwargs["position"] = channel_position 206 | channel_updated = True 207 | 208 | if channel_updated: 209 | try: 210 | await existing_channel.edit(**update_kwargs) 211 | position_msg = f" (moved to position {channel_position})" if channel_position is not None and "position" in update_kwargs else "" 212 | logger.info( 213 | "Updated channel '%s' in category '%s'%s", 214 | channel_config["name"], 215 | category_name, 216 | position_msg, 217 | ) 218 | return True 219 | except (discord.Forbidden, discord.HTTPException) as e: 220 | logger.error( 221 | "Failed to update channel '%s': %s", 222 | channel_config["name"], 223 | e, 224 | ) 225 | 226 | return False 227 | 228 | 229 | async def _create_channel_in_category( 230 | guild: discord.Guild, 231 | channel_config: dict, 232 | category: discord.CategoryChannel, 233 | category_name: str, 234 | channel_position: int = None, 235 | ) -> Optional[discord.abc.GuildChannel]: 236 | """Create a new channel in the specified category.""" 237 | try: 238 | channel_kwargs = { 239 | "name": channel_config["name"], 240 | "category": category, 241 | } 242 | 243 | # Add topic if specified 244 | if "topic" in channel_config: 245 | channel_kwargs["topic"] = channel_config["topic"] 246 | 247 | # Add NSFW setting if specified 248 | if "nsfw" in channel_config: 249 | channel_kwargs["nsfw"] = channel_config["nsfw"] 250 | 251 | # Add position if specified (for YAML order-based positioning) 252 | if channel_position is not None: 253 | channel_kwargs["position"] = channel_position 254 | 255 | channel_type = channel_config["type"].lower() 256 | 257 | if channel_type == "text": 258 | new_channel = await guild.create_text_channel(**channel_kwargs) 259 | elif channel_type == "voice": 260 | # Voice channels don't support topic, so remove it if present 261 | if "topic" in channel_kwargs: 262 | del channel_kwargs["topic"] 263 | new_channel = await guild.create_voice_channel(**channel_kwargs) 264 | else: 265 | logger.error("Unknown channel type: %s", channel_type) 266 | return None 267 | 268 | position_msg = f" at position {channel_position}" if channel_position is not None else "" 269 | logger.info( 270 | "Created %s channel '%s' in category '%s'%s", 271 | channel_type, 272 | channel_config["name"], 273 | category_name, 274 | position_msg, 275 | ) 276 | return new_channel 277 | 278 | except (discord.Forbidden, discord.HTTPException) as e: 279 | logger.error( 280 | "Failed to create channel '%s' in category '%s': %s", 281 | channel_config["name"], 282 | category_name, 283 | e, 284 | ) 285 | return None 286 | 287 | 288 | def create_category_result_embed( 289 | category_name: str, 290 | result: CategoryResult, 291 | ) -> discord.Embed: 292 | """Create an embed showing category processing results.""" 293 | embed = create_embed( 294 | title="✅ Category Processed", 295 | description=f"Successfully processed category: **{category_name}**", 296 | color=discord.Color.green(), 297 | ) 298 | 299 | # Add fields 300 | embed.add_field(name="Category", value=f"**{category_name}**", inline=True) 301 | if result.category_updated: 302 | embed.add_field(name="Category Updated", value="✅ Position", inline=True) 303 | else: 304 | embed.add_field(name="Category Updated", value="❌ No changes", inline=True) 305 | 306 | embed.add_field( 307 | name="Channels Created", 308 | value=str(len(result.created_channels)), 309 | inline=True, 310 | ) 311 | embed.add_field( 312 | name="Channels Updated", 313 | value=str(len(result.updated_channels)), 314 | inline=True, 315 | ) 316 | embed.add_field( 317 | name="Channels Skipped", 318 | value=str(len(result.skipped_channels)), 319 | inline=True, 320 | ) 321 | embed.add_field( 322 | name="Extra Channels", value=str(len(result.extra_channels)), inline=True 323 | ) 324 | 325 | if result.created_channels: 326 | channel_list = "\n".join( 327 | [f"• {channel.mention} (new)" for channel in result.created_channels] 328 | ) 329 | embed.add_field(name="New Channels", value=channel_list, inline=False) 330 | 331 | if result.updated_channels: 332 | channel_list = "\n".join( 333 | [f"• {channel.mention} (updated)" for channel in result.updated_channels] 334 | ) 335 | embed.add_field(name="Updated Channels", value=channel_list, inline=False) 336 | 337 | if result.extra_channels: 338 | channel_list = "\n".join( 339 | [f"• {channel.mention} (not in YAML)" for channel in result.extra_channels] 340 | ) 341 | embed.add_field( 342 | name="Extra Channels (Not in YAML)", 343 | value=channel_list, 344 | inline=False, 345 | ) 346 | 347 | return embed 348 | 349 | 350 | async def handle_category_response( 351 | interaction: discord.Interaction, 352 | embed: discord.Embed, 353 | extra_channels: List[discord.abc.GuildChannel], 354 | category_name: str, 355 | ) -> None: 356 | """Handle the response for category processing.""" 357 | if extra_channels: 358 | delete_view = DeleteExtraChannelsView(extra_channels, category_name) 359 | await interaction.followup.send(embed=embed, view=delete_view) 360 | else: 361 | await interaction.followup.send(embed=embed) 362 | -------------------------------------------------------------------------------- /docs/src/templates/category-templates.md: -------------------------------------------------------------------------------- 1 | # Category Templates 2 | 3 | Category templates in GitCord allow you to create standardized category configurations that group related channels together. This guide covers how to create, use, and manage category templates. 4 | 5 | ## What are Category Templates? 6 | 7 | Category templates are YAML configuration files that define Discord categories and their associated channels. They provide a structured way to organize server content by grouping related channels together under logical categories. 8 | 9 | ## Template Structure 10 | 11 | ### Basic Category Template 12 | 13 |
    14 | Basic Category Template Example 15 | 16 | A category template consists of a YAML file with category configuration: 17 | 18 | ```yaml 19 | name: Community 20 | position: 0 21 | type: category 22 | 23 | channels: 24 | - general 25 | - introductions 26 | - memes 27 | - off-topic 28 | ``` 29 | 30 |
    31 | 32 | ### Required Fields 33 | 34 | Every category template must include these fields: 35 | 36 | - **`name`**: The display name of the category 37 | - **`position`**: The position of the category in the server sidebar (0 = top) 38 | - **`type`**: Must be "category" 39 | - **`channels`**: List of channel file names (without .yaml extension) 40 | 41 | ### Optional Fields 42 | 43 |
    44 | Optional Fields Example 45 | 46 | You can include additional fields for advanced configuration: 47 | 48 | ```yaml 49 | name: Community 50 | position: 0 51 | type: category 52 | description: "Community discussion channels" 53 | 54 | channels: 55 | - general 56 | - introductions 57 | - memes 58 | - off-topic 59 | 60 | # Advanced options (future features) 61 | permissions: 62 | - role: "@everyone" 63 | allow: ["view_channel", "send_messages"] 64 | - role: "Moderator" 65 | allow: ["manage_messages"] 66 | ``` 67 | 68 |
    69 | 70 | ## Creating Category Templates 71 | 72 | ### Step 1: Plan Your Category 73 | 74 | Before creating a template, consider: 75 | 76 | 1. **Purpose**: What is the category for? 77 | 2. **Channels**: What channels should be included? 78 | 3. **Organization**: How should channels be ordered? 79 | 4. **Permissions**: What permissions should be set? 80 | 81 | ### Step 2: Create Channel Templates 82 | 83 |
    84 | Channel Template Example: general.yaml 85 | 86 | First, create individual channel templates for each channel in the category: 87 | 88 | ```yaml 89 | # general.yaml 90 | name: general 91 | type: text 92 | position: 0 93 | topic: "General chat for everyone" 94 | nsfw: false 95 | ``` 96 | 97 |
    98 | 99 |
    100 | Channel Template Example: introductions.yaml 101 | 102 | ```yaml 103 | # introductions.yaml 104 | name: introductions 105 | type: text 106 | position: 1 107 | topic: "Introduce yourself to the community!" 108 | nsfw: false 109 | ``` 110 | 111 |
    112 | 113 | ### Step 3: Create Category Template 114 | 115 |
    116 | Category Template Example: community.yaml 117 | 118 | Create the category template that references the channel files: 119 | 120 | ```yaml 121 | # community.yaml 122 | name: Community 123 | position: 0 124 | type: category 125 | 126 | channels: 127 | - general 128 | - introductions 129 | - memes 130 | - off-topic 131 | ``` 132 | 133 |
    134 | 135 | ### Step 4: Test Your Template 136 | 137 | Test your category template: 138 | 139 | 1. **Validate YAML syntax** using a YAML validator 140 | 2. **Ensure all channel files exist** and are valid 141 | 3. **Test in a development server** first 142 | 4. **Check channel ordering** and positioning 143 | 144 | ## Template Examples 145 | 146 |
    147 | Community Category Example 148 | 149 | A typical community category with discussion channels: 150 | 151 | ```yaml 152 | # community.yaml 153 | name: Community 154 | position: 0 155 | type: category 156 | 157 | channels: 158 | - general 159 | - introductions 160 | - memes 161 | - off-topic 162 | - suggestions 163 | ``` 164 | 165 | **Associated Channel Files:** 166 | - `general.yaml` - Main chat channel 167 | - `introductions.yaml` - New member introductions 168 | - `memes.yaml` - Memes and humor 169 | - `off-topic.yaml` - Random discussions 170 | - `suggestions.yaml` - Community suggestions 171 | 172 |
    173 | 174 |
    175 | Voice Channels Category Example 176 | 177 | A category for voice communication: 178 | 179 | ```yaml 180 | # voice.yaml 181 | name: Voice Channels 182 | position: 1 183 | type: category 184 | 185 | channels: 186 | - general-voice 187 | - gaming 188 | - music 189 | - chill 190 | ``` 191 | 192 | **Associated Channel Files:** 193 | - `general-voice.yaml` - General voice chat 194 | - `gaming.yaml` - Gaming voice channel 195 | - `music.yaml` - Music listening 196 | - `chill.yaml` - Relaxed voice chat 197 | 198 |
    199 | 200 |
    201 | Administrative Category Example 202 | 203 | A category for server administration: 204 | 205 | ```yaml 206 | # admin.yaml 207 | name: Administrative 208 | position: 2 209 | type: category 210 | 211 | channels: 212 | - announcements 213 | - rules 214 | - staff-chat 215 | - logs 216 | ``` 217 | 218 | **Associated Channel Files:** 219 | - `announcements.yaml` - Server announcements 220 | - `rules.yaml` - Server rules 221 | - `staff-chat.yaml` - Staff discussions 222 | - `logs.yaml` - Bot logs 223 | 224 |
    225 | 226 |
    227 | Gaming Category Example 228 | 229 | A category for gaming-related channels: 230 | 231 | ```yaml 232 | # gaming.yaml 233 | name: Gaming 234 | position: 3 235 | type: category 236 | 237 | channels: 238 | - general-gaming 239 | - minecraft 240 | - valorant 241 | - league-of-legends 242 | - gaming-news 243 | ``` 244 | 245 | **Associated Channel Files:** 246 | - `general-gaming.yaml` - General gaming discussion 247 | - `minecraft.yaml` - Minecraft-specific chat 248 | - `valorant.yaml` - Valorant discussion 249 | - `league-of-legends.yaml` - LoL discussion 250 | - `gaming-news.yaml` - Gaming news and updates 251 | 252 |
    253 | 254 | ## Using Category Templates 255 | 256 | ### Single Category Creation 257 | 258 | Use the `!createcategory` or `/createcategory` command: 259 | 260 | 1. **Run the command**: `!createcategory` or `/createcategory` 261 | 2. **Provide the template path**: Enter the path to your category YAML file 262 | 3. **Review the results**: Check the bot's response for success/errors 263 | 264 | ### Multiple Categories 265 | 266 |
    267 | Multiple Category Creation Example (Bash) 268 | 269 | Create multiple categories by running the command for each: 270 | 271 | ```bash 272 | # Create community category 273 | !createcategory community.yaml 274 | 275 | # Create voice category 276 | !createcategory voice.yaml 277 | 278 | # Create admin category 279 | !createcategory admin.yaml 280 | ``` 281 | 282 |
    283 | 284 | ### Template Organization 285 | 286 |
    287 | Template Organization Example 288 | 289 | Organize your templates logically: 290 | 291 | ``` 292 | templates/ 293 | ├── categories/ 294 | │ ├── community.yaml 295 | │ ├── voice.yaml 296 | │ ├── admin.yaml 297 | │ └── gaming.yaml 298 | └── channels/ 299 | ├── community/ 300 | │ ├── general.yaml 301 | │ ├── introductions.yaml 302 | │ └── memes.yaml 303 | ├── voice/ 304 | │ ├── general-voice.yaml 305 | │ └── gaming.yaml 306 | └── admin/ 307 | ├── announcements.yaml 308 | └── rules.yaml 309 | ``` 310 | 311 |
    312 | 313 | ## Advanced Templates 314 | 315 |
    316 | Conditional Channels Example 317 | 318 | Create templates that adapt based on server needs: 319 | 320 | ```yaml 321 | # flexible-community.yaml 322 | name: Community 323 | position: 0 324 | type: category 325 | 326 | channels: 327 | - general 328 | - introductions 329 | # Optional channels based on server size 330 | - memes # For larger servers 331 | - off-topic # For larger servers 332 | - suggestions # For active communities 333 | ``` 334 | 335 |
    336 | 337 |
    338 | Template Inheritance Example 339 | 340 | Create base templates that can be extended: 341 | 342 | ```yaml 343 | # base-category.yaml 344 | type: category 345 | description: "Base category template" 346 | 347 | # community.yaml (extends base-category.yaml) 348 | name: Community 349 | position: 0 350 | channels: 351 | - general 352 | - introductions 353 | ``` 354 | 355 |
    356 | 357 |
    358 | Dynamic Positioning Example 359 | 360 | Use relative positioning for flexible layouts: 361 | 362 | ```yaml 363 | # community.yaml 364 | name: Community 365 | position: 0 366 | type: category 367 | 368 | channels: 369 | - general # position: 0 370 | - introductions # position: 1 371 | - memes # position: 2 372 | - off-topic # position: 3 373 | ``` 374 | 375 |
    376 | 377 | ## Best Practices 378 | 379 | ### Naming Conventions 380 | 381 | 1. **Use descriptive names**: Make category purposes clear 382 | 2. **Use consistent naming**: Follow a pattern across categories 383 | 3. **Avoid special characters**: Stick to letters, numbers, and spaces 384 | 4. **Keep names concise**: But informative 385 | 386 | ### Organization 387 | 388 | 1. **Group related channels**: Put similar channels together 389 | 2. **Use logical positioning**: Order categories by importance 390 | 3. **Consider user experience**: Make navigation intuitive 391 | 4. **Plan for growth**: Leave room for future categories 392 | 393 | ### Channel Management 394 | 395 | 1. **Limit channels per category**: Don't overload categories 396 | 2. **Use consistent channel ordering**: Within categories 397 | 3. **Set appropriate permissions**: For category and channels 398 | 4. **Regular maintenance**: Update categories as needed 399 | 400 | ## Template Validation 401 | 402 | ### YAML Syntax 403 | 404 |
    405 | YAML Syntax Validation Example 406 | 407 | Ensure your YAML is valid: 408 | 409 | ```yaml 410 | # Valid YAML 411 | name: Community 412 | position: 0 413 | type: category 414 | 415 | channels: 416 | - general 417 | - introductions 418 | 419 | # Invalid YAML (incorrect indentation) 420 | name: Community 421 | position: 0 422 | type: category 423 | channels: 424 | - general 425 | - introductions 426 | ``` 427 | 428 |
    429 | 430 | ### Required Fields 431 | 432 |
    433 | Missing Required Field Example 434 | 435 | Check that all required fields are present: 436 | 437 | ```yaml 438 | # Missing required field 'channels' 439 | name: Community 440 | position: 0 441 | type: category 442 | # This will cause an error 443 | ``` 444 | 445 |
    446 | 447 | ### Channel File References 448 | 449 |
    450 | Channel File Reference Example 451 | 452 | Ensure all referenced channel files exist: 453 | 454 | ```yaml 455 | # community.yaml 456 | name: Community 457 | position: 0 458 | type: category 459 | 460 | channels: 461 | - general # Requires general.yaml 462 | - introductions # Requires introductions.yaml 463 | - nonexistent # This will cause an error 464 | ``` 465 | 466 |
    467 | 468 | ## Troubleshooting 469 | 470 | ### Common Issues 471 | 472 | 1. **"Invalid YAML" errors**: 473 | - Check YAML syntax and indentation 474 | - Use a YAML validator 475 | - Ensure proper field structure 476 | 477 | 2. **"Missing required field" errors**: 478 | - Verify all required fields are present 479 | - Check field names for typos 480 | - Ensure proper field hierarchy 481 | 482 | 3. **"Channel file not found" errors**: 483 | - Check that all channel files exist 484 | - Verify file paths are correct 485 | - Ensure channel files are valid YAML 486 | 487 | 4. **"Category already exists" errors**: 488 | - Check if category name conflicts 489 | - Use unique names for categories 490 | - Consider using the update feature 491 | 492 | ### Debug Tips 493 | 494 | 1. **Test with simple templates** first 495 | 2. **Use YAML validators** to check syntax 496 | 3. **Check file paths** are correct 497 | 4. **Review bot logs** for detailed errors 498 | 5. **Test in development** before production 499 | 500 | ## Template Sharing 501 | 502 | ### Version Control 503 | 504 |
    505 | Version Control Example (Bash) 506 | 507 | Store templates in version control: 508 | 509 | ```bash 510 | # Add templates to git 511 | git add templates/ 512 | git commit -m "Add category templates" 513 | git push origin main 514 | ``` 515 | 516 |
    517 | 518 | ### Template Repositories 519 | 520 | Share templates with the community: 521 | 522 | 1. **Create a template repository** 523 | 2. **Document your templates** 524 | 3. **Provide examples** 525 | 4. **Accept contributions** 526 | 527 | ### Template Standards 528 | 529 | Follow community standards: 530 | 531 | 1. **Use consistent naming** 532 | 2. **Include documentation** 533 | 3. **Provide examples** 534 | 4. **Test thoroughly** 535 | 536 | ## Server Structure Examples 537 | 538 |
    539 | Server Structure: Small Community 540 | 541 | ``` 542 | 📁 Community (0) 543 | ├── # general 544 | ├── # introductions 545 | └── # off-topic 546 | 547 | 📁 Voice Channels (1) 548 | ├── 🔊 General Voice 549 | └── 🔊 Gaming Voice 550 | 551 | 📁 Administrative (2) 552 | ├── # announcements 553 | └── # rules 554 | ``` 555 | 556 |
    557 | 558 |
    559 | Server Structure: Medium Gaming 560 | 561 | ``` 562 | 📁 Community (0) 563 | ├── # general 564 | ├── # introductions 565 | ├── # memes 566 | └── # off-topic 567 | 568 | 📁 Gaming (1) 569 | ├── # general-gaming 570 | ├── # minecraft 571 | ├── # valorant 572 | └── # gaming-news 573 | 574 | 📁 Voice Channels (2) 575 | ├── 🔊 General Voice 576 | ├── 🔊 Gaming Voice 577 | └── 🔊 Music Voice 578 | 579 | 📁 Administrative (3) 580 | ├── # announcements 581 | ├── # rules 582 | └── # staff-chat 583 | ``` 584 | 585 |
    586 | 587 |
    588 | Server Structure: Large Community 589 | 590 | ``` 591 | 📁 Community (0) 592 | ├── # general 593 | ├── # introductions 594 | ├── # memes 595 | ├── # off-topic 596 | └── # suggestions 597 | 598 | 📁 Gaming (1) 599 | ├── # general-gaming 600 | ├── # minecraft 601 | ├── # valorant 602 | ├── # league-of-legends 603 | └── # gaming-news 604 | 605 | 📁 Voice Channels (2) 606 | ├── 🔊 General Voice 607 | ├── 🔊 Gaming Voice 608 | ├── 🔊 Music Voice 609 | └── 🔊 Chill Voice 610 | 611 | 📁 Events (3) 612 | ├── # events 613 | ├── # event-voice 614 | └── # event-announcements 615 | 616 | 📁 Administrative (4) 617 | ├── # announcements 618 | ├── # rules 619 | ├── # staff-chat 620 | └── # logs 621 | ``` 622 | 623 |
    624 | 625 | ## Future Enhancements 626 | 627 | ### Planned Features 628 | 629 | 1. **Category Permissions**: Set permissions at category level 630 | 2. **Template Variables**: Dynamic category values 631 | 3. **Template Inheritance**: Base category extension 632 | 4. **Category Nesting**: Nested category support 633 | 5. **Category Templates**: Reusable category patterns 634 | 635 | ### Extension Points 636 | 637 | 1. **Custom Category Types**: Support for new category features 638 | 2. **Advanced Properties**: More category configuration options 639 | 3. **Category Macros**: Reusable category components 640 | 4. **Category Import/Export**: Category sharing mechanisms 641 | 5. **Category Preview**: Visual category previews 642 | -------------------------------------------------------------------------------- /src/gitcord/utils/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper utilities for GitCord bot. 3 | """ 4 | 5 | import os 6 | import re 7 | from datetime import datetime 8 | from typing import Optional, Union, List 9 | 10 | import discord 11 | import yaml 12 | from discord.ext import commands 13 | 14 | 15 | def format_latency(latency: float) -> str: 16 | """ 17 | Format latency in milliseconds. 18 | 19 | Args: 20 | latency: Latency in seconds 21 | 22 | Returns: 23 | Formatted latency string 24 | """ 25 | return f"{round(latency * 1000)}ms" 26 | 27 | 28 | # pylint: disable=too-many-arguments, too-many-positional-arguments 29 | def create_embed( 30 | title: str, 31 | description: str, 32 | color: Union[int, discord.Color] = discord.Color.from_str("#6f7dff"), 33 | author: Optional[discord.Member] = None, 34 | timestamp: Optional[datetime] = None, 35 | footer: Optional[str] = None, 36 | ) -> discord.Embed: 37 | """ 38 | Create a formatted Discord embed. 39 | 40 | Args: 41 | title: Embed title 42 | description: Embed description 43 | color: Embed color 44 | author: Optional author member 45 | timestamp: Optional timestamp 46 | footer: Optional footer text 47 | 48 | Returns: 49 | Formatted Discord embed 50 | """ 51 | embed = discord.Embed( 52 | title=title, 53 | description=description, 54 | color=color, 55 | timestamp=timestamp or datetime.utcnow(), 56 | ) 57 | 58 | if author: 59 | embed.set_author(name=author.display_name, icon_url=author.display_avatar.url) 60 | 61 | if footer: 62 | embed.set_footer(text=footer) 63 | 64 | return embed 65 | 66 | 67 | def truncate_text(text: str, max_length: int = 1024) -> str: 68 | """ 69 | Truncate text to a maximum length. 70 | 71 | Args: 72 | text: Text to truncate 73 | max_length: Maximum length 74 | 75 | Returns: 76 | Truncated text 77 | """ 78 | if len(text) <= max_length: 79 | return text 80 | 81 | return text[: max_length - 3] + "..." 82 | 83 | 84 | def format_time_delta(seconds: float) -> str: 85 | """ 86 | Format time delta in a human-readable format. 87 | 88 | Args: 89 | seconds: Time in seconds 90 | 91 | Returns: 92 | Formatted time string 93 | """ 94 | if seconds < 60: 95 | return f"{seconds:.1f}s" 96 | if seconds < 3600: 97 | minutes = seconds / 60 98 | return f"{minutes:.1f}m" 99 | hours = seconds / 3600 100 | return f"{hours:.1f}h" 101 | 102 | 103 | def parse_monolithic_template(yaml_path: str) -> dict: 104 | """Parse and validate the monolithic YAML template file.""" 105 | if not os.path.exists(yaml_path): 106 | raise FileNotFoundError(f"Template file not found at: {yaml_path}") 107 | 108 | try: 109 | with open(yaml_path, "r", encoding="utf-8") as file: 110 | template_config = yaml.safe_load(file) 111 | except yaml.YAMLError as e: 112 | raise ValueError(f"Invalid YAML format: {e}") from e 113 | 114 | if template_config is None: 115 | raise ValueError("Template file is empty or invalid.") 116 | 117 | # Validate required fields 118 | if "categories" not in template_config: 119 | raise ValueError("Missing required field: categories") 120 | 121 | if not isinstance(template_config["categories"], list): 122 | raise ValueError("categories must be a list") 123 | 124 | # Validate each category 125 | for i, category in enumerate(template_config["categories"]): 126 | if not isinstance(category, dict): 127 | raise ValueError(f"Category {i} must be a dictionary") 128 | 129 | required_category_fields = ["name", "type"] 130 | for field in required_category_fields: 131 | if field not in category: 132 | raise ValueError(f"Category {i} missing required field: {field}") 133 | 134 | if "channels" in category: 135 | if not isinstance(category["channels"], list): 136 | raise ValueError(f"Category {i} channels must be a list") 137 | 138 | # Validate each channel in the category 139 | for j, channel in enumerate(category["channels"]): 140 | if not isinstance(channel, dict): 141 | raise ValueError(f"Category {i}, channel {j} must be a dictionary") 142 | 143 | required_channel_fields = ["name", "type"] 144 | for field in required_channel_fields: 145 | if field not in channel: 146 | raise ValueError(f"Category {i}, channel {j} missing required field: {field}") 147 | 148 | return template_config 149 | 150 | 151 | def parse_monolithic_template_from_str(yaml_str: str) -> dict: 152 | """Parse and validate monolithic YAML template from a string.""" 153 | try: 154 | template_config = yaml.safe_load(yaml_str) 155 | except yaml.YAMLError as e: 156 | raise ValueError(f"Invalid YAML format: {e}") from e 157 | 158 | if template_config is None: 159 | raise ValueError("Template YAML is empty or invalid.") 160 | 161 | # Validate required fields 162 | if "categories" not in template_config: 163 | raise ValueError("Missing required field: categories") 164 | 165 | if not isinstance(template_config["categories"], list): 166 | raise ValueError("categories must be a list") 167 | 168 | # Validate each category 169 | for i, category in enumerate(template_config["categories"]): 170 | if not isinstance(category, dict): 171 | raise ValueError(f"Category {i} must be a dictionary") 172 | 173 | required_category_fields = ["name", "type"] 174 | for field in required_category_fields: 175 | if field not in category: 176 | raise ValueError(f"Category {i} missing required field: {field}") 177 | 178 | if "channels" in category: 179 | if not isinstance(category["channels"], list): 180 | raise ValueError(f"Category {i} channels must be a list") 181 | 182 | # Validate each channel in the category 183 | for j, channel in enumerate(category["channels"]): 184 | if not isinstance(channel, dict): 185 | raise ValueError(f"Category {i}, channel {j} must be a dictionary") 186 | 187 | required_channel_fields = ["name", "type"] 188 | for field in required_channel_fields: 189 | if field not in channel: 190 | raise ValueError(f"Category {i}, channel {j} missing required field: {field}") 191 | 192 | return template_config 193 | 194 | 195 | def get_template_path(guild_id: int) -> Optional[str]: 196 | """Get the template path for a guild, looking for template.yaml in the root of the template repo.""" 197 | from .template_metadata import load_metadata 198 | 199 | meta = load_metadata(guild_id) 200 | if not meta or not os.path.exists(meta.get("local_path", "")): 201 | return None 202 | 203 | template_path = os.path.join(meta["local_path"], "template.yaml") 204 | if os.path.exists(template_path): 205 | return template_path 206 | 207 | return None 208 | 209 | 210 | # Legacy functions - kept for backward compatibility during transition 211 | def parse_channel_config(yaml_path: str) -> dict: 212 | """Parse and validate the YAML configuration file.""" 213 | if not os.path.exists(yaml_path): 214 | raise ValueError(f"YAML file not found at: {yaml_path}") 215 | 216 | with open(yaml_path, "r", encoding="utf-8") as file: 217 | channel_config = yaml.safe_load(file) 218 | 219 | # Validate required fields 220 | required_fields = ["name", "type"] 221 | for field in required_fields: 222 | if field not in channel_config: 223 | raise ValueError(f"Missing required field: {field}") 224 | 225 | return channel_config 226 | 227 | 228 | def parse_category_config(yaml_path: str) -> dict: 229 | """Parse and validate category YAML configuration file.""" 230 | if not os.path.exists(yaml_path): 231 | raise FileNotFoundError(f"YAML file not found at: {yaml_path}") 232 | 233 | try: 234 | with open(yaml_path, "r", encoding="utf-8") as file: 235 | category_config = yaml.safe_load(file) 236 | except yaml.YAMLError as e: 237 | raise ValueError(f"Invalid YAML format: {e}") from e 238 | 239 | # Validate required fields 240 | required_fields = ["name", "type", "channels"] 241 | for field in required_fields: 242 | if field not in category_config: 243 | raise ValueError(f"Missing required field: {field}") 244 | 245 | return category_config 246 | 247 | 248 | def parse_category_config_from_str(yaml_str: str) -> dict: 249 | """Parse and validate category YAML configuration from a string.""" 250 | import yaml 251 | 252 | try: 253 | category_config = yaml.safe_load(yaml_str) 254 | except yaml.YAMLError as e: 255 | raise ValueError(f"Invalid YAML format: {e}") from e 256 | if category_config is None: 257 | raise ValueError("YAML is empty or invalid.") 258 | required_fields = ["name", "type", "channels"] 259 | for field in required_fields: 260 | if field not in category_config: 261 | raise ValueError(f"Missing required field: {field}") 262 | return category_config 263 | 264 | 265 | def parse_channel_config_from_str(yaml_str: str) -> dict: 266 | """Parse and validate channel YAML configuration from a string.""" 267 | import yaml 268 | 269 | channel_config = yaml.safe_load(yaml_str) 270 | if channel_config is None: 271 | raise ValueError("YAML is empty or invalid.") 272 | required_fields = ["name", "type"] 273 | for field in required_fields: 274 | if field not in channel_config: 275 | raise ValueError(f"Missing required field: {field}") 276 | return channel_config 277 | 278 | 279 | def create_channel_kwargs( 280 | channel_config: dict, category: Optional[discord.CategoryChannel] = None, position: Optional[int] = None 281 | ) -> dict: 282 | """Create channel creation parameters from config.""" 283 | channel_kwargs = { 284 | "name": channel_config["name"], 285 | "category": category, 286 | } 287 | 288 | # Add optional parameters if they exist 289 | if "topic" in channel_config: 290 | channel_kwargs["topic"] = channel_config["topic"] 291 | if "nsfw" in channel_config: 292 | channel_kwargs["nsfw"] = channel_config["nsfw"] 293 | 294 | # Add position if specified (for YAML order-based positioning) 295 | if position is not None: 296 | channel_kwargs["position"] = position 297 | 298 | return channel_kwargs 299 | 300 | 301 | async def create_channel_by_type( 302 | guild: Optional[discord.Guild], channel_config: dict, channel_kwargs: dict 303 | ) -> Optional[discord.abc.GuildChannel]: 304 | """Create a channel based on its type.""" 305 | if not guild: 306 | return None 307 | 308 | channel_type = channel_config["type"].lower() 309 | 310 | if channel_type == "text": 311 | return await guild.create_text_channel(**channel_kwargs) 312 | if channel_type == "voice": 313 | return await guild.create_voice_channel(**channel_kwargs) 314 | return None 315 | 316 | 317 | def check_channel_exists( 318 | category: discord.CategoryChannel, channel_name: str 319 | ) -> Optional[discord.abc.GuildChannel]: 320 | """Check if a channel already exists in a category.""" 321 | return discord.utils.get(category.channels, name=channel_name) 322 | 323 | 324 | def create_error_embed(title: str, description: str) -> discord.Embed: 325 | """Create a standardized error embed.""" 326 | return create_embed(title=title, description=description, color=discord.Color.red()) 327 | 328 | 329 | def create_success_embed(title: str, description: str) -> discord.Embed: 330 | """Create a standardized success embed.""" 331 | return create_embed( 332 | title=title, description=description, color=discord.Color.green() 333 | ) 334 | 335 | 336 | def create_channel_list_embed( 337 | title: str, 338 | created_channels: List[discord.abc.GuildChannel], 339 | total_channels: int, 340 | category_name: str, 341 | ) -> discord.Embed: 342 | """Create an embed showing channel creation results.""" 343 | embed = create_success_embed( 344 | title=title, description=f"Successfully processed category **{category_name}**" 345 | ) 346 | 347 | embed.add_field( 348 | name="Channels Created", 349 | value=f"{len(created_channels)}/{total_channels}", 350 | inline=False, 351 | ) 352 | 353 | if created_channels: 354 | channel_list = "\n".join( 355 | [f"• {channel.mention}" for channel in created_channels] 356 | ) 357 | embed.add_field(name="Created Channels", value=channel_list, inline=False) 358 | 359 | return embed 360 | 361 | 362 | async def handle_command_error( 363 | ctx: commands.Context, error: commands.CommandError, logger 364 | ) -> None: 365 | """Handle common command errors.""" 366 | if isinstance(error, commands.MissingPermissions): 367 | embed = create_error_embed( 368 | "❌ Permission Denied", "You don't have permission to use this command." 369 | ) 370 | await ctx.send(embed=embed) 371 | logger.error("Permission error in command") 372 | elif isinstance(error, discord.Forbidden): 373 | embed = create_error_embed( 374 | "❌ Permission Error", 375 | "The bot doesn't have permission to perform this action.", 376 | ) 377 | await ctx.send(embed=embed) 378 | logger.error("Discord permission error in command") 379 | elif isinstance(error, discord.HTTPException): 380 | embed = create_error_embed( 381 | "❌ Discord Error", f"A Discord error occurred: {error}" 382 | ) 383 | await ctx.send(embed=embed) 384 | logger.error(f"Discord HTTP error in command: {error}") 385 | else: 386 | embed = create_error_embed( 387 | "❌ Unexpected Error", f"An unexpected error occurred: {error}" 388 | ) 389 | await ctx.send(embed=embed) 390 | logger.error(f"Unexpected error in command: {error}") 391 | 392 | 393 | async def handle_interaction_error( 394 | interaction: discord.Interaction, error: Exception, logger 395 | ) -> None: 396 | """Handle common interaction errors.""" 397 | if isinstance(error, discord.Forbidden): 398 | embed = create_error_embed( 399 | "❌ Permission Error", 400 | "The bot doesn't have permission to perform this action.", 401 | ) 402 | await interaction.followup.send(embed=embed) 403 | logger.error("Discord permission error in interaction") 404 | elif isinstance(error, discord.HTTPException): 405 | embed = create_error_embed( 406 | "❌ Discord Error", f"A Discord error occurred: {error}" 407 | ) 408 | await interaction.followup.send(embed=embed) 409 | logger.error(f"Discord HTTP error in interaction: {error}") 410 | else: 411 | embed = create_error_embed( 412 | "❌ Unexpected Error", f"An unexpected error occurred: {str(error)}" 413 | ) 414 | await interaction.followup.send(embed=embed) 415 | logger.error(f"Unexpected error in interaction: {error}") 416 | 417 | 418 | def clean_webpage_text(text: str, max_length: int = 1900) -> str: 419 | """Clean and truncate webpage text content.""" 420 | # Clean up the text 421 | lines = (line.strip() for line in text.splitlines()) 422 | chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) 423 | text = " ".join(chunk for chunk in chunks if chunk) 424 | 425 | # Remove excessive whitespace 426 | text = re.sub(r"\s+", " ", text) 427 | 428 | # Limit text length 429 | if len(text) > max_length: 430 | text = text[:max_length] + "...\n\n*Content truncated due to length limits*" 431 | 432 | return text 433 | -------------------------------------------------------------------------------- /src/gitcord/cogs/channels.py: -------------------------------------------------------------------------------- 1 | """ 2 | Channel management commands cog for GitCord bot. 3 | Contains commands for creating and managing channels and categories. 4 | """ 5 | 6 | from dataclasses import dataclass 7 | from typing import Optional, Union 8 | import os 9 | 10 | import yaml 11 | import discord 12 | from discord import app_commands 13 | from discord.ext import commands 14 | 15 | from .base_cog import BaseCog 16 | from ..utils.helpers import ( 17 | create_embed, 18 | parse_channel_config, 19 | parse_category_config, 20 | create_channel_kwargs, 21 | create_channel_by_type, 22 | check_channel_exists, 23 | get_template_path, 24 | ) 25 | from ..utils import template_metadata 26 | from ..constants.paths import get_template_repo_dir 27 | from ..views import DeleteExtraChannelsView 28 | 29 | 30 | @dataclass 31 | class CategoryResult: 32 | """Result of category processing operations.""" 33 | 34 | category: Optional[discord.CategoryChannel] 35 | created_channels: list 36 | updated_channels: list 37 | skipped_channels: list 38 | extra_channels: list 39 | is_update: bool 40 | 41 | 42 | class Channels(BaseCog): 43 | """Channel management utility commands.""" 44 | 45 | def __init__(self, bot: commands.Bot): 46 | """Initialize the Channels cog.""" 47 | super().__init__(bot) 48 | self.logger.info("Channels cog loaded") 49 | 50 | def _get_template_path(self, guild_id: int, file_name: str = None, folder: str = None) -> Optional[str]: 51 | """Get the template path for a guild, now looking for monolithic template.yaml first.""" 52 | 53 | # Try the new monolithic template format first 54 | template_path = get_template_path(guild_id) 55 | if template_path: 56 | return template_path 57 | 58 | # Fall back to legacy format if requested 59 | meta = template_metadata.load_metadata(guild_id) 60 | if not meta or not os.path.exists(meta.get("local_path", "")): 61 | return None 62 | 63 | base_path = meta["local_path"] 64 | 65 | if folder: 66 | base_path = os.path.join(base_path, folder) 67 | if not os.path.isdir(base_path): 68 | return None 69 | 70 | if file_name: 71 | return os.path.join(base_path, file_name) 72 | 73 | return base_path 74 | 75 | def _ensure_guild( 76 | self, ctx_or_interaction: Union[commands.Context, discord.Interaction] 77 | ) -> Optional[discord.Guild]: 78 | """Ensure we have a valid guild context.""" 79 | if isinstance(ctx_or_interaction, commands.Context): 80 | return ctx_or_interaction.guild 81 | return ctx_or_interaction.guild 82 | 83 | def _ensure_permissions( 84 | self, ctx_or_interaction: Union[commands.Context, discord.Interaction] 85 | ) -> bool: 86 | """Check if user has manage channels permission.""" 87 | if isinstance(ctx_or_interaction, commands.Context): 88 | # Type check to ensure author is a Member (has guild_permissions) 89 | if not isinstance(ctx_or_interaction.author, discord.Member): 90 | return False 91 | return ctx_or_interaction.author.guild_permissions.manage_channels 92 | # For interactions, we need to check if user is a member 93 | if ctx_or_interaction.user is None: 94 | return False 95 | # Type check to ensure user is a Member (has guild_permissions) 96 | if not isinstance(ctx_or_interaction.user, discord.Member): 97 | return False 98 | # At this point we know user is a Member, so we can safely access guild_permissions 99 | member = ctx_or_interaction.user # type: discord.Member 100 | return member.guild_permissions.manage_channels 101 | 102 | async def _create_single_channel( 103 | self, ctx: commands.Context, yaml_path: str 104 | ) -> None: 105 | """Create a single channel from YAML configuration.""" 106 | try: 107 | channel_config = parse_channel_config(yaml_path) 108 | except ValueError as e: 109 | await self.send_error(ctx, "❌ Invalid YAML", str(e)) 110 | return 111 | 112 | # Prepare channel creation parameters 113 | channel_kwargs = create_channel_kwargs(channel_config) 114 | 115 | # Create the channel based on type 116 | new_channel = await create_channel_by_type( 117 | ctx.guild, channel_config, channel_kwargs 118 | ) 119 | 120 | if not new_channel: 121 | await self.send_error( 122 | ctx, 123 | "❌ Invalid Channel Type", 124 | f"Channel type '{channel_config['type']}' is not supported. " 125 | "Use 'text' or 'voice'.", 126 | ) 127 | return 128 | 129 | # Create success embed 130 | embed = create_embed( 131 | title="✅ Channel Created", 132 | description=f"Successfully created channel: {new_channel.mention}", 133 | color=discord.Color.green(), 134 | ) 135 | 136 | # Add fields manually 137 | embed.add_field(name="Name", value=channel_kwargs["name"], inline=True) 138 | embed.add_field(name="Type", value=channel_config["type"], inline=True) 139 | embed.add_field( 140 | name="NSFW", value=channel_kwargs.get("nsfw", False), inline=True 141 | ) 142 | embed.add_field( 143 | name="Topic", 144 | value=channel_config.get("topic", "No topic set"), 145 | inline=False, 146 | ) 147 | 148 | await ctx.send(embed=embed) 149 | self.logger.info( 150 | "Channel '%s' created successfully by %s", 151 | channel_config["name"], 152 | ctx.author, 153 | ) 154 | 155 | async def _update_existing_channel( 156 | self, existing_channel: discord.abc.GuildChannel, channel_config: dict 157 | ) -> bool: 158 | """Update an existing channel if needed.""" 159 | channel_updated = False 160 | update_kwargs = {} 161 | 162 | # Check if it's a text channel for topic 163 | if isinstance(existing_channel, discord.TextChannel): 164 | if existing_channel.topic != channel_config.get("topic", ""): 165 | update_kwargs["topic"] = channel_config.get("topic", "") 166 | channel_updated = True 167 | 168 | # Check NSFW for text and voice channels 169 | if isinstance(existing_channel, (discord.TextChannel, discord.VoiceChannel)): 170 | if existing_channel.nsfw != channel_config.get("nsfw", False): 171 | update_kwargs["nsfw"] = channel_config.get("nsfw", False) 172 | channel_updated = True 173 | 174 | # Check position for all channel types 175 | if "position" in channel_config and hasattr(existing_channel, "position"): 176 | if existing_channel.position != channel_config["position"]: 177 | update_kwargs["position"] = channel_config["position"] 178 | channel_updated = True 179 | 180 | if not channel_updated: 181 | return False 182 | 183 | # Type check to ensure we can edit the channel 184 | if isinstance(existing_channel, (discord.TextChannel, discord.VoiceChannel)): 185 | await existing_channel.edit(**update_kwargs) 186 | self.logger.info( 187 | "Channel '%s' updated in category", 188 | channel_config["name"], 189 | ) 190 | return True 191 | self.logger.warning( 192 | "Cannot edit channel '%s' of type %s", 193 | channel_config["name"], 194 | type(existing_channel).__name__, 195 | ) 196 | return False 197 | 198 | async def _create_channel_in_category( 199 | self, 200 | guild: discord.Guild, 201 | channel_config: dict, 202 | category: discord.CategoryChannel, 203 | ) -> Optional[discord.abc.GuildChannel]: 204 | """Create a new channel in the category.""" 205 | channel_kwargs = create_channel_kwargs(channel_config, category) 206 | new_channel = await create_channel_by_type( 207 | guild, channel_config, channel_kwargs 208 | ) 209 | 210 | if new_channel: 211 | self.logger.info( 212 | "Channel '%s' created successfully in category", 213 | channel_config["name"], 214 | ) 215 | return new_channel 216 | self.logger.warning( 217 | "Skipping channel '%s': Invalid type '%s'", 218 | channel_config["name"], 219 | channel_config["type"], 220 | ) 221 | return None 222 | 223 | async def _process_channel_in_category( 224 | self, 225 | guild: discord.Guild, 226 | existing_category: discord.CategoryChannel, 227 | channel_name: str, 228 | yaml_channel_names: set, 229 | ) -> tuple[ 230 | Optional[discord.abc.GuildChannel], Optional[discord.abc.GuildChannel], str 231 | ]: 232 | """Process a single channel in a category.""" 233 | try: 234 | channel_yaml_path = self._get_template_path(guild.id, f"{channel_name}.yaml") 235 | if not channel_yaml_path: 236 | self.logger.error("No template repo found for guild %s. Use !git clone first.", guild.id) 237 | return None, None, "" 238 | 239 | channel_config = parse_channel_config(channel_yaml_path) 240 | 241 | existing_channel = check_channel_exists( 242 | existing_category, channel_config["name"] 243 | ) 244 | 245 | if existing_channel: 246 | if await self._update_existing_channel( 247 | existing_channel, channel_config 248 | ): 249 | yaml_channel_names.add(channel_config["name"]) 250 | return existing_channel, None, "" 251 | return None, None, channel_config["name"] 252 | 253 | new_channel = await self._create_channel_in_category( 254 | guild, channel_config, existing_category 255 | ) 256 | if new_channel: 257 | yaml_channel_names.add(channel_config["name"]) 258 | return None, new_channel, "" 259 | return None, None, "" 260 | 261 | except (ValueError, FileNotFoundError, yaml.YAMLError) as e: 262 | self.logger.error("Failed to create channel '%s': %s", channel_name, e) 263 | return None, None, "" 264 | 265 | async def _process_existing_category_channels( 266 | self, 267 | guild: discord.Guild, 268 | existing_category: discord.CategoryChannel, 269 | category_config: dict, 270 | ) -> tuple[list, list, list, list]: 271 | """Process channels in an existing category.""" 272 | updated_channels = [] 273 | created_channels = [] 274 | skipped_channels = [] 275 | extra_channels = [] 276 | yaml_channel_names = set() 277 | 278 | for channel_name in category_config["channels"]: 279 | updated_channel, new_channel, skipped_name = ( 280 | await self._process_channel_in_category( 281 | guild, existing_category, channel_name, yaml_channel_names 282 | ) 283 | ) 284 | 285 | if updated_channel: 286 | updated_channels.append(updated_channel) 287 | elif new_channel: 288 | created_channels.append(new_channel) 289 | elif skipped_name: 290 | skipped_channels.append(skipped_name) 291 | 292 | # Find extra channels 293 | for channel in existing_category.channels: 294 | if channel.name not in yaml_channel_names: 295 | extra_channels.append(channel) 296 | 297 | return created_channels, updated_channels, skipped_channels, extra_channels 298 | 299 | async def _handle_existing_category( 300 | self, 301 | guild: discord.Guild, 302 | existing_category: discord.CategoryChannel, 303 | category_config: dict, 304 | ) -> tuple[list, list, list, list]: 305 | """Handle processing of an existing category.""" 306 | return await self._process_existing_category_channels( 307 | guild, existing_category, category_config 308 | ) 309 | 310 | async def _create_new_category( 311 | self, guild: discord.Guild, category_config: dict 312 | ) -> tuple[discord.CategoryChannel, list]: 313 | """Create a new category with channels.""" 314 | category_kwargs = { 315 | "name": category_config["name"], 316 | "position": category_config.get("position", 0), 317 | } 318 | new_category = await guild.create_category(**category_kwargs) 319 | created_channels = [] 320 | 321 | for channel_name in category_config["channels"]: 322 | try: 323 | channel_yaml_path = self._get_template_path(guild.id, f"{channel_name}.yaml") 324 | if not channel_yaml_path: 325 | self.logger.error("No template repo found for guild %s. Use !git clone first.", guild.id) 326 | continue 327 | 328 | channel_config = parse_channel_config(channel_yaml_path) 329 | 330 | existing_channel = check_channel_exists( 331 | new_category, channel_config["name"] 332 | ) 333 | if existing_channel: 334 | self.logger.warning( 335 | "Channel '%s' already exists in category '%s', skipping", 336 | channel_config["name"], 337 | category_config["name"], 338 | ) 339 | continue 340 | 341 | new_channel = await self._create_channel_in_category( 342 | guild, channel_config, new_category 343 | ) 344 | if new_channel: 345 | created_channels.append(new_channel) 346 | 347 | except (ValueError, FileNotFoundError, yaml.YAMLError) as e: 348 | self.logger.error("Failed to create channel '%s': %s", channel_name, e) 349 | continue 350 | 351 | return new_category, created_channels 352 | 353 | async def _create_category_common( 354 | self, guild: discord.Guild, yaml_path: str 355 | ) -> CategoryResult: 356 | """Common logic for creating/updating categories.""" 357 | try: 358 | category_config = parse_category_config(yaml_path) 359 | except (ValueError, FileNotFoundError, yaml.YAMLError) as e: 360 | self.logger.error("Failed to parse category config: %s", e) 361 | return CategoryResult(None, [], [], [], [], False) 362 | 363 | # Check if category already exists 364 | existing_category = discord.utils.get( 365 | guild.categories, name=category_config["name"] 366 | ) 367 | 368 | if existing_category: 369 | # Category exists, process it 370 | created_channels, updated_channels, skipped_channels, extra_channels = ( 371 | await self._handle_existing_category( 372 | guild, existing_category, category_config 373 | ) 374 | ) 375 | return CategoryResult( 376 | existing_category, 377 | created_channels, 378 | updated_channels, 379 | skipped_channels, 380 | extra_channels, 381 | True, 382 | ) 383 | # Create new category 384 | new_category, created_channels = await self._create_new_category( 385 | guild, category_config 386 | ) 387 | return CategoryResult(new_category, created_channels, [], [], [], False) 388 | 389 | def _add_channel_list_field( 390 | self, embed: discord.Embed, channels: list, field_name: str 391 | ) -> None: 392 | """Add a channel list field to an embed.""" 393 | if channels: 394 | channel_list = "\n".join( 395 | [f"• {channel.mention} ({field_name.lower()})" for channel in channels] 396 | ) 397 | embed.add_field( 398 | name=f"{field_name} Channels", value=channel_list, inline=False 399 | ) 400 | 401 | def _create_category_result_embed(self, result: CategoryResult) -> discord.Embed: 402 | """Create result embed for category operations.""" 403 | if not result.category: 404 | raise ValueError("Category cannot be None when creating result embed") 405 | 406 | title = "✅ Category Updated" if result.is_update else "✅ Category Created" 407 | description = f"Successfully processed category: **{result.category.name}**" 408 | 409 | embed = create_embed( 410 | title=title, 411 | description=description, 412 | color=discord.Color.green(), 413 | ) 414 | 415 | # Add basic fields 416 | embed.add_field(name="Category", value=result.category.mention, inline=True) 417 | embed.add_field( 418 | name="Channels Created", 419 | value=str(len(result.created_channels)), 420 | inline=True, 421 | ) 422 | embed.add_field( 423 | name="Channels Updated", 424 | value=str(len(result.updated_channels)), 425 | inline=True, 426 | ) 427 | embed.add_field( 428 | name="Channels Skipped", 429 | value=str(len(result.skipped_channels)), 430 | inline=True, 431 | ) 432 | embed.add_field( 433 | name="Extra Channels", value=str(len(result.extra_channels)), inline=True 434 | ) 435 | 436 | # Add detailed lists 437 | self._add_channel_list_field(embed, result.created_channels, "New") 438 | self._add_channel_list_field(embed, result.updated_channels, "Updated") 439 | 440 | if result.extra_channels: 441 | channel_list = "\n".join( 442 | [ 443 | f"• {channel.mention} (not in YAML)" 444 | for channel in result.extra_channels 445 | ] 446 | ) 447 | embed.add_field( 448 | name="Extra Channels (Not in YAML)", value=channel_list, inline=False 449 | ) 450 | 451 | return embed 452 | 453 | @commands.command(name="createchannel") 454 | @commands.has_permissions(manage_channels=True) 455 | async def createchannel(self, ctx: commands.Context) -> None: 456 | """Create a channel based on properties defined in a YAML file.""" 457 | # Check if we have a monolithic template first 458 | template_path = self._get_template_path(ctx.guild.id) 459 | if template_path and template_path.endswith("template.yaml"): 460 | await self.send_error(ctx, "⚠️ Command Deprecated", 461 | "This server uses the new monolithic template format. Please use `!git pull` to apply the entire template instead of creating individual channels.") 462 | return 463 | 464 | # Fall back to legacy format 465 | yaml_path = self._get_template_path(ctx.guild.id, "off-topic.yaml") 466 | if not yaml_path: 467 | await self.send_error(ctx, "❌ No Template Repository", 468 | "No template repository found for this server. Use `!git clone ` first to set up a template repository.") 469 | return 470 | await self._create_single_channel(ctx, yaml_path) 471 | 472 | @commands.command(name="createcategory") 473 | @commands.has_permissions(manage_channels=True) 474 | async def createcategory(self, ctx: commands.Context) -> None: 475 | """Create a category and its channels based on properties defined in a YAML file.""" 476 | guild = self._ensure_guild(ctx) 477 | if not guild: 478 | await self.send_error(ctx, "❌ Error", "Guild not found") 479 | return 480 | 481 | # Check if we have a monolithic template first 482 | template_path = self._get_template_path(guild.id) 483 | if template_path and template_path.endswith("template.yaml"): 484 | await self.send_error(ctx, "⚠️ Command Deprecated", 485 | "This server uses the new monolithic template format. Please use `!git pull` to apply the entire template instead of creating individual categories.") 486 | return 487 | 488 | # Fall back to legacy format 489 | yaml_path = self._get_template_path(guild.id, "category.yaml") 490 | if not yaml_path: 491 | await self.send_error(ctx, "❌ No Template Repository", 492 | "No template repository found for this server. Use `!git clone ` first to set up a template repository.") 493 | return 494 | 495 | try: 496 | result = await self._create_category_common(guild, yaml_path) 497 | 498 | if not result.category: 499 | await self.send_error( 500 | ctx, "❌ Error", "Failed to process category configuration" 501 | ) 502 | return 503 | 504 | embed = self._create_category_result_embed(result) 505 | 506 | if result.extra_channels: 507 | embed.add_field( 508 | name="Action Required", 509 | value="Use the button below to delete extra channels if needed.", 510 | inline=False, 511 | ) 512 | view = DeleteExtraChannelsView(result.extra_channels, self.logger) 513 | await ctx.send(embed=embed, view=view) 514 | else: 515 | await ctx.send(embed=embed) 516 | 517 | self.logger.info( 518 | "Category '%s' processed: %d created, %d updated, %d skipped, %d extra", 519 | result.category.name, 520 | len(result.created_channels), 521 | len(result.updated_channels), 522 | len(result.skipped_channels), 523 | len(result.extra_channels), 524 | ) 525 | 526 | except (discord.DiscordException, OSError) as e: 527 | self.logger.error("Unexpected error in createcategory command: %s", e) 528 | await self.send_error( 529 | ctx, "❌ Unexpected Error", f"An unexpected error occurred: {str(e)}" 530 | ) 531 | 532 | @app_commands.command( 533 | name="createcategory", 534 | description="Create a category and its channels from YAML configuration", 535 | ) 536 | @app_commands.describe(yaml_path="Path to the category YAML file (optional)") 537 | async def createcategory_slash( 538 | self, interaction: discord.Interaction, yaml_path: str | None = None 539 | ) -> None: 540 | """Slash command to create a category and its channels based on YAML configuration.""" 541 | await interaction.response.defer() 542 | 543 | # Check permissions 544 | if not self._ensure_permissions(interaction): 545 | embed = create_embed( 546 | title="❌ Permission Denied", 547 | description="You need the 'Manage Channels' permission to use this command.", 548 | color=discord.Color.red(), 549 | ) 550 | await interaction.followup.send(embed=embed) 551 | return 552 | 553 | # Ensure we have a guild 554 | guild = self._ensure_guild(interaction) 555 | if not guild: 556 | embed = create_embed( 557 | title="❌ Error", 558 | description="This command can only be used in a server.", 559 | color=discord.Color.red(), 560 | ) 561 | await interaction.followup.send(embed=embed) 562 | return 563 | 564 | # Use default path if none provided 565 | if yaml_path is None: 566 | # Check if we have a monolithic template first 567 | template_path = self._get_template_path(guild.id) 568 | if template_path and template_path.endswith("template.yaml"): 569 | embed = create_embed( 570 | title="⚠️ Command Deprecated", 571 | description="This server uses the new monolithic template format. Please use `!git pull` to apply the entire template instead of creating individual categories.", 572 | color=discord.Color.orange(), 573 | ) 574 | await interaction.followup.send(embed=embed) 575 | return 576 | 577 | # Fall back to legacy format 578 | yaml_path = self._get_template_path(guild.id, "category.yaml") 579 | if not yaml_path: 580 | embed = create_embed( 581 | title="❌ No Template Repository", 582 | description="No template repository found for this server. Use `!git clone ` first to set up a template repository.", 583 | color=discord.Color.red(), 584 | ) 585 | await interaction.followup.send(embed=embed) 586 | return 587 | 588 | try: 589 | result = await self._create_category_common(guild, yaml_path) 590 | 591 | if not result.category: 592 | embed = create_embed( 593 | title="❌ Error", 594 | description="Failed to process category configuration. " 595 | "Please check the YAML file.", 596 | color=discord.Color.red(), 597 | ) 598 | await interaction.followup.send(embed=embed) 599 | return 600 | 601 | embed = self._create_category_result_embed(result) 602 | 603 | # Add delete button if there are extra channels 604 | if result.extra_channels: 605 | delete_view = DeleteExtraChannelsView( 606 | result.extra_channels, self.logger 607 | ) 608 | await interaction.followup.send(embed=embed, view=delete_view) 609 | else: 610 | await interaction.followup.send(embed=embed) 611 | 612 | self.logger.info( 613 | "Category '%s' processed: %d created, %d updated, %d skipped, %d extra", 614 | result.category.name, 615 | len(result.created_channels), 616 | len(result.updated_channels), 617 | len(result.skipped_channels), 618 | len(result.extra_channels), 619 | ) 620 | 621 | except discord.Forbidden: 622 | embed = create_embed( 623 | title="❌ Permission Error", 624 | description="I don't have permission to create categories or channels " 625 | "in this server.", 626 | color=discord.Color.red(), 627 | ) 628 | await interaction.followup.send(embed=embed) 629 | self.logger.error("Permission error in createcategory slash command") 630 | except discord.HTTPException as e: 631 | embed = create_embed( 632 | title="❌ Discord Error", 633 | description=f"Discord API error: {str(e)}", 634 | color=discord.Color.red(), 635 | ) 636 | await interaction.followup.send(embed=embed) 637 | self.logger.error( 638 | "Discord HTTP error in createcategory slash command: %s", e 639 | ) 640 | except (discord.DiscordException, OSError) as e: 641 | embed = create_embed( 642 | title="❌ Unexpected Error", 643 | description=f"An unexpected error occurred: {str(e)}", 644 | color=discord.Color.red(), 645 | ) 646 | await interaction.followup.send(embed=embed) 647 | self.logger.error("Unexpected error in createcategory slash command: %s", e) 648 | 649 | 650 | async def setup(bot: commands.Bot) -> None: 651 | """Set up the Channels cog.""" 652 | await bot.add_cog(Channels(bot)) 653 | --------------------------------------------------------------------------------