Basics: Minimal Module
What MUST be there
Any module MUST contain:
| # | Thing | Where | Required? |
|---|---|---|---|
| 1 | class Meta |
at module level (outside the Module class) | YES |
| 2 | name, description, version, tags |
inside Meta | YES |
| 3 | @loader.tds |
above the Module class | YES |
| 4 | class XxxModule(loader.Module) |
any class inheriting loader.Module |
YES |
| 5 | strings = {} |
inside the Module class | YES |
The class name must end with Module. That's how the bot identifies the entry point.
For example:
- HelloModule — correct
- WikipediaModule — correct
- MyEpicModule — correct
The filename can be anything. But by convention:
- File hello.py → class HelloModule
- File wikipedia.py → class WikipediaModule
Minimal Working Module
Create a file hello.py:
from mxc import utils
from .. import loader
class Meta:
name = "HelloWorld"
description = "Hello world, my first module"
version = "1.0.0"
tags = ["test"]
@loader.tds
class HelloWorldModule(loader.Module):
strings = {
"hello": "Hello, world!",
}
@loader.command()
async def hello(self, mx, event):
"""Say hello"""
await utils.answer(mx, self.strings["hello"], event=event)
That's it. This is a working module.
Let's Break Down Each Part
1. Imports
utils— functions for replies, requests, etc.loader— decorators for commands, watchers, etc.
2. class Meta — REQUIRED
class Meta:
name = "HelloWorld" # module name
description = "..." # description
version = "1.0.0" # version
tags = ["test"] # tags for searching
Meta must be at module level, not inside the Module class. Without it, the module simply won't load.
Optional fields:
author = "https://github.com/username" # author (github link)
dependencies = ["aiohttp"] # pip dependencies
3. @loader.tds — REQUIRED
Class decorator. Without it, the module won't be registered.
What it does:
- Checks that strings = {} exists
- Collects command documentation
- Registers the module in the system
4. strings = {} — REQUIRED
Why is this needed?
- Convenience: all text in one place, not scattered across the code
- Don't wanna bother? Leave strings = {} empty
- Future: strings are needed for the translation system (i18n)
You can use HTML:
5. Command
@loader.command()
async def hello(self, mx, event):
"""Say hello"""
await utils.answer(mx, self.strings["hello"], event=event)
- Function name = command name.
async def hello(...)→ command.hello self— module instancemx— bot interface (client,fsm,security)event— message event- Docstring
"""Say hello"""— this is the command help, REQUIRED
Module Lifecycle
If you need to do something on start or stop:
@loader.tds
class MyModule(loader.Module):
strings = {"start": "Module started!"}
async def _matrix_start(self, mx):
"""Called when the module loads"""
self.log.info("Module is starting")
await utils.answer(mx, self.strings["start"], room_id="!logs:server")
async def _matrix_stop(self, mx):
"""Called when the module unloads"""
self.log.info("Module is stopping")
_matrix_start— async, asynchronous initialization_matrix_stop— async only
Database (key-value)
Every module has access to the database via self._get and self._set.
# Save a value
await self._set("my_key", "my_value")
await self._set("counter", 42)
# Read a value
value = await self._get("my_key") # → "my_value"
counter = await self._get("counter", 0) # → 42 (default if missing)
# Delete (pass None — it gets deleted)
await self._set("my_key", None)
Note: Community modules only see THEIR OWN keys. ScopedDatabase automatically prefixes keys with the module name.
Counter example:
@loader.command()
async def counter(self, mx, event):
"""Command counter"""
count = await self._get("count", 0)
count += 1
await self._set("count", count)
await utils.answer(mx, f"Count: {count}", event=event)
config — module settings
Via config the user can configure the module without touching the code.
config = {
"api_key": loader.ConfigValue(
default=None, description="API key", required=True,
),
"delay": loader.ConfigValue(
default=5, description="Delay (sec)",
validator=lambda x: isinstance(x, int) and x >= 0,
),
}
required=True— won't let anyone use commands until it's filled indefault=None— if required, always set None, not an empty stringvalidator— validation function, returns True/False
Accessing config:
self.config.get("api_key") # read
self.config["api_key"] # read (short)
self.config.set("api_key", val) # write
await self.config.set_async("api_key", val) # write (async)
Logging
Logs at WARNING level and above are automatically sent to the log room.
User Errors
If a user misuses a command:
from mxc.exceptions import UsageError
@loader.command()
async def give(self, mx, event, who: str = None):
"""<who> — give something"""
if not who:
raise UsageError("You need to specify who!")
The bot will automatically show the command help.
What to Remember
class Meta— required, at module level@loader.tds— required above the classstrings = {}— required inside the class- Class name must contain
Moduleat the end (HelloModule,WikipediaModule) - Community modules only see their own data in the DB (ScopedDatabase)
- Don't touch
sys,subprocess,socket— the firewall won't let you