├── .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 | [](https://evolvewithevan.github.io/gitcord/)
6 | [](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 |
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 |
--------------------------------------------------------------------------------