Skip to main content

Overview

The McDisClient class extends discord.ext.commands.Bot and serves as the core of McDis-RCON. It manages configuration, processes, plugins, addons, and Discord integration.

Class Definition

from mcdis_rcon.classes import McDisClient

class McDisClient(commands.Bot):
    def __init__(self):
        super().__init__(blank_space, intents = discord.Intents().all())

Attributes

prefix
str
default:"!!"
Command prefix for Discord panel commands
cwd
str
Current working directory (from os.getcwd())
config
dict
Loaded configuration from md_config.yml
panel
discord.TextChannel
Discord text channel where the control panel is displayed
_
Callable[[str], str]
Translation function for localization (gettext)
path_backups
str
default:".mdbackups"
Directory for storing server backups
path_addons
str
default:".mdaddons"
Directory containing global addons
addons
dict
Dictionary of loaded addon instances
flask
FlaskManager
Flask web server instance (if enabled)
processes
list[Union[Network, Server]]
All managed processes (servers and networks)
networks
list[Network]
Network processes only (Velocity, Waterfall, etc.)
servers
list[Server]
Server processes only (Vanilla, Paper, Fabric, etc.)
uploader
Uploader
File upload manager instance
files_manager
FilesManager
File management system instance
is_running
bool
default:"False"
Flag indicating if McDis-RCON has completed initialization
discord_listeners
list[str]
List of available Discord event listeners from behaviours directory

Configuration Loading

_load_config()

def _load_config(self):
    try:
        self.config = read_yml('md_config.yml')
    except Exception as error:
        print('The file \'md_config.yml\' could not be opened.')
        os._exit(0)
Loads and validates md_config.yml. Validates:
  • Bot token (string)
  • Panel ID (integer)
  • Language (en or es)
  • Backup count (1-5)
  • Flask configuration
  • Process definitions

Process Management

_load_processes()

async def _load_processes(self):
    for name in self.config['Processes']['Servers']:
        server = Server(name, self, self.config['Processes']['Servers'][name])
        self.processes.append(server)
        self.servers.append(server)

    for name in self.config['Processes']['Networks']:
        network = Network(name, self, self.config['Processes']['Networks'][name])
        self.processes.append(network)
        self.networks.append(network)
Creates Server and Network objects from configuration.

Addon Management

_load_addons(reload=False)

async def _load_addons(self, *, reload: bool = False):
    if reload: 
        self.unload_addons()
    
    files_in_addons_dir = os.listdir(self.path_addons)
    valid_extensions = ['.py', '.mcdis']
    
    addons = [file for file in files_in_addons_dir
              if os.path.splitext(file)[1] in valid_extensions or
              os.path.exists(os.path.join(self.path_addons, file, '__init__.py'))]
Loads mdaddons from .mdaddons/ directory. Supports:
  • .py files
  • Folder addons with __init__.py

call_addons(function, args)

async def call_addons(self, function: str, args: tuple = tuple()):
    for name, addon in self.addons.items():
        try:
            func = getattr(addon, function, None)
            if func:
                await func(*args)
        except Exception:
            await self.error_report(
                title=f'{function}() of {addon}',
                error=traceback.format_exc()
            )
Calls a method on all loaded addons.

Error Handling

error_report(title, error)

async def error_report(self, *, title: str, error: str, should_print: bool = True):
    error_log = f'- Error Report [{title}]:\n\n{error}'.replace('`',''')
    mrkd_error = f'```diff\n{truncate(error_log, 1980)}\n```'
    error_reports = await thread('Error Reports', self.panel)
    error_report = await error_reports.send(mrkd_error)
    if should_print: 
        print(f'\n{error_log}')
    
    return error_report.jump_url
Posts error reports to the Discord “Error Reports” thread.

Discord Events

on_ready()

async def on_ready(self):
    if self.is_running: 
        return
    
    self._load_panel()
    print(self._('Logged in as {}!').format(self.user))
    
    await self._load_processes()
    await self._load_addons()
    await self._load_behaviours()
    
    self.flask = FlaskManager(self)
    asyncio.create_task(self._load_banner())
    
    self.is_running = True
    await execute_and_wait(self.console_listener)
Called when the bot successfully connects to Discord.

panel_interface(message)

async def panel_interface(self, message: discord.Message):
    if message.author.bot: 
        return

    if message.channel.id == self.panel.id:
        if self.is_command(message.content.lower(), 'start-all'):
            await message.delete()
            response = await message.channel.send(
                self._('✔ Initializing processes.'))
            await response.delete(delay = 2)
            
            for process in self.processes: 
                process.start()
Handles commands sent in the panel channel.

Utility Methods

is_command(message, command, console=False)

def is_command(self, message: str, command: str, console: bool = False):
    dummy = message + ' '
    return dummy.startswith(f'{self.prefix}{command} ') if not console else dummy.startswith(command)
Checks if a message matches a command pattern.

is_valid_mcdis_path(path, check_if_file=False, check_if_dir=False)

def is_valid_mcdis_path(self, path: str, *, check_if_file: bool = False, check_if_dir: bool = False):
    real_path = un_mcdis_path(path)
    new_path = os.path.join(self.cwd, real_path)
    
    if not path.split(os.sep)[0] == 'McDis':
        return self._('✖ The path must be a McDis path. E.g.: `McDis/Backups`.')
    if not new_path.startswith(self.cwd):
        return self._('✖ You must work within the directory where McDis is running.')
Validates McDis paths and prevents directory traversal.

Shutdown Handling

on_stop()

def on_stop(self):
    self.unload_addons()
    
    if any([process.is_running() for process in self.processes]):
        print(self._('Closing processes...'))
        
        for process in self.processes:
            process.stop(omit_task=True)
        
        # Wait up to 60 seconds
        i = 60
        while i > 0 and any([process.is_running() for process in self.processes]):
            i -= 1
            time.sleep(1)
        
        # Force kill if needed
        if any([process.is_running() for process in self.processes]):
            for process in self.processes:
                process.kill(omit_task=True)
    
    os._exit(0)
Gracefully shuts down all processes before exiting.

Usage Example

from mcdis_rcon.classes import McDisClient

# Accessing the client in a plugin
class mdplugin:
    def __init__(self, process):
        self.process = process
        self.client = process.client  # McDisClient instance
        
        # Access client features
        print(f"Panel ID: {self.client.panel.id}")
        print(f"Config: {self.client.config}")
        print(f"Processes: {len(self.client.processes)}")

Next Steps

Process Class

Learn about process management

Creating Addons

Build custom addons

Build docs developers (and LLMs) love