Skip to content

API: handlers

handlers.moduleHandler

ModuleHandler

Discover, initialize, and manage feature modules.

Scans the ./modules directory, reads each module's manifest.json, executes the module's initFile (usually main.py) and wires exported commands, events, interface hooks and settings into the running bot.

Source code in handlers/moduleHandler.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
class ModuleHandler:
    """Discover, initialize, and manage feature modules.

    Scans the `./modules` directory, reads each module's `manifest.json`, executes the
    module's `initFile` (usually `main.py`) and wires exported commands, events,
    interface hooks and settings into the running bot.
    """

    def __init__(self, bot: ExtendedClient, logger: Logger):
        """Initialize the handler.

        Args:
            bot: The extended Discord client.
            logger: Logger used for diagnostics.
        """
        self.bot = bot
        self.logger = logger
        self.modules_path = Path("./modules")
        self.loaded_modules: Dict[str, Module] = {}

    def register_permissions(self, module_name: str, permissions: List[str]):
        """Register a list of permission nodes on behalf of a module.

        Note:
            This implementation registers nodes that always resolve to `True`.
            Replace the lambda with proper checks (or namespace handlers) if your
            modules rely on real permission enforcement.

        Args:
            module_name: Name of the module requesting the nodes.
            permissions: Permission paths (e.g., "Feature.Admin.Purge", "Role.*").
        """
        for permission in permissions:
            try:
                self.bot.permission_manager.register_node(permission, lambda client, node, member, channel: True)
                self.logger.info(f"Permission '{permission}' registered for module '{module_name}'.")
            except Exception as e:
                self.logger.error(f"Failed to register permission '{permission}' for module '{module_name}': {e}")


    async def load_modules(self, specific_module: Optional[str] = None):
        """Load modules from disk and attach their commands/events.

        Walks `self.modules_path`, reads each `manifest.json`, executes the module
        setup file, builds a :class:`Module` instance and delegates:
          - commands to `CommandHandler.load_commands_from_folder`
          - events to `EventHandler.load_events_from_module`

        Args:
            specific_module: If provided, load only that folder name; otherwise load all.
        """
        modules_path = self.modules_path

        for module_folder in modules_path.iterdir():
            if not module_folder.is_dir():
                continue

            if specific_module and module_folder.name != specific_module:
                continue

            manifest_path = module_folder / "manifest.json"
            if not manifest_path.exists():
                self.logger.warning(f"Manifest not found for module: {module_folder.name}")
                continue

            # Load the module's manifest
            try:
                with open(manifest_path, "r", encoding="utf-8") as manifest_file:
                    manifest = json.load(manifest_file)
            except json.JSONDecodeError as e:
                self.logger.error(f"Failed to parse manifest for module {module_folder.name}: {e}")
                continue

            # Extract module information
            name = manifest.get("name", module_folder.name)
            self.logger.info(f"Loading {name} module...")
            description = manifest.get("description", "No description provided.")
            version = manifest.get("version", "1.0.0")
            color = manifest.get("color", "#FFFFFF")
            setup_file = module_folder / manifest.get("initFile", "main.py")

            commands_folder = module_folder / manifest.get("commandsFolder", "commands")
            events_folder = module_folder / manifest.get("eventsFolder", "events")
            translations_folder = module_folder / manifest.get("translationsFolder", "translations")

            base_package = f"modules.{module_folder.name}.commands"

            # Execute the setup function and retrieve interface and settings
            setup_data = self._execute_setup(setup_file)
            if setup_data is None:
                self.logger.error(f"Setup failed for module {name}. Skipping module.")
                continue

            interface = setup_data.get("interface", {})
            settings = setup_data.get("settings", [])
            user_settings = setup_data.get("userSettings", [])

            # Create the module instance
            module = Module(
                name=name,
                path=str(module_folder),
                description=description,
                version=version,
                color=color,
                logger=self.logger,
                init_func=setup_data.get("initFunc"),
                data=manifest,
                commands={"text": {}, "slash": {}},
                interfacer=interface,
                settings=settings,
                user_settings=user_settings,
            )

            if self.bot.command_handler:
                await self.bot.command_handler.load_commands_from_folder(commands_folder, base_package, module)
            else:
                self.logger.error("CommandHandler is not initialized.")

            # Load events
            if self.bot.event_handler:
                self.bot.event_handler.load_events_from_module(name, events_folder, module)
            else:
                self.logger.error("EventHandler is not initialized.")

            # Store the module in the loaded modules dictionary
            self.loaded_modules[name] = module
            self.logger.info(f"Successfully loaded module: {name}")

        self.bot.modules = self.loaded_modules

    def _execute_setup(self, setup_file_path: Path) -> Optional[Dict[str, Any]]:
        """Execute the `setup(bot, logger)` function exported by the module init file.

        The `setup` function should return a dict with any of:
            - "interface": an object or dict with callable utilities the module exposes
            - "settings": a list of guild‑scoped Setting instances
            - "userSettings": a list of member‑scoped Setting instances
            - "initFunc": optional callable to run after loading (stored on Module)

        Args:
            setup_file_path: Absolute path to the module's init file.

        Returns:
            A dict with setup data, or `None` if the init file cannot be executed
            or does not return a proper mapping.
        """
        if not setup_file_path.exists():
            self.logger.warning(f"Init file not found: {setup_file_path}")
            return None

        module_name = f"init_{setup_file_path.stem}"
        spec = importlib.util.spec_from_file_location(module_name, setup_file_path)
        if spec is None:
            self.logger.error(f"Failed to load init file: {setup_file_path}")
            return None

        module = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module
        loader = spec.loader
        if loader is None:
            self.logger.error(f"Loader not found for init file: {setup_file_path}")
            return None

        try:
            loader.exec_module(module)
            setup_func = getattr(module, "setup", None)
            if callable(setup_func):
                setup_data = setup_func(self.bot, self.logger)
                if isinstance(setup_data, dict):
                    return setup_data
                else:
                    self.logger.error(f"Setup function in {setup_file_path} did not return a dictionary.")
                    return None
            else:
                self.logger.error(f"No callable 'setup' function found in {setup_file_path}.")
                return None
        except Exception as e:
            self.logger.error(f"Error executing setup function in {setup_file_path}: {e}")
            return None

    async def unload_modules(self):
        """Unload all modules that were previously loaded.

        Calls each module's `unload(bot)` coroutine (if implemented), removes it
        from the in‑memory registry, and logs results.
        """
        for module_name, module in list(self.loaded_modules.items()):
            try:
                await module.unload(self.bot)
                del self.loaded_modules[module_name]
                self.logger.info(f"Unloaded module: {module_name}")
            except Exception as e:
                self.logger.error(f"Failed to unload module {module_name}: {e}")

    async def reload_modules(self):
        """Reload all modules by unloading then loading them again.

        Useful during development or when hot‑reloading code on disk.
        """
        await self.unload_modules()
        await self.load_modules()

__init__(bot, logger)

Initialize the handler.

Parameters:

Name Type Description Default
bot ExtendedClient

The extended Discord client.

required
logger Logger

Logger used for diagnostics.

required
Source code in handlers/moduleHandler.py
20
21
22
23
24
25
26
27
28
29
30
def __init__(self, bot: ExtendedClient, logger: Logger):
    """Initialize the handler.

    Args:
        bot: The extended Discord client.
        logger: Logger used for diagnostics.
    """
    self.bot = bot
    self.logger = logger
    self.modules_path = Path("./modules")
    self.loaded_modules: Dict[str, Module] = {}

load_modules(specific_module=None) async

Load modules from disk and attach their commands/events.

Walks self.modules_path, reads each manifest.json, executes the module setup file, builds a :class:Module instance and delegates: - commands to CommandHandler.load_commands_from_folder - events to EventHandler.load_events_from_module

Parameters:

Name Type Description Default
specific_module Optional[str]

If provided, load only that folder name; otherwise load all.

None
Source code in handlers/moduleHandler.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
async def load_modules(self, specific_module: Optional[str] = None):
    """Load modules from disk and attach their commands/events.

    Walks `self.modules_path`, reads each `manifest.json`, executes the module
    setup file, builds a :class:`Module` instance and delegates:
      - commands to `CommandHandler.load_commands_from_folder`
      - events to `EventHandler.load_events_from_module`

    Args:
        specific_module: If provided, load only that folder name; otherwise load all.
    """
    modules_path = self.modules_path

    for module_folder in modules_path.iterdir():
        if not module_folder.is_dir():
            continue

        if specific_module and module_folder.name != specific_module:
            continue

        manifest_path = module_folder / "manifest.json"
        if not manifest_path.exists():
            self.logger.warning(f"Manifest not found for module: {module_folder.name}")
            continue

        # Load the module's manifest
        try:
            with open(manifest_path, "r", encoding="utf-8") as manifest_file:
                manifest = json.load(manifest_file)
        except json.JSONDecodeError as e:
            self.logger.error(f"Failed to parse manifest for module {module_folder.name}: {e}")
            continue

        # Extract module information
        name = manifest.get("name", module_folder.name)
        self.logger.info(f"Loading {name} module...")
        description = manifest.get("description", "No description provided.")
        version = manifest.get("version", "1.0.0")
        color = manifest.get("color", "#FFFFFF")
        setup_file = module_folder / manifest.get("initFile", "main.py")

        commands_folder = module_folder / manifest.get("commandsFolder", "commands")
        events_folder = module_folder / manifest.get("eventsFolder", "events")
        translations_folder = module_folder / manifest.get("translationsFolder", "translations")

        base_package = f"modules.{module_folder.name}.commands"

        # Execute the setup function and retrieve interface and settings
        setup_data = self._execute_setup(setup_file)
        if setup_data is None:
            self.logger.error(f"Setup failed for module {name}. Skipping module.")
            continue

        interface = setup_data.get("interface", {})
        settings = setup_data.get("settings", [])
        user_settings = setup_data.get("userSettings", [])

        # Create the module instance
        module = Module(
            name=name,
            path=str(module_folder),
            description=description,
            version=version,
            color=color,
            logger=self.logger,
            init_func=setup_data.get("initFunc"),
            data=manifest,
            commands={"text": {}, "slash": {}},
            interfacer=interface,
            settings=settings,
            user_settings=user_settings,
        )

        if self.bot.command_handler:
            await self.bot.command_handler.load_commands_from_folder(commands_folder, base_package, module)
        else:
            self.logger.error("CommandHandler is not initialized.")

        # Load events
        if self.bot.event_handler:
            self.bot.event_handler.load_events_from_module(name, events_folder, module)
        else:
            self.logger.error("EventHandler is not initialized.")

        # Store the module in the loaded modules dictionary
        self.loaded_modules[name] = module
        self.logger.info(f"Successfully loaded module: {name}")

    self.bot.modules = self.loaded_modules

register_permissions(module_name, permissions)

Register a list of permission nodes on behalf of a module.

Note

This implementation registers nodes that always resolve to True. Replace the lambda with proper checks (or namespace handlers) if your modules rely on real permission enforcement.

Parameters:

Name Type Description Default
module_name str

Name of the module requesting the nodes.

required
permissions List[str]

Permission paths (e.g., "Feature.Admin.Purge", "Role.*").

required
Source code in handlers/moduleHandler.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def register_permissions(self, module_name: str, permissions: List[str]):
    """Register a list of permission nodes on behalf of a module.

    Note:
        This implementation registers nodes that always resolve to `True`.
        Replace the lambda with proper checks (or namespace handlers) if your
        modules rely on real permission enforcement.

    Args:
        module_name: Name of the module requesting the nodes.
        permissions: Permission paths (e.g., "Feature.Admin.Purge", "Role.*").
    """
    for permission in permissions:
        try:
            self.bot.permission_manager.register_node(permission, lambda client, node, member, channel: True)
            self.logger.info(f"Permission '{permission}' registered for module '{module_name}'.")
        except Exception as e:
            self.logger.error(f"Failed to register permission '{permission}' for module '{module_name}': {e}")

reload_modules() async

Reload all modules by unloading then loading them again.

Useful during development or when hot‑reloading code on disk.

Source code in handlers/moduleHandler.py
206
207
208
209
210
211
212
async def reload_modules(self):
    """Reload all modules by unloading then loading them again.

    Useful during development or when hot‑reloading code on disk.
    """
    await self.unload_modules()
    await self.load_modules()

unload_modules() async

Unload all modules that were previously loaded.

Calls each module's unload(bot) coroutine (if implemented), removes it from the in‑memory registry, and logs results.

Source code in handlers/moduleHandler.py
192
193
194
195
196
197
198
199
200
201
202
203
204
async def unload_modules(self):
    """Unload all modules that were previously loaded.

    Calls each module's `unload(bot)` coroutine (if implemented), removes it
    from the in‑memory registry, and logs results.
    """
    for module_name, module in list(self.loaded_modules.items()):
        try:
            await module.unload(self.bot)
            del self.loaded_modules[module_name]
            self.logger.info(f"Unloaded module: {module_name}")
        except Exception as e:
            self.logger.error(f"Failed to unload module {module_name}: {e}")

handlers.commandHandler

CommandHandler

Central registry and loader for module commands.

Dynamically imports command files, collects exported objects, and registers: - Slash commands / groups into the app command tree - Prefix commands into the bot - Cogs (async add + bookkeeping) - Detailed help entries - Deferred Subcommand attachments (processed later)

Source code in handlers/commandHandler.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class CommandHandler:
    """Central registry and loader for module commands.

    Dynamically imports command files, collects exported objects, and registers:
      - Slash commands / groups into the app command tree
      - Prefix commands into the bot
      - Cogs (async add + bookkeeping)
      - Detailed help entries
      - Deferred Subcommand attachments (processed later)
    """

    def __init__(self, bot: commands.Bot, logger: Logger):
        """Initialize the handler.

        Args:
            bot: Discord.py bot (or ExtendedClient).
            logger: Logger used for diagnostics.
        """
        self.bot = bot
        self.logger = logger
        self.pending_subcommands = []  # Queue for subcommands waiting for parent groups
        self.detailed_help = {}  # Store detailed help information

    async def load_commands_from_folder(self, folder: Path, base_package: str, module: Module):
        """Import and register all command files found under a folder.

        Recursively walks `folder`, imports each `.py` file as a module under
        `base_package`, then processes its `exports` iterable (if present).

        Args:
            folder: Directory containing command files.
            base_package: Python package prefix to assign to imports (e.g. "modules.foo.commands").
            module: The owning :class:`Module` instance for bookkeeping.
        """
        if not folder.exists():
            self.logger.warning(f"Commands folder '{folder}' does not exist for module '{module.name}'.")
            return

        for command_file in folder.rglob("*.py"):
            if "__pycache__" in command_file.parts:
                continue
            if command_file.stem.startswith("_"):
                continue

            # Determine the module name based on the relative path
            relative_path = command_file.relative_to(folder).with_suffix("")  # Remove .py
            module_name = f"{base_package}.{'.'.join(relative_path.parts)}"

            # Load the module
            spec = importlib.util.spec_from_file_location(module_name, command_file)
            if spec is None:
                self.logger.error(f"Failed to load command module: {command_file} for module '{module.name}'.")
                continue

            module_obj = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = module_obj
            loader = spec.loader
            if loader is None:
                self.logger.error(f"Loader not found for command module: {command_file} in module '{module.name}'.")
                continue

            try:
                loader.exec_module(module_obj)
            except Exception as e:
                self.logger.error(f"Error executing module '{module_name}': {e}")
                continue

            # Process the exported items in the module
            exports = getattr(module_obj, "exports", [])
            for item in exports:
                await self.process_export(item, module)

    async def process_export(self, export: Any, module: Module):
        """Process a single exported symbol from a command file.

        Supported export types:
            - `app_commands.Command` / `app_commands.Group` → slash registration
            - `commands.Command` → prefix command registration
            - `commands.Cog` instance → async add, track commands
            - `CommandHelp` → stored in `bot.detailed_help`
            - `Subcommand` → queued and attached by `process_pending_subcommands`
            - A class with `async def setup(bot)` → invoked
            - A subclass of `commands.Cog` without `setup` → instantiated and added

        Args:
            export: The exported object to register.
            module: The owning module.
        """
        if isinstance(export, app_commands.Command) or isinstance(export, app_commands.Group):
            self.register_slash_command(export, module)
        elif isinstance(export, commands.Command):
            self.register_regular_command(export, module)
        elif isinstance(export, commands.Cog):
            await self._add_cog_async(export, module)
        elif isinstance(export, CommandHelp):
            self.bot.detailed_help[export.name] = export
            self.logger.info(f"Registered detailed help for '{export.name}' in module '{module.name}'.")
        elif isinstance(export, Subcommand):
            self.register_subcommand(export, module)
        elif isclass(export):
            setup_method = getattr(export, "setup", None)
            if setup_method:
                if iscoroutinefunction(setup_method):
                    await setup_method(self.bot)
                    self.logger.info(f"Async setup invoked for '{export.__name__}' in module '{module.name}'.")
                else:
                    self.logger.error(f"Setup method for '{export.__name__}' is not async. Skipping in module '{module.name}'.")
            else:
                if issubclass(export, commands.Cog):
                    try:
                        cog_instance = export(self.bot)
                        await self._add_cog_async(cog_instance, module)
                    except Exception as e:
                        self.logger.error(f"Failed to instantiate cog '{export.__name__}' in module '{module.name}': {e}")
                else:
                    self.logger.error(
                        f"Unrecognized class '{export.__name__}' in module '{module.name}'. It has no setup method and is not a Cog."
                    )
        else:
            self.logger.warning(f"Unrecognized export: {export} in module '{module.name}'")

    def register_slash_command(self, command: app_commands.Command | app_commands.Group, module: Module):
        """Register a slash command or group into the bot's command tree.

        Also records the command under `module.commands["slash"]`.

        Args:
            command: The slash command or group to add.
            module: The owning module used for bookkeeping.
        """
        if isinstance(command, app_commands.Group):
            self.bot.tree.add_command(command)
            self.logger.info(f"Registered command group '{command.name}' in module '{module.name}'.")
            module.commands["slash"][command.name] = command
        else:
            self.bot.tree.add_command(command)
            self.logger.info(f"Registered slash command '{command.name}' in module '{module.name}'.")
            module.commands["slash"][command.name] = command

    def register_regular_command(self, command: commands.Command, module: Module):
        """Register a text (prefix) command into the bot.

        Also records the command under `module.commands["text"]`.

        Args:
            command: The command object to add via `bot.add_command`.
            module: The owning module used for bookkeeping.
        """
        self.bot.add_command(command)
        self.logger.info(f"Registered regular command '{command.name}' in module '{module.name}'.")
        module.commands["text"][command.name] = command

    def register_subcommand(self, subcommand: Subcommand, module: Module):
        """Queue a `Subcommand` for attachment to its parent group.

        If the parent group does not yet exist, the subcommand is queued until
        `process_pending_subcommands()` creates or finds the parent.

        Args:
            subcommand: The subcommand descriptor.
            module: The owning module used for bookkeeping.
        """
        if subcommand.parent_name:
            self.pending_subcommands.append({
                "command": app_commands.Command(
                    name=subcommand.name,
                    description=subcommand.description,
                    callback=subcommand.callback,
                ),
                "parent_name": subcommand.parent_name,
                "module": module  # Associar o módulo ao subcomando
            })
            self.logger.info(f"Queued subcommand '{subcommand.name}' for parent '{subcommand.parent_name}' in module '{module.name}'.")
        else:
            self.logger.warning(f"Subcommand '{subcommand.name}' has no parent group specified in module '{module.name}'.")

    def process_pending_subcommands(self):
        """Attach queued subcommands to their parent groups.

        If a parent group is missing, creates it automatically and adds it to
        the app command tree. Updates `module.commands["slash"]` accordingly.
        """
        for item in self.pending_subcommands[:]:
            parent = self.bot.tree.get_command(item["parent_name"])
            module = item["module"]
            if not parent:
                # Automatically create a parent group if it doesn't exist
                parent = app_commands.Group(
                    name=item["parent_name"],
                    description=f"Group for {item['parent_name']} commands.",
                )
                self.bot.tree.add_command(parent)
                self.logger.info(f"Created missing parent group '{item['parent_name']}' in module '{module.name}'.")

            if isinstance(parent, app_commands.Group):
                parent.add_command(item["command"])
                self.logger.info(f"Added subcommand '{item['command'].name}' to group '{item['parent_name']}' in module '{module.name}'.")
                # Associar o subcomando ao módulo
                module.commands["slash"][item["command"].name] = item["command"]
                self.pending_subcommands.remove(item)
            else:
                self.logger.warning(
                    f"Parent group '{item['parent_name']}' is not a valid group for subcommand '{item['command'].name}' in module '{module.name}'."
                )

    async def _add_cog_async(self, cog_instance: commands.Cog, module: Module):
        """Add a Cog asynchronously and record its commands.

        After adding the Cog, enumerates both text and slash commands available
        from the Cog and stores them under `module.commands`.

        Args:
            cog_instance: Instantiated Cog to add.
            module: The owning module used for bookkeeping.
        """
        try:
            await self.bot.add_cog(cog_instance)
            text_commands = cog_instance.get_commands()
            slash_commands = cog_instance.get_app_commands()

            for command in text_commands:
                module.commands["text"][command.name] = command

            for slash in slash_commands:
                module.commands["slash"][slash.name] = slash

            self.logger.info(f"Registered cog '{type(cog_instance).__name__}' in module '{module.name}'.")
        except Exception as e:
            self.logger.error(f"Failed to register cog '{type(cog_instance).__name__}' in module '{module.name}': {e}")

__init__(bot, logger)

Initialize the handler.

Parameters:

Name Type Description Default
bot Bot

Discord.py bot (or ExtendedClient).

required
logger Logger

Logger used for diagnostics.

required
Source code in handlers/commandHandler.py
25
26
27
28
29
30
31
32
33
34
35
def __init__(self, bot: commands.Bot, logger: Logger):
    """Initialize the handler.

    Args:
        bot: Discord.py bot (or ExtendedClient).
        logger: Logger used for diagnostics.
    """
    self.bot = bot
    self.logger = logger
    self.pending_subcommands = []  # Queue for subcommands waiting for parent groups
    self.detailed_help = {}  # Store detailed help information

load_commands_from_folder(folder, base_package, module) async

Import and register all command files found under a folder.

Recursively walks folder, imports each .py file as a module under base_package, then processes its exports iterable (if present).

Parameters:

Name Type Description Default
folder Path

Directory containing command files.

required
base_package str

Python package prefix to assign to imports (e.g. "modules.foo.commands").

required
module Module

The owning :class:Module instance for bookkeeping.

required
Source code in handlers/commandHandler.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
async def load_commands_from_folder(self, folder: Path, base_package: str, module: Module):
    """Import and register all command files found under a folder.

    Recursively walks `folder`, imports each `.py` file as a module under
    `base_package`, then processes its `exports` iterable (if present).

    Args:
        folder: Directory containing command files.
        base_package: Python package prefix to assign to imports (e.g. "modules.foo.commands").
        module: The owning :class:`Module` instance for bookkeeping.
    """
    if not folder.exists():
        self.logger.warning(f"Commands folder '{folder}' does not exist for module '{module.name}'.")
        return

    for command_file in folder.rglob("*.py"):
        if "__pycache__" in command_file.parts:
            continue
        if command_file.stem.startswith("_"):
            continue

        # Determine the module name based on the relative path
        relative_path = command_file.relative_to(folder).with_suffix("")  # Remove .py
        module_name = f"{base_package}.{'.'.join(relative_path.parts)}"

        # Load the module
        spec = importlib.util.spec_from_file_location(module_name, command_file)
        if spec is None:
            self.logger.error(f"Failed to load command module: {command_file} for module '{module.name}'.")
            continue

        module_obj = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module_obj
        loader = spec.loader
        if loader is None:
            self.logger.error(f"Loader not found for command module: {command_file} in module '{module.name}'.")
            continue

        try:
            loader.exec_module(module_obj)
        except Exception as e:
            self.logger.error(f"Error executing module '{module_name}': {e}")
            continue

        # Process the exported items in the module
        exports = getattr(module_obj, "exports", [])
        for item in exports:
            await self.process_export(item, module)

process_export(export, module) async

Process a single exported symbol from a command file.

Supported export types
  • app_commands.Command / app_commands.Group → slash registration
  • commands.Command → prefix command registration
  • commands.Cog instance → async add, track commands
  • CommandHelp → stored in bot.detailed_help
  • Subcommand → queued and attached by process_pending_subcommands
  • A class with async def setup(bot) → invoked
  • A subclass of commands.Cog without setup → instantiated and added

Parameters:

Name Type Description Default
export Any

The exported object to register.

required
module Module

The owning module.

required
Source code in handlers/commandHandler.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
async def process_export(self, export: Any, module: Module):
    """Process a single exported symbol from a command file.

    Supported export types:
        - `app_commands.Command` / `app_commands.Group` → slash registration
        - `commands.Command` → prefix command registration
        - `commands.Cog` instance → async add, track commands
        - `CommandHelp` → stored in `bot.detailed_help`
        - `Subcommand` → queued and attached by `process_pending_subcommands`
        - A class with `async def setup(bot)` → invoked
        - A subclass of `commands.Cog` without `setup` → instantiated and added

    Args:
        export: The exported object to register.
        module: The owning module.
    """
    if isinstance(export, app_commands.Command) or isinstance(export, app_commands.Group):
        self.register_slash_command(export, module)
    elif isinstance(export, commands.Command):
        self.register_regular_command(export, module)
    elif isinstance(export, commands.Cog):
        await self._add_cog_async(export, module)
    elif isinstance(export, CommandHelp):
        self.bot.detailed_help[export.name] = export
        self.logger.info(f"Registered detailed help for '{export.name}' in module '{module.name}'.")
    elif isinstance(export, Subcommand):
        self.register_subcommand(export, module)
    elif isclass(export):
        setup_method = getattr(export, "setup", None)
        if setup_method:
            if iscoroutinefunction(setup_method):
                await setup_method(self.bot)
                self.logger.info(f"Async setup invoked for '{export.__name__}' in module '{module.name}'.")
            else:
                self.logger.error(f"Setup method for '{export.__name__}' is not async. Skipping in module '{module.name}'.")
        else:
            if issubclass(export, commands.Cog):
                try:
                    cog_instance = export(self.bot)
                    await self._add_cog_async(cog_instance, module)
                except Exception as e:
                    self.logger.error(f"Failed to instantiate cog '{export.__name__}' in module '{module.name}': {e}")
            else:
                self.logger.error(
                    f"Unrecognized class '{export.__name__}' in module '{module.name}'. It has no setup method and is not a Cog."
                )
    else:
        self.logger.warning(f"Unrecognized export: {export} in module '{module.name}'")

process_pending_subcommands()

Attach queued subcommands to their parent groups.

If a parent group is missing, creates it automatically and adds it to the app command tree. Updates module.commands["slash"] accordingly.

Source code in handlers/commandHandler.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def process_pending_subcommands(self):
    """Attach queued subcommands to their parent groups.

    If a parent group is missing, creates it automatically and adds it to
    the app command tree. Updates `module.commands["slash"]` accordingly.
    """
    for item in self.pending_subcommands[:]:
        parent = self.bot.tree.get_command(item["parent_name"])
        module = item["module"]
        if not parent:
            # Automatically create a parent group if it doesn't exist
            parent = app_commands.Group(
                name=item["parent_name"],
                description=f"Group for {item['parent_name']} commands.",
            )
            self.bot.tree.add_command(parent)
            self.logger.info(f"Created missing parent group '{item['parent_name']}' in module '{module.name}'.")

        if isinstance(parent, app_commands.Group):
            parent.add_command(item["command"])
            self.logger.info(f"Added subcommand '{item['command'].name}' to group '{item['parent_name']}' in module '{module.name}'.")
            # Associar o subcomando ao módulo
            module.commands["slash"][item["command"].name] = item["command"]
            self.pending_subcommands.remove(item)
        else:
            self.logger.warning(
                f"Parent group '{item['parent_name']}' is not a valid group for subcommand '{item['command'].name}' in module '{module.name}'."
            )

register_regular_command(command, module)

Register a text (prefix) command into the bot.

Also records the command under module.commands["text"].

Parameters:

Name Type Description Default
command Command

The command object to add via bot.add_command.

required
module Module

The owning module used for bookkeeping.

required
Source code in handlers/commandHandler.py
153
154
155
156
157
158
159
160
161
162
163
164
def register_regular_command(self, command: commands.Command, module: Module):
    """Register a text (prefix) command into the bot.

    Also records the command under `module.commands["text"]`.

    Args:
        command: The command object to add via `bot.add_command`.
        module: The owning module used for bookkeeping.
    """
    self.bot.add_command(command)
    self.logger.info(f"Registered regular command '{command.name}' in module '{module.name}'.")
    module.commands["text"][command.name] = command

register_slash_command(command, module)

Register a slash command or group into the bot's command tree.

Also records the command under module.commands["slash"].

Parameters:

Name Type Description Default
command Command | Group

The slash command or group to add.

required
module Module

The owning module used for bookkeeping.

required
Source code in handlers/commandHandler.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def register_slash_command(self, command: app_commands.Command | app_commands.Group, module: Module):
    """Register a slash command or group into the bot's command tree.

    Also records the command under `module.commands["slash"]`.

    Args:
        command: The slash command or group to add.
        module: The owning module used for bookkeeping.
    """
    if isinstance(command, app_commands.Group):
        self.bot.tree.add_command(command)
        self.logger.info(f"Registered command group '{command.name}' in module '{module.name}'.")
        module.commands["slash"][command.name] = command
    else:
        self.bot.tree.add_command(command)
        self.logger.info(f"Registered slash command '{command.name}' in module '{module.name}'.")
        module.commands["slash"][command.name] = command

register_subcommand(subcommand, module)

Queue a Subcommand for attachment to its parent group.

If the parent group does not yet exist, the subcommand is queued until process_pending_subcommands() creates or finds the parent.

Parameters:

Name Type Description Default
subcommand Subcommand

The subcommand descriptor.

required
module Module

The owning module used for bookkeeping.

required
Source code in handlers/commandHandler.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def register_subcommand(self, subcommand: Subcommand, module: Module):
    """Queue a `Subcommand` for attachment to its parent group.

    If the parent group does not yet exist, the subcommand is queued until
    `process_pending_subcommands()` creates or finds the parent.

    Args:
        subcommand: The subcommand descriptor.
        module: The owning module used for bookkeeping.
    """
    if subcommand.parent_name:
        self.pending_subcommands.append({
            "command": app_commands.Command(
                name=subcommand.name,
                description=subcommand.description,
                callback=subcommand.callback,
            ),
            "parent_name": subcommand.parent_name,
            "module": module  # Associar o módulo ao subcomando
        })
        self.logger.info(f"Queued subcommand '{subcommand.name}' for parent '{subcommand.parent_name}' in module '{module.name}'.")
    else:
        self.logger.warning(f"Subcommand '{subcommand.name}' has no parent group specified in module '{module.name}'.")

handlers.eventHandler

EventHandler

Dynamic loader/registrar for module events.

Imports Python files from each module's events folder and registers exported event listeners on the bot using bot.add_listener.

Source code in handlers/eventHandler.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class EventHandler:
    """Dynamic loader/registrar for module events.

    Imports Python files from each module's `events` folder and registers exported
    event listeners on the bot using `bot.add_listener`.
    """

    def __init__(self, bot: commands.Bot, logger: Logger):
        """Initialize the handler.

        Args:
            bot: Discord.py bot (or ExtendedClient).
            logger: Logger used for diagnostics.
        """
        self.bot = bot
        self.logger = logger

    def load_events_from_module(self, module_name: str, events_path: Path, module: Module):
        """Import and register event listeners from a module's `events` directory.

        Each event file may expose an `exports` list of dicts with keys:
            - "event": Discord.py event name (e.g., "on_message")
            - "func": callable listener to be registered via `bot.add_listener`

        Args:
            module_name: Module's folder name (used to build import name).
            events_path: Path to the module's events directory.
            module: The owning :class:`Module` to record listeners against.
        """
        if not events_path.exists() or not events_path.is_dir():
            self.logger.warning(f"Events folder '{events_path}' not found for module '{module_name}'.")
            return

        # Recursively search for .py files in the events directory
        for event_file in events_path.rglob("*.py"):
            if event_file.stem.startswith("_"):  # Skip special files like __init__.py
                continue

            # Import the event module

            event_module_name = f"modules.{module_name}.events.{event_file.stem}"  # Inclui 'modules.'

            spec = importlib.util.spec_from_file_location(event_module_name, event_file)
            if spec is None:
                self.logger.error(f"Failed to load event '{event_file}' in module '{module_name}'.")
                continue

            event_module = importlib.util.module_from_spec(spec)
            sys.modules[event_module_name] = event_module
            loader = spec.loader
            if loader is None:
                self.logger.error(f"Loader not found for event '{event_file}' in module '{module_name}'.")
                continue

            try:
                loader.exec_module(event_module)
            except Exception as e:
                self.logger.error(f"Error executing event '{event_file}' in module '{module_name}': {e}")
                continue

            # Register event listeners
            if hasattr(event_module, "exports"):
                exports = getattr(event_module, "exports")
                if isinstance(exports, list):
                    for export in exports:
                        if isinstance(export, dict):
                            event_name = export.get("event")
                            func = export.get("func")
                            if event_name and callable(func):
                                try:
                                    self.bot.add_listener(func, event_name)
                                    module.register_event(event_name, func)  # Register in the module
                                    self.logger.info(f"Registered event '{event_name}' from module '{module_name}'.")
                                except Exception as e:
                                    self.logger.error(f"Error registering event '{event_name}' from module '{module_name}': {e}")

__init__(bot, logger)

Initialize the handler.

Parameters:

Name Type Description Default
bot Bot

Discord.py bot (or ExtendedClient).

required
logger Logger

Logger used for diagnostics.

required
Source code in handlers/eventHandler.py
17
18
19
20
21
22
23
24
25
def __init__(self, bot: commands.Bot, logger: Logger):
    """Initialize the handler.

    Args:
        bot: Discord.py bot (or ExtendedClient).
        logger: Logger used for diagnostics.
    """
    self.bot = bot
    self.logger = logger

load_events_from_module(module_name, events_path, module)

Import and register event listeners from a module's events directory.

Each event file may expose an exports list of dicts with keys: - "event": Discord.py event name (e.g., "on_message") - "func": callable listener to be registered via bot.add_listener

Parameters:

Name Type Description Default
module_name str

Module's folder name (used to build import name).

required
events_path Path

Path to the module's events directory.

required
module Module

The owning :class:Module to record listeners against.

required
Source code in handlers/eventHandler.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def load_events_from_module(self, module_name: str, events_path: Path, module: Module):
    """Import and register event listeners from a module's `events` directory.

    Each event file may expose an `exports` list of dicts with keys:
        - "event": Discord.py event name (e.g., "on_message")
        - "func": callable listener to be registered via `bot.add_listener`

    Args:
        module_name: Module's folder name (used to build import name).
        events_path: Path to the module's events directory.
        module: The owning :class:`Module` to record listeners against.
    """
    if not events_path.exists() or not events_path.is_dir():
        self.logger.warning(f"Events folder '{events_path}' not found for module '{module_name}'.")
        return

    # Recursively search for .py files in the events directory
    for event_file in events_path.rglob("*.py"):
        if event_file.stem.startswith("_"):  # Skip special files like __init__.py
            continue

        # Import the event module

        event_module_name = f"modules.{module_name}.events.{event_file.stem}"  # Inclui 'modules.'

        spec = importlib.util.spec_from_file_location(event_module_name, event_file)
        if spec is None:
            self.logger.error(f"Failed to load event '{event_file}' in module '{module_name}'.")
            continue

        event_module = importlib.util.module_from_spec(spec)
        sys.modules[event_module_name] = event_module
        loader = spec.loader
        if loader is None:
            self.logger.error(f"Loader not found for event '{event_file}' in module '{module_name}'.")
            continue

        try:
            loader.exec_module(event_module)
        except Exception as e:
            self.logger.error(f"Error executing event '{event_file}' in module '{module_name}': {e}")
            continue

        # Register event listeners
        if hasattr(event_module, "exports"):
            exports = getattr(event_module, "exports")
            if isinstance(exports, list):
                for export in exports:
                    if isinstance(export, dict):
                        event_name = export.get("event")
                        func = export.get("func")
                        if event_name and callable(func):
                            try:
                                self.bot.add_listener(func, event_name)
                                module.register_event(event_name, func)  # Register in the module
                                self.logger.info(f"Registered event '{event_name}' from module '{module_name}'.")
                            except Exception as e:
                                self.logger.error(f"Error registering event '{event_name}' from module '{module_name}': {e}")