Introduction
Welcome to the maubot docs!
This book contains usage and developer documentation for maubot. The book is built using mdBook. The source code is available in the mautrix/docs repo.
Discussion
Matrix room: #maubot:maunium.net
Setup
Requirements
Production setup
- Create a directory (
mkdir maubot
) and enter it (cd maubot
). Do not clone the repository. If you want to use a specific version from git rather than PyPI, use the development setup instructions. - Set up a virtual environment.
- Create with
virtualenv -p /usr/bin/python3 .
(note the dot at the end)- You should not use a subdirectory for the virtualenv in this production setup. The pip install step places some required files at the root of the environment.
- Activate with
source ./bin/activate
- Create with
- Install with
pip install --upgrade maubot
- Copy
example-config.yaml
toconfig.yaml
and update to your liking. - Create the log directory and all directories used in
plugin_directories
(usuallymkdir plugins trash logs
). - Start with
python3 -m maubot
. - The management interface should now be available at http://localhost:29316/_matrix/maubot or whatever you configured.
Upgrading
- Run the install command again (step #3).
- Restart maubot.
Development setup
- Clone the repository.
- Optional, but strongly recommended: Set up a virtual environment.
- Create with
virtualenv -p /usr/bin/python3 .venv
- Activate with
source .venv/bin/activate
- Create with
- Install with
pip install --editable .
(note the dot at the end) - Build the frontend:
cd maubot/management/frontend
- Install dependencies with
yarn
- Build with
yarn build
- Optional: Configure debug file open so that you can open files in your IDE by clicking on stack trace lines in the frontend log viewer.
- Continue from step 4 of the production setup. Note that the example config
to copy will be inside the
maubot
directory, not in the repo root.
Upgrading
- Pull changes from Git.
- Update dependencies with
pip install --upgrade -r requirements.txt
. - Restart maubot.
Setup with Docker
Docker images are hosted on dock.mau.dev
- Create a directory (
mkdir maubot
) and enter it (cd maubot
). - Pull the docker image with
docker pull dock.mau.dev/maubot/maubot:<version>
. Replace<version>
with the version you want to run (e.g.latest
) - Run the container for the first time, so it can create a config file for you:
docker run --rm -v $PWD:/data:z dock.mau.dev/maubot/maubot:<version>
- Update the config to your liking.
- Run maubot:
docker run --restart unless-stopped -p 29316:29316 -v $PWD:/data:z dock.mau.dev/maubot/maubot:<version>
- The management interface should now be available at http://localhost:29316/_matrix/maubot or whatever you configured.
Upgrading
- Pull the new version (setup step 1).
- Restart the container.
Reverse proxying
The maubot management interface has a log viewer which uses a websocket to get
real-time logs from the backend. If you're using a reverse proxy (as you should)
in most cases, you probably need to configure it to allow websockets for the
/_matrix/maubot/v1/logs
endpoint.
Caddy
Caddy 2 supports websockets out of the box with no additional configuration.
example.com {
reverse_proxy /_matrix/maubot http://localhost:29316
}
Nginx
server {
listen 443 ssl;
...
location /_matrix/maubot/v1/logs {
proxy_pass http://localhost:29316;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Forwarded-For $remote_addr;
}
location /_matrix/maubot {
proxy_pass http://localhost:29316;
proxy_set_header X-Forwarded-For $remote_addr;
}
...
}
Basic usage
After setting up maubot, you can use the web management interface to make it do things. The default location of the management interface is http://localhost:29316/_matrix/maubot.
Logging into the interface happens with the credentials configured in the
admins
section in the config. Note that the root
user can't have a password,
so you must use a different username for logging in.
Uploading plugins
Plugins contain the actual code for a bot, so the first step is to upload a plugin. There's currently no central location to download plugins, but there's a list of plugins at plugins.mau.bot.
Common places to get plugins:
- GitHub releases (assets section), e.g. https://github.com/maubot/echo/releases
- mau.dev CI, e.g. https://mau.dev/maubot/github/-/pipelines
- Cloning the repo and building it yourself:
mbc build
will package everything properly and output the.mbp
file- you can also zip it yourself with
zip -9r plugin.mbp *
After you have the .mbp
file, click the +
button next to the "Plugins"
header and drop your .mbp
file in the upload box.
Creating clients
Each Matrix account you want to use as a bot needs to be added as a client. You
could have one bot account that does everything, or many bot accounts with
different purposes. To create a client, click the +
button next to the
"Clients" header and fill in the form.
- The homeserver dropdown gets values from the config's
homeservers
section, but you can also type a full URL in that box. - The access token and device ID are acquired by logging into the account.
There are several ways to do that, including the
mbc auth
command included in maubot.- Note that encryption won't work if you reuse the token from an e2ee-capable client like Element, so make sure you get a fresh token if you want to use the bot in encrypted rooms.
- The avatar URL can be left empty, you can upload an image after creating the client.
- Both the display name and avatar URL can be set to
disable
if you don't want maubot to change them. If left empty, maubot will remove the displayname and avatar on Matrix.
Creating instances
Instances are how plugins are linked to clients. After you have uploaded a
plugin and created a client, simply click the +
button next to the "Instances"
header, choose your client and plugin in the relevant dropdowns, invent an
instance ID and click "Create".
You may have to refresh the management UI if the plugin and client you created don't show up in the dropdowns.
If the plugin has any configuration options, the config editor will show up under the created instance.
Encryption
Dependencies
To enable encryption, you must first have maubot installed with the encryption
optional dependency. To do this, you can either add [encryption]
at the end
of the package in the pip install
command, e.g. pip install --upgrade maubot[encryption]
.
Alternatively, you can install the dependencies manually (python-olm
,
pycryptodome
and unpaddedbase64
). The Docker image has all optional
dependencies installed by default.
Note that installing python-olm
requires libolm3 with dev headers, Python dev
headers, and a C compiler. This means libolm-dev
, python3-dev
and
build-essential
on Debian 11+ and Ubuntu 19.10+.
If you want to avoid the dev headers, you can install the libolm3
package
without -dev and get a pre-compiled python-olm from gitlab.matrix.org's PyPI
registry. However, this method has not been tested properly, so it might not
work at all.
pip install python-olm --extra-index-url https://gitlab.matrix.org/api/v4/projects/27/packages/pypi/simple
To install python-olm on macOS, you can use libolm from homebrew like this:
brew install libolm
pip3 install python-olm --global-option="build_ext" --global-option="--include-dirs="`brew --prefix libolm`"/include" --global-option="--library-dirs="`brew --prefix libolm`"/lib"
Getting a fresh device ID
When using maubot with encryption, you must have an access token and a device ID
that haven't been used in an e2ee-capable client. In other words, you can't take
the access token from Element, you have to log in manually. The easiest way to
do that is to use mbc auth
.
Actually enabling encryption
After installing dependencies, put the device ID in the maubot client, either
using the web UI or just the --update-client
flag with mbc auth
.
maubot cli
Maubot includes the mbc
(maubot CLI) tool for some management
tasks.
When you install maubot, mbc
will be available inside the virtual environment
automatically. You can install maubot locally without actually running the
server to get the mbc
command.
To get started with mbc
, first log into your maubot instance using mbc login
.
mbc login
The mbc login
command logs you into your maubot instance to enable most other
mbc
commands like uploading plugins (mbc upload
) or logging into bot
accounts (mbc auth
).
Running mbc login
without arguments will prompt you to provide the instance
details interactively. Alternatively, you can use the CLI flags to log in
non-interactively. See mbc login --help
for the list of flags.
mbc auth
The mbc auth
command can be used to log into Matrix accounts.
To log in with mbc auth
, first make sure you have your homeserver listed in
the homeservers
section in the maubot config (the secret can be empty). If you
haven't used the mbc
tool before, log into your maubot instance with
mbc login
. Finally, run mbc auth
and fill in the parameters:
- The homeserver is the dictionary key, i.e. server name (not URL) from
the
homeservers
config. - The username can be either the username or full user ID, that doesn't matter.
- The password is the password.
If the command says "Registration target server not found", it means you didn't
add the server to homeservers
properly or didn't enter the correct name in
mbc
.
If you want to register an account, you must pass --register
as a parameter.
This also requires the secret
to be set in the config.
Single sign-on is also supported by using the --sso
flag. When that flag is
passed, the command will open the login page in a browser instead of prompting
for username and password.
Additionally, there's a --update-client
parameter that tells maubot to store
the created access token as a client instance so you don't have to do it
manually in the web interface.
mbc build
The mbc build
command zips plugin files into a .mbp
file that can be
uploaded to the maubot server.
The simplest way to use it is to run mbc build
in the directory that contains
maubot.yaml
, and it will output the matching .mbp
file in the same
directory.
There are also some other options available:
mbc build <directory>
to build the plugin from a different directory.mbc build -o <path>
to output the.mbp
file to a different location.mbc build -u
to upload the plugin directly to a server instead of saving it to a local file.
The -u
/--upload
flag is especially useful when developing plugins, as the
server will reload all instances so you can test the changes immediately.
Management API
Most of the API is simple HTTP+JSON and has OpenAPI documentation (see spec.yaml, rendered). However, some parts of the API aren't documented in the OpenAPI document.
Matrix API proxy
The full Matrix API can be accessed for each client with a request to
/_matrix/maubot/v1/proxy/<user>/<path>
. <user>
is the Matrix user
ID of the user to access the API as and <path>
is the whole API
path to access (e.g. /_matrix/client/r0/whoami
).
The body, headers, query parameters, etc are sent to the Matrix server as-is, with a few exceptions:
- The
Authorization
header will be replaced with the access token for the Matrix user from the maubot database. - The
access_token
query parameter will be removed.
Log viewing
- Open websocket to
/_matrix/maubot/v1/logs
. - Send authentication token as a plain string.
- Server will respond with
{"auth_success": true}
and then with{"history": ...}
where...
is a list of log entries. - Server will send new log entries as JSON.
Log entry object format
Log entries are a JSON-serialized form of Python log records.
Log entries will always have:
id
- A string that should uniquely identify the row. Currently uses therelativeCreated
field of Python logging records.msg
- The text in the entry.time
- The ISO date when the log entry was created.
Log entries should also always have:
levelname
- The log level (e.g.DEBUG
orERROR
).levelno
- The integer log level.name
- The name of the logger. Common values:maubot.client.<mxid>
- Client loggers (Matrix HTTP requests)maubot.instance.<id>
- Plugin instance loggersmaubot.loader.zip
- The zip plugin loader (plugins don't have their own logs)
module
- The Python module name where the log call happened.pathname
- The full path of the file where the log call happened.filename
- The file name portion ofpathname
lineno
- The line in code where the log call happened.funcName
- The name of the function where the log call happened.
Log entries might have:
exc_info
- The formatted exception info if an exception was logged.matrix_http_request
- The info about a Matrix HTTP request. Subfields:method
- The HTTP method used.path
- The API path used.content
- The content sent.user
- The appservice user who the request was ran as.
Debug file open
For debug and development purposes, the API and frontend support clicking on lines in stack traces to open that line in the selected editor.
Configuration
First, the directory where maubot is run from must have a
.dev-open-cfg.yaml
file. The file should contain the following
fields:
editor
- The command to run to open a file.$path
is replaced with the full file path.$line
is replaced with the line number.
pathmap
- A list of find-and-replaces to execute on paths. These are needed to map things like.mbp
files to the extracted sources on disk. Each pathmap entry should have:find
- The regex to match.replace
- The replacement. May insert capture groups with Python syntax (e.g.\1
)
Example file:
editor: pycharm --line $line $path
pathmap:
- find: "maubot/plugins/xyz\\.maubot\\.(.+)-v.+(?:-ts[0-9]+)?.mbp"
replace: "mbplugins/\\1"
- find: "maubot/.venv/lib/python3.6/site-packages/mautrix"
replace: "mautrix-python/mautrix"
API
Clients can GET /_matrix/maubot/v1/debug/open
to check if the file
open endpoint has been set up. The response is a JSON object with a
single field enabled
. If the value is true, the endpoint can be used.
To open files, clients can POST /_matrix/maubot/v1/debug/open
with
a JSON body containing
path
- The full file path to openline
- The line number to open
Standalone mode
The normal mode in maubot is very dynamic: the config file doesn't really contain any runtime details other than a general web server, database, etc. Everything else is set up at runtime using the web management interface or the management API directly. This dynamicness is very useful for developing bots and works fine for deploying it on personal servers, but it's not optimal for larger production deployments.
The solution is standalone mode: a separate entrypoint that runs a single maubot plugin with a predefined Matrix account.
Additionally, standalone mode supports using appservice transactions to receive events instead of /sync, which is often useful for huge production instances with lots of traffic.
Basic usage
- Set up a virtual environment.
- Create with
virtualenv -p /usr/bin/python3 .venv
- Activate with
source .venv/bin/activate
- Create with
- Install maubot into the virtualenv with
pip install --upgrade maubot[all]
[all]
at the end will install all optional dependencies! The e2ee optional dependency requires libolm3 to be installed natively.- Alternatively install the latest git version with
pip install --upgrade maubot[all]@git+https://github.com/maubot/maubot.git
- Extract the plugin you want to run into the directory
.mbp
files can be extracted withunzip
.- You can also just clone the plugin repository and use it directly.
- After extracting, you should have
maubot.yaml
and some Python modules in the directory.
- Install any dependencies that the plugin has into the virtualenv manually
(they should be listed in
maubot.yaml
). - Copy the standalone example config to the same directory as
config.yaml
and fill it out.- If the plugin has a config, you should copy the contents from the plugin's
base-config.yaml
into theplugin_config
object in the standalone config, then fill it out as needed.
- If the plugin has a config, you should copy the contents from the plugin's
- Run the bot with
python -m maubot.standalone
Getting started
This guide assumes you already have a maubot instance set up.
A maubot plugin file (.mbp
) is a zip file that contains maubot.yaml
and some
Python modules. The maubot.yaml
file contains metadata for maubot, such as the
plugin's ID and what Python modules it contains.
The Plugin metadata page documents all options
available in maubot.yaml
. A minimal meta file looks like this:
maubot: 0.1.0
id: com.example.examplebot
version: 1.0.0
license: MIT
modules:
- examplebot
main_class: ExampleBot
The file above will tell maubot to load the examplebot
Python module and find
the ExampleBot
class inside it. Note that Python modules are currently loaded
into the global context, so they must be unique. The class must inherit the
Plugin
class from maubot. A simple bot that does nothing would therefore look
like this:
from maubot import Plugin
class ExampleBot(Plugin):
pass
A bot that does nothing is a bit boring, so let's make it do something. The
maubot.handlers
module contains decorators that can be used to define event
handlers, chat commands and HTTP endpoints. Most bots use commands, so let's
start with that.
The simplest command handlers are simply methods that take one parameter (the
event) and are decorated with @command.new()
. The method name will be used as
the command.
from maubot import Plugin, MessageEvent
from maubot.handlers import command
class ExampleBot(Plugin):
@command.new()
async def hello_world(self, evt: MessageEvent) -> None:
await evt.reply("Hello, World!")
With the maubot.yaml meta file above and this Python file saved as
examplebot/__init__.py
, you can build the plugin and try it out. To build
plugins, you can either use mbc build
or just zip it yourself
(zip -9r plugin.mbp *
in the directory with maubot.yaml
). After you have
the .mbp
file, upload it to your maubot instance (see Basic usage), then try
to use the !hello-world
command.
If you make any changes, you can use mbc build --upload
to build and upload
the plugin directly to the server. Any plugin instances will be reloaded
automatically so you can try out your changes immediately after uploading.
Python tips
This page collects various details about Python and how maubot uses modern Python features. It is not yet ready, but the info below might already be of some use.
asyncio
maubot and mautrix-python use the asyncio library in Python 3. This means that everything is single-threaded, but still asynchronous.
When writing a plugin that interacts with some network service, it is strongly recommended to use asyncio libraries for that interaction. If you use a traditional synchronous library, it will block everything else running on the same maubot instance.
For example, if you need to make HTTP requests, use aiohttp instead of
requests. Plugins have convenient access to an aiohttp client instance through
the http
property in the plugin base class.
If there are no asyncio libraries available for the thing you want to do, you
can run the synchronous methods in a separate thread using asyncio's built-in
run_in_executor
with a ThreadPoolExecutor
.
If you haven't used asyncio before, you may want to read a guide on the topic before developing maubot plugins. Some potentially good ones are:
- https://lonami.dev/blog/asyncio/ (short)
- https://realpython.com/async-io-python/ (a bit longer)
- https://superfastpython.com/python-asyncio/ (very long)
Type hints
Most of the methods in maubot and mautrix-python have proper type hints. Even Matrix events are parsed into convenient type-hinted objects. Using an editor that provides autocompletion based on type hints is recommended.
API reference
This part of the docs contains method/field references for various classes in maubot. These are currently not autogenerated, as I haven't found a good Python API reference generator.
Plugin metadata
The maubot.yaml
file can contain the following fields:
maubot
- The minimum version of maubot that the plugin requires. Currently only v0.1.0 exists, so the field doesn't do anything yet.id
- An unique identifier for the plugin. It should follow Java package naming conventions (use your own domain, notxyz.maubot
).version
- The version of the plugin in PEP 440 format.license
- The SPDX license identifier for the plugin. Optional, assumes all rights reserved if omitted.modules
- A list of Python modules that the plugin includes.- Python modules are either directories with an
__init__.py
file, or simply Python files. - Submodules that are imported by modules listed here don't need to be listed separately. However, top-level modules must always be listed even if they're imported by other modules.
- Modules are loaded in the given order, which means that dependencies must be first, and usually the module containing your main class will be last.
- Currently module names must be globally unique.
- Python modules are either directories with an
main_class
- The main class of the plugin asmodule/ClassName
.- If
module/
is omitted, maubot will look for the class in the last module specified in themodules
list. - Even if the module is not omitted, it must still be listed in the
modules
array.
- If
extra_files
- An instruction for thembc build
command to bundle additional files in the.mbp
file. Used for things like example configs.dependencies
- A list of Python modules and their version ranges that the plugin requires. This is currently not used, but in the future maubot will offer to automatically install dependencies when uploading a plugin.- This should only include top-level dependencies of the plugin, i.e. things
that you explicitly
import
. Don't specify transitive dependencies. - Core maubot dependencies should also not be specified. Specifically, don't
include
mautrix
,aiohttp
,yarl
,asyncpg
,aiosqlite
,ruamel.yaml
orattrs
. Also obviously don't include maubot itself. - It's recommended to specify version ranges (e.g. based on semver), not exact versions.
- This should only include top-level dependencies of the plugin, i.e. things
that you explicitly
soft_dependencies
- Same asdependencies
, but not required for the plugin to function.config
- Whether the plugin has a configurationwebapp
- Whether the plugin registers custom HTTP handlersdatabase
- Whether the plugin has a database
Plugin fields and lifecycle methods
The Plugin
base class has various lifecycle methods and properties that may be
useful for plugins.
Fields
client
- The mautrix client instance for the bot, can be used to make arbitrary Matrix API requests.http
- An aiohttp client instance, can be used to make arbitrary HTTP requests.id
- The ID of the plugin instance.log
- A logger for the plugin instance.loop
- The asyncio event loop.loader
- The class used to load the plugin files. Can be used to read arbitrary files from the plugin's .mbp archive.config
- If the config is enabled, the data from the config (see the Configuration page).database
- If the database is enabled, the database engine (see the Database page).webapp
- If the HTTP handlers are enabled, the aiohttpUrlDispatcher
for the plugin (see the HTTP handlers) page.webapp_url
- If the HTTP handlers are enabled, the public base URL where the endpoints are exposed.
Methods
register_handler_class(object)
- Register another object where handlers are read from (see the Handlers page).
Lifecycle methods
These are methods that the plugin itself can override.
async def start()
- Called when the plugin instance is starting.async def stop()
- Called when the plugin instance is stopping.async def pre_start()
- Called when the plugin instance is starting, before the handlers of the main class are registered.async def pre_stop()
- Called when the plugin instance is stopping, before any handlers are unregistered.def get_config_class() -> Type[Config]
- When the plugin has config, this must return the class used for parsing and updating the config.def on_external_config_update()
- Called when the plugin instance's config is updated from the API. By default, this will callself.config.load_and_update()
.
MessageEvent reference
These methods are available in maubot's MessageEvent
class.
async react(key: str) -> EventID
- React to the command with the given key. The key can be arbitrary unicode text, but usually reactions are emojis.async mark_read()
- Send a read receipt for the command event.async respond(content) -> EventID
- Respond to a message without replying. Optional parameters:event_type
(defaults toEventType.ROOM_MESSAGE
) - The event type to send the response as.markdown
(defaults toTrue
) - Whether thecontent
should be parsed as markdown. Only applies if the content parameter is a string.allow_html
(defaults toTrue
) - Whether thecontent
should allow HTML tags. Only applies if the content parameter is a string.edits
(optional, event ID orMessageEvent
, defaults toNone
) - The event that the response event should edit.in_thread
(optional) - Whether the response should be in a thread with the command. By default (None
), the response is in a thread if the command is in a thread. IfFalse
, the response will never be in a thread. IfTrue
, the response will always be in a thread (creating a thread with the command as the root if necessary).
async reply(content) -> EventID
- Reply to a message. Same parameters asrespond()
, except noedits
option.async edit(content) -> EventID
- Edit the event. Same parameters asreply()
. Note that while this won't throw an error for editing non-own messages, most clients won't render such edits.content
- Some useful methods are also inside the content property:relates_to
- A property containing the event'sm.relates_to
data wrapped in aRelatesTo
object.get_reply_to() -> EventID
- Get the event ID the command is replying to.get_edit() -> EventID
- Get the event ID the command is editing.
Event fields
These are the fields in the MessageEvent
type (and also all other room events)
. They're a part of the Matrix spec, but maubot parses them into convenient
objects.
room_id
- The ID of the room where the event was sent.event_id
- The ID of the event.sender
- The ID of the user who sent the event.timestamp
- The Unix timestamp of the event with millisecond precision (this is calledorigin_server_ts
in the protocol).type
- The event type as anEventType
enum instance.content
- The content of the event. This is parsed into specific classes depending on the event type andmsgtype
. If the type isn't recognized, this will be parsed into anObj
, which is just a dict that allows accessing fields with the dot notation.unsigned
- Additional information that's not signed in the federation protocol. Usually set by the local homeserver.transaction_id
- If the message was sent by the current user, the transaction ID that was used to send the event.
m.room.message
content
These are the fields in the MessageEventContent
type. As with event fields,
they're a part of the Matrix spec.
body
- The plaintext body.msgtype
- The message type as aMessageType
enum instance.
For m.text
, m.notice
and m.emote
(TextMessageEventContent
):
format
- The format forformatted_body
as aFormat
enum instance.formatted_body
- The formatted body (usually HTML).
For m.image
, m.video
, m.audio
and m.file
(MediaMessageEventContent
):
url
- Themxc://
URL to the file (if unencrypted).file
- The URL and encryption keys for the file (if encrypted).- TODO: add methods to conveniently decrypt data using the
EncryptedFile
object.
- TODO: add methods to conveniently decrypt data using the
info
- Additional info about the file. Everything here is optional and set by the sender's client (i.e. not trustworthy).mimetype
- The mime type of the file.size
- The size of the file in bytes.- TODO: document msgtype-specific info fields.
For m.location
(LocationMessageEventContent
):
geo_uri
- The coordinates the event is referring to as ageo:<latitude>,<longitude>
URI.info
- Optional info containing a thumbnail (same as image thumbnails).
Handlers
Plugins can register various types of handlers using decorators from the
maubot.handlers
module.
Currently, there are three types of handlers:
command
for conveniently defining command handlers.event
for handling raw Matrix events.web
for adding custom HTTP endpoints.
Splitting handlers into multiple files
By default, maubot will only scan the plugin's main class for decorated methods.
If you want to split handlers into multiple files, you'll have to use the
register_handler_class
method.
second_handler.py
:
from maubot import MessageEvent
from maubot.handlers import command
class SecondHandlerClass:
@command.new()
async def example(self, evt: MessageEvent) -> None:
pass
__init__.py
:
from maubot import Plugin
from .second_handler import SecondHandlerClass
class SplitBot(Plugin):
async def start(self) -> None:
handler = SecondHandlerClass()
self.register_handler_class(handler)
Command handlers
Commands are the most common way of interacting with bots. The command system in maubot lets you define commands, subcommands, argument parsing and more.
The simplest command handler is just a method that takes a MessageEvent
and is
decorated with @command.new()
. The MessageEvent
object contains all the info
about the event (sender, room ID, timestamp, etc) and some convenience methods,
like replying to the command (evt.reply
). See the MessageEvent reference for
more details on that.
from maubot import Plugin, MessageEvent
from maubot.handlers import command
class SimpleBot(Plugin):
@command.new()
async def command(self, evt: MessageEvent) -> None: ...
@command.new()
parameters
The @command.new()
decorator has various parameters which can be used to
change how the command itself works:
name
The name of the command. This defaults to the name of the method. The parameter
can either be a string, or a function. The function can take either zero or one
argument (the Plugin
instance). The latter case is meant for making the name
configurable.
A command defined like this would be ran with !hello-world
:
from maubot import Plugin, MessageEvent
from maubot.handlers import command
class CustomNameBot(Plugin):
@command.new(name="hello-world")
async def hmm(self, evt: MessageEvent) -> None: ...
Here get_command_name
is a function that takes one argument, self
. It then
gets the command_prefix
config field and returns that as the command prefix.
See the Configuration page for details on how to have a config for the plugin.
from maubot import Plugin, MessageEvent
from maubot.handlers import command
class RenamingBot(Plugin):
def get_command_name(self) -> str:
return self.config["command_prefix"]
@command.new(name=get_command_name)
async def hmm(self, evt: MessageEvent) -> None: ...
help
A short help text for the command. This essentially just adds some metadata to the function that contains the help text. The metadata is currently only used for subcommands (for commands that require a subcommand, no arguments will produce a help message), but it's theoretically possible to use it for other purposes too.
aliases
This defines additional names for the command. Aliases can be used to trigger
the command, but they won't show up in help texts or other such things. The
parameter is similar to name
: it can either be a tuple/list/set, or a
function.
The function takes one or two parameters (either just the command, or the plugin instance and the command) and returns a boolean to indicate whether the given parameter is a valid alias for the command.
If the parameter is a function, it must return True
for the primary command
name. If it's a list, it doesn't have to include the primary name.
from maubot import Plugin, MessageEvent
from maubot.handlers import command
class RenamingAliasBot(Plugin):
def is_match(self, command: str) -> bool:
return command == "hmm" or command in self.config["command_aliases"]
@command.new(aliases=is_match)
async def hmm(self, evt: MessageEvent) -> None: ...
event_type
and msgtypes
Command handlers are fundamentally just wrappers for raw event handlers. The
event_type
and msgtype
parameters can be set to change what event and
message types the command handler reacts to.
event_type
is a single EventType
, defaulting to EventType.ROOM_MESSAGE
.
Multiple event types for a command handler are currently not supported.
msgtypes
is any iterable (list, tuple, etc) of allowed MessageType
s, it
defaults to only allowing MessageType.TEXT
.
require_subcommand
This parameter makes the command always output a help message if a subcommand
wasn't used. This defaults to true, but it only affects commands that have at
least one subcommand defined. You should change this to False
if your
top-level command has its own functionality. For example, the XKCD bot
has !xkcd <number>
as a top-level command with one argument, and some
subcommands like !xkcd search <query>
.
arg_fallthrough
For commands with both subcommands and top-level arguments, this parameter can
be used to make the top-level arguments fall through to the subcommand handlers.
The command will be parsed
as !<command> <top-level arguments> <subcommand> <subcommand arguments>
must_consume_args
Whether the command definition has to consume all arguments that the user provides. If this is true (the default), then any arguments that can't be parsed will cause the bot to send a help message instead of executing the command handler.
Subcommands
For more complicated bots, it's often useful to have multiple distinct command handlers for different tasks. While it's technically possible to use separate commands, it might be nicer to have everything under a single command. To do this, maubot allows you to define subcommands.
Subcommands are defined by using the .subcommand()
decorator of a top-level
command that was defined earlier:
from maubot import Plugin, MessageEvent
from maubot.handlers import command
class SimpleBot(Plugin):
@command.new(name="hello", require_subcommand=True)
async def base_command(self, evt: MessageEvent) -> None:
# When you require a subcommand, the base command handler
# doesn't have to do anything.
pass
@base_command.subcommand(help="Do subcommand things")
async def subcommand(self, evt: MessageEvent) -> None:
await evt.react("subcommand!")
In this case, the subcommand handler would be triggered by !hello subcommand
.
If you send just !hello
, it will respond with a help page. The help
argument
from the subcommand decorator is how the help page is populated.
The subcommand
decorator has the same parameters as top-level commands, except
it can't define the event_type
and msgtypes
that the handler catches. You
can also nest subcommands as deep as you like (i.e. in the above example,
@subcommand.subcommand()
is a valid decorator for a third method).
Passive command handlers
Passive commands are command handlers that don't follow a strict syntax
like !command <arguments>
, but instead just match some regular expressions
anywhere in a message.
Passive commands are created using the @command.passive(regex)
decorator.
The regex
is a required argument. It can either be a string (in which case
maubot will compile it) or a pre-re.compile
'd Pattern object.
from typing import Tuple
from maubot import Plugin, MessageEvent
from maubot.handlers import command
class PassiveBot(Plugin):
@command.passive("cat")
async def command(self, evt: MessageEvent, match: Tuple[str]) -> None:
await evt.react("ðïļ")
Handler parameters
Like command handlers, the first parameter is the MessageEvent
object. In
addition to that, passive commands always have a second parameter. By default,
the parameter is a tuple where the first element is the full match, and the
following elements are individual capture groups. If the multiple
flag is set,
then the parameter is an array containing one or more such tuples.
@command.passive()
parameters
field
This defines which field to run the regex against in the event content. The
parameter is a function that takes a single argument, the event object. The
default value will return evt.content.body
(i.e. the plaintext body of the
message).
multiple
...
event_type
and msgtypes
These work the same way as in @command.new()
case_insensitive
, multiline
and dot_all
These parameters enable regex flags. They only apply if you pass a string
instead of a compiled pattern as the regex. They set the re.IGNORECASE
, re.MULTILINE
and re.DOTALL
flags respectively.
Raw event handlers
Raw event handlers can be used to handle any incoming Matrix events. They don't have any kind of filters, so make sure to ignore echo events from the bot itself if necessary.
from maubot import Plugin
from maubot.handlers import event
from mautrix.types import EventType, StateEvent
class RawEventHandlingBot(Plugin):
@event.on(EventType.ROOM_TOMBSTONE)
async def handle_tombstone(self, evt: StateEvent) -> None:
self.log.info(f"{evt.room_id} was upgraded into "
f"{evt.content.replacement_room}")
await self.client.send_text(evt.room_id, "ðŠĶ")
If you want to handle custom events, you'll have to construct an EventType
instance first:
from maubot import Plugin
from maubot.handlers import event
from mautrix.types import EventType, GenericEvent
custom_event = EventType.find("com.example.custom",
t_class=EventType.Class.MESSAGE)
class CustomEventHandlingBot(Plugin):
@event.on(custom_event)
async def handle_custom_event(self, evt: GenericEvent) -> None:
self.log.info("Custom event data: %s", evt.content)
The t_class
field defines what type of event you want. For normal room events,
it's either MESSAGE
or STATE
. If you set it to STATE
, your handler won't
receive any non-state events, which is meant to protect from a fairly common
class of bugs.
HTTP handlers
Plugins can define custom HTTP endpoints that are published under the main maubot web server. The endpoints can do anything from JSON APIs to fully fledged websites.
Web handlers are disabled by default. You must set webapp: true
in your
plugin's maubot.yaml
to enable them. After that, simply decorate an aiohttp
handler method with @web.method(path)
where method
is the HTTP method,
e.g. get
or post
:
from maubot import Plugin
from maubot.handlers import web
from aiohttp.web import Request, Response, json_response
class WebsiteBot(Plugin):
@web.get("/id")
async def get_id(self, req: Request) -> Response:
return json_response({"id": self.id})
@web.post("/data/{id}")
async def post_data(self, req: Request) -> Response:
data_id = req.match_info["id"]
data = await req.text()
self.log.debug(f"Received data with ID {data_id}: {data}")
return Response(status=200)
The defined endpoints are reachable at
http://your.maubot.instance/_matrix/maubot/plugin/<instance ID>/<path>
.
Plugins can find their own public web base URL in self.webapp_url
.
The web server itself is aiohttp. Refer to the aiohttp server docs for more
info on how to make request handler methods, responses and other such things.
The maubot.handlers.web
module is designed to work like
aiohttp's RouteTableDef
, but there may be minor differences.
Configuration
Plugins can have instance-specific YAML config files. When a plugin has a config, the maubot admin panel will have a config editor for each instance of the plugin, which can be used to conveniently modify the config at runtime.
Quick start
To add configuration to your plugin, first create a file called
base-config.yaml
with the default config. Let's use a simple example with a
user ID whitelist and a customizable command prefix:
# Who is allowed to use the bot?
whitelist:
- "@user:example.com"
# The prefix for the main command without the !
command_prefix: hello-world
After that, add config
and extra_files
to your maubot.yaml
:
config: true
extra_files:
- base-config.yaml
Finally, define a Config
class and implement the get_config_class
method:
from typing import Type
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from maubot import Plugin, MessageEvent
from maubot.handlers import command
class Config(BaseProxyConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None:
helper.copy("whitelist")
helper.copy("command_prefix")
class ConfigurableBot(Plugin):
async def start(self) -> None:
self.config.load_and_update()
def get_command_name(self) -> str:
return self.config["command_prefix"]
@command.new(name=get_command_name)
async def hmm(self, evt: MessageEvent) -> None:
if evt.sender in self.config["whitelist"]:
await evt.reply("You're whitelisted ð")
@classmethod
def get_config_class(cls) -> Type[BaseProxyConfig]:
return Config
Config updates
The do_update
method is called whenever the user modifies the config. The
purpose of the method is to copy values from the user-provided config into the
base config bundled with the plugin. The result of that update is then stored as
the actual plugin config.
The purpose of this is to ensure that the config data is always what the plugin
expects. If you make a config schema change, simply implement a migration in
do_update
, and old configs will be automatically updated.
For example, say you wanted a more granular permission system than a whitelist. First, you'd change the base config to only have your new schema:
# Who is allowed to use the bot?
permissions:
"@user:example.com": 100
# The prefix for the main command without the !
command_prefix: hello-world
Then, you'd implement a migration in the do_update
method by checking if the
old field is present. When updating the config, self
is the old or
user-specified config and helper.base
is the base config that the values
should be copied to.
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
class Config(BaseProxyConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None:
if "whitelist" in self:
# Give everyone in the old whitelist level 100
helper.base["permissions"] = {user_id: 100
for user_id in self["whitelist"]}
else:
# Config is already using new system, just copy permissions like usual
helper.copy("permissions")
helper.copy("command_prefix")
Helper methods
The ConfigUpdateHelper
contains two methods and properties:
base
- The contents ofbase-config.yaml
.source
- The old or user-specified config. This is the same asself
in thedo_update
method.copy(from_path, to_path)
- Copy a value from the source to the base.from_path
is the path in the source (old/user-specified) config, whileto_path
is the path in the base config. If not specified,to_path
will be the same asfrom_path
. This will check if the value is present in the source config before copying it.copy_dict(from_path, to_path, override)
- Copy entries in a dict from the source to the base. Ifoverride
is set to false, keys that are in the base config but not in the source config will be kept in the end result, which is useful if you have a set of keys that must be defined.
Modifying the config from the plugin
The config can also be modified by the plugin itself. Simply store whatever
values you want in the self.config
dict, then call self.config.save()
. The
config updater will not be ran when you save the config, but it will be ran when
the plugin is next reloaded, so make sure the updater won't remove your changes.
Database
Plugins can request an instance-specific SQL database.
Maubot supports two types of databases: SQLAlchemy (legacy) and asyncpg/aiosqlite (new). The SQLAlchemy mode is deprecated and not documented.
Note that the new asyncpg system requires maubot 0.3.0 or higher.
Quick start
Update maubot.yaml
to tell the server that your plugin wants a database:
database: true
database_type: asyncpg
The database connector will be provided in the database
property. The type is
mautrix.util.async_db.Database
, which emulates asyncpg's pool interface.
For legacy plugins (with database_type
unset or set to sqlalchemy
), the
database
property contains an SQLAlchemy engine.
For details on the methods available in the Database
class,
see Database API reference.
You can also find an example plugin in the maubot repo: examples/database/
Schema upgrades
mautrix-python includes a simple framework for versioning the schema.
The migrations are simply Python functions which are registered in an
UpgradeTable
. By default, the migrations are registered in order and each one
bumps the version by 1. The example below would result in the version
table
containing 2
.
from mautrix.util.async_db import UpgradeTable, Scheme
upgrade_table = UpgradeTable()
@upgrade_table.register(description="Initial revision")
async def upgrade_v1(conn: Connection, scheme: Scheme) -> None:
if scheme == Scheme.SQLITE:
await conn.execute(
"""CREATE TABLE foo (
test INTEGER PRIMARY KEY
)"""
)
else:
await conn.execute(
"""CREATE TABLE foo (
test INTEGER GENERATED ALWAYS AS IDENTITY
)"""
)
@upgrade_table.register(description="Add text column")
async def upgrade_v2(conn: Connection) -> None:
await conn.execute("ALTER TABLE foo ADD COLUMN text TEXT DEFAULT 'hi'")
After you have an UpgradeTable
, you need to expose it from your plugin class
so maubot knows to use it:
from mautrix.util.async_db import UpgradeTable
from maubot import Plugin
from .migrations import upgrade_table
class DatabasefulBot(Plugin):
@classmethod
def get_db_upgrade_table(cls) -> UpgradeTable:
return upgrade_table
Maubot will then run the migrations always before starting your bot.
Database API reference
You can also view the Sphinx docs for the database at https://docs.mau.fi/python/latest/api/mautrix.util/async_db.html, but as of writing, they aren't ready yet.
Database
acquire
is an async context manager that returns a Connection
:
async with self.database.acquire() as conn:
conn.execute(...)
The class also contains execute
, executemany
, fetch
, fetchrow
,
fetchval
and table_exists
as convenience methods, which simply acquire a
connection and run the single method using it (see reference below).
Connection
Records returned by the fetch methods are either asyncpg.Record
or the stdlib's
sqlite3.Row
. Both work mostly the same way (can access fields by both
column index and name).
execute(query: str, *args: Any) -> str
- Execute a query without reading the response:await conn.execute("INSERT INTO foo (text) VALUES ($1)", "hello world")
executemany(query: str, *args: Any) -> str
- Execute a query multiple times:await conn.execute( "INSERT INTO foo (text) VALUES ($1)", [("hello world 1",), ("hello world 2",), ("hello world 3",)], )
fetch(query: str, *args: Any) -> list[Row | Record]
- Execute a query and get a list of rows in response:rows = await conn.fetch("SELECT text FROM foo") for row in rows: print(row["text"])
fetchrow(query: str, *args: Any) -> Row | Record | None
- Execute a query and get the first rows (orNone
, if there are no rows):row = await conn.fetchrow("SELECT text FROM foo WHERE test=1") if row: print("Found", row["text"]) else: print("Row not found :(")
fetchval(query: str, *args: Any, column: int = 0) -> Any
- Execute a query and get a single column from the first row (orNone
if there are no rows):text = await conn.fetchval("SELECT text FROM foo WHERE test=1") print(text)
table_exists(name: str) -> bool
- Check if a table exists in the database.- Postgres only:
copy_records_to_table(table_name: str, *, records: list[tuple[Any, ...]], columns: tuple[str, ...]) -> None
- Efficiently insert multiple rows into the database:await conn.copy_records_to_table( table_name="foo", records=[("hello world 1",), ("hello world 2",), ("hello world 3",)], columns=("text",), )