nixos/maubot: init

This commit is contained in:
chayleaf 2023-11-04 08:53:27 +07:00
parent e96b8fd970
commit 00070cf866
No known key found for this signature in database
GPG Key ID: 78171AD46227E68E
4 changed files with 564 additions and 1 deletions

View File

@ -14,7 +14,7 @@ In addition to numerous new and upgraded packages, this release has the followin
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
- Create the first release note entry in this section!
- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).
## Backward Incompatibilities {#sec-release-24.05-incompatibilities}

View File

@ -621,6 +621,7 @@
./services/matrix/appservice-irc.nix
./services/matrix/conduit.nix
./services/matrix/dendrite.nix
./services/matrix/maubot.nix
./services/matrix/mautrix-facebook.nix
./services/matrix/mautrix-telegram.nix
./services/matrix/mautrix-whatsapp.nix

View File

@ -0,0 +1,103 @@
# Maubot {#module-services-maubot}
[Maubot](https://github.com/maubot/maubot) is a plugin-based bot
framework for Matrix.
## Configuration {#module-services-maubot-configuration}
1. Set [](#opt-services.maubot.enable) to `true`. The service will use
SQLite by default.
2. If you want to use PostgreSQL instead of SQLite, do this:
```nix
services.maubot.settings.database = "postgresql://maubot@localhost/maubot";
```
If the PostgreSQL connection requires a password, you will have to
add it later on step 8.
3. If you plan to expose your Maubot interface to the web, do something
like this:
```nix
services.nginx.virtualHosts."matrix.example.org".locations = {
"/_matrix/maubot/" = {
proxyPass = "http://127.0.0.1:${toString config.services.maubot.settings.server.port}";
proxyWebsockets = true;
};
};
services.maubot.settings.server.public_url = "matrix.example.org";
# do the following only if you want to use something other than /_matrix/maubot...
services.maubot.settings.server.ui_base_path = "/another/base/path";
```
4. Optionally, set `services.maubot.pythonPackages` to a list of python3
packages to make available for Maubot plugins.
5. Optionally, set `services.maubot.plugins` to a list of Maubot
plugins (full list available at https://plugins.maubot.xyz/):
```nix
services.maubot.plugins = with config.services.maubot.package.plugins; [
reactbot
# This will only change the default config! After you create a
# plugin instance, the default config will be copied into that
# instance's config in Maubot's database, and further base config
# changes won't affect the running plugin.
(rss.override {
base_config = {
update_interval = 60;
max_backoff = 7200;
spam_sleep = 2;
command_prefix = "rss";
admins = [ "@chayleaf:pavluk.org" ];
};
})
];
# ...or...
services.maubot.plugins = config.services.maubot.package.plugins.allOfficialPlugins;
# ...or...
services.maubot.plugins = config.services.maubot.package.plugins.allPlugins;
# ...or...
services.maubot.plugins = with config.services.maubot.package.plugins; [
(weather.override {
# you can pass base_config as a string
base_config = ''
default_location: New York
default_units: M
default_language:
show_link: true
show_image: false
'';
})
];
```
6. Start Maubot at least once before doing the following steps (it's
necessary to generate the initial config).
7. If your PostgreSQL connection requires a password, add
`database: postgresql://user:password@localhost/maubot`
to `/var/lib/maubot/config.yaml`. This overrides the Nix-provided
config. Even then, don't remove the `database` line from Nix config
so the module knows you use PostgreSQL!
8. To create a user account for logging into Maubot web UI and
configuring it, generate a password using the shell command
`mkpasswd -R 12 -m bcrypt`, and edit `/var/lib/maubot/config.yaml`
with the following:
```yaml
admins:
admin_username: $2b$12$g.oIStUeUCvI58ebYoVMtO/vb9QZJo81PsmVOomHiNCFbh0dJpZVa
```
Where `admin_username` is your username, and `$2b...` is the bcrypted
password.
9. Optional: if you want to be able to register new users with the
Maubot CLI (`mbc`), and your homeserver is private, add your
homeserver's registration key to `/var/lib/maubot/config.yaml`:
```yaml
homeservers:
matrix.example.org:
url: https://matrix.example.org
secret: your-very-secret-key
```
10. Restart Maubot after editing `/var/lib/maubot/config.yaml`,and
Maubot will be available at
`https://matrix.example.org/_matrix/maubot`. If you want to use the
`mbc` CLI, it's available using the `maubot` package (`nix-shell -p
maubot`).

View File

@ -0,0 +1,459 @@
{ lib
, config
, pkgs
, ...
}:
let
cfg = config.services.maubot;
wrapper1 =
if cfg.plugins == [ ]
then cfg.package
else cfg.package.withPlugins (_: cfg.plugins);
wrapper2 =
if cfg.pythonPackages == [ ]
then wrapper1
else wrapper1.withPythonPackages (_: cfg.pythonPackages);
settings = lib.recursiveUpdate cfg.settings {
plugin_directories.trash =
if cfg.settings.plugin_directories.trash == null
then "delete"
else cfg.settings.plugin_directories.trash;
server.unshared_secret = "generate";
};
finalPackage = wrapper2.withBaseConfig settings;
isPostgresql = db: builtins.isString db && lib.hasPrefix "postgresql://" db;
isLocalPostgresDB = db: isPostgresql db && builtins.any (x: lib.hasInfix x db) [
"@127.0.0.1/"
"@::1/"
"@[::1]/"
"@localhost/"
];
parsePostgresDB = db:
let
noSchema = lib.removePrefix "postgresql://" db;
in {
username = builtins.head (lib.splitString "@" noSchema);
database = lib.last (lib.splitString "/" noSchema);
};
postgresDBs = [
cfg.settings.database
cfg.settings.crypto_database
cfg.settings.plugin_databases.postgres
];
localPostgresDBs = builtins.filter isLocalPostgresDB postgresDBs;
parsedLocalPostgresDBs = map parsePostgresDB localPostgresDBs;
parsedPostgresDBs = map parsePostgresDB postgresDBs;
hasLocalPostgresDB = localPostgresDBs != [ ];
in
{
options.services.maubot = with lib; {
enable = mkEnableOption (mdDoc "maubot");
package = lib.mkPackageOptionMD pkgs "maubot" { };
plugins = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression ''
with config.services.maubot.package.plugins; [
xyz.maubot.reactbot
xyz.maubot.rss
];
'';
description = mdDoc ''
List of additional maubot plugins to make available.
'';
};
pythonPackages = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression ''
with pkgs.python3Packages; [
aiohttp
];
'';
description = mdDoc ''
List of additional Python packages to make available for maubot.
'';
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/maubot";
description = mdDoc ''
The directory where maubot stores its stateful data.
'';
};
extraConfigFile = mkOption {
type = types.str;
default = "./config.yaml";
defaultText = literalExpression ''"''${config.services.maubot.dataDir}/config.yaml"'';
description = mdDoc ''
A file for storing secrets. You can pass homeserver registration keys here.
If it already exists, **it must contain `server.unshared_secret`** which is used for signing API keys.
If `configMutable` is not set to true, **maubot user must have write access to this file**.
'';
};
configMutable = mkOption {
type = types.bool;
default = false;
description = mdDoc ''
Whether maubot should write updated config into `extraConfigFile`. **This will make your Nix module settings have no effect besides the initial config, as extraConfigFile takes precedence over NixOS settings!**
'';
};
settings = mkOption {
default = { };
description = mdDoc ''
YAML settings for maubot. See the
[example configuration](https://github.com/maubot/maubot/blob/master/maubot/example-config.yaml)
for more info.
Secrets should be passed in by using `extraConfigFile`.
'';
type = with types; submodule {
options = {
database = mkOption {
type = str;
default = "sqlite:maubot.db";
example = "postgresql://username:password@hostname/dbname";
description = mdDoc ''
The full URI to the database. SQLite and Postgres are fully supported.
Other DBMSes supported by SQLAlchemy may or may not work.
'';
};
crypto_database = mkOption {
type = str;
default = "default";
example = "postgresql://username:password@hostname/dbname";
description = mdDoc ''
Separate database URL for the crypto database. By default, the regular database is also used for crypto.
'';
};
database_opts = mkOption {
type = types.attrs;
default = { };
description = mdDoc ''
Additional arguments for asyncpg.create_pool() or sqlite3.connect()
'';
};
plugin_directories = mkOption {
default = { };
description = mdDoc "Plugin directory paths";
type = submodule {
options = {
upload = mkOption {
type = types.str;
default = "./plugins";
defaultText = literalExpression ''"''${config.services.maubot.dataDir}/plugins"'';
description = mdDoc ''
The directory where uploaded new plugins should be stored.
'';
};
load = mkOption {
type = types.listOf types.str;
default = [ "./plugins" ];
defaultText = literalExpression ''[ "''${config.services.maubot.dataDir}/plugins" ]'';
description = mdDoc ''
The directories from which plugins should be loaded. Duplicate plugin IDs will be moved to the trash.
'';
};
trash = mkOption {
type = with types; nullOr str;
default = "./trash";
defaultText = literalExpression ''"''${config.services.maubot.dataDir}/trash"'';
description = mdDoc ''
The directory where old plugin versions and conflicting plugins should be moved. Set to null to delete files immediately.
'';
};
};
};
};
plugin_databases = mkOption {
description = mdDoc "Plugin database settings";
default = { };
type = submodule {
options = {
sqlite = mkOption {
type = types.str;
default = "./plugins";
defaultText = literalExpression ''"''${config.services.maubot.dataDir}/plugins"'';
description = mdDoc ''
The directory where SQLite plugin databases should be stored.
'';
};
postgres = mkOption {
type = types.nullOr types.str;
default = if isPostgresql cfg.settings.database then "default" else null;
defaultText = literalExpression ''if isPostgresql config.services.maubot.settings.database then "default" else null'';
description = mdDoc ''
The connection URL for plugin database. See [example config](https://github.com/maubot/maubot/blob/master/maubot/example-config.yaml) for exact format.
'';
};
postgres_max_conns_per_plugin = mkOption {
type = types.nullOr types.int;
default = 3;
description = mdDoc ''
Maximum number of connections per plugin instance.
'';
};
postgres_opts = mkOption {
type = types.attrs;
default = { };
description = mdDoc ''
Overrides for the default database_opts when using a non-default postgres connection URL.
'';
};
};
};
};
server = mkOption {
default = { };
description = mdDoc "Listener config";
type = submodule {
options = {
hostname = mkOption {
type = types.str;
default = "127.0.0.1";
description = mdDoc ''
The IP to listen on
'';
};
port = mkOption {
type = types.port;
default = 29316;
description = mdDoc ''
The port to listen on
'';
};
public_url = mkOption {
type = types.str;
default = "http://${cfg.settings.server.hostname}:${toString cfg.settings.server.port}";
defaultText = literalExpression ''"http://''${config.services.maubot.settings.server.hostname}:''${toString config.services.maubot.settings.server.port}"'';
description = mdDoc ''
Public base URL where the server is visible.
'';
};
ui_base_path = mkOption {
type = types.str;
default = "/_matrix/maubot";
description = mdDoc ''
The base path for the UI.
'';
};
plugin_base_path = mkOption {
type = types.str;
default = "${config.services.maubot.settings.server.ui_base_path}/plugin/";
defaultText = literalExpression ''
"''${config.services.maubot.settings.server.ui_base_path}/plugin/"
'';
description = mdDoc ''
The base path for plugin endpoints. The instance ID will be appended directly.
'';
};
override_resource_path = mkOption {
type = types.nullOr types.str;
default = null;
description = mdDoc ''
Override path from where to load UI resources.
'';
};
};
};
};
homeservers = mkOption {
type = types.attrsOf (types.submodule {
options = {
url = mkOption {
type = types.str;
description = mdDoc ''
Client-server API URL
'';
};
};
});
default = {
"matrix.org" = {
url = "https://matrix-client.matrix.org";
};
};
description = mdDoc ''
Known homeservers. This is required for the `mbc auth` command and also allows more convenient access from the management UI.
If you want to specify registration secrets, pass this via extraConfigFile instead.
'';
};
admins = mkOption {
type = types.attrsOf types.str;
default = { root = ""; };
description = mdDoc ''
List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password
to prevent normal login. Root is a special user that can't have a password and will always exist.
'';
};
api_features = mkOption {
type = types.attrsOf bool;
default = {
login = true;
plugin = true;
plugin_upload = true;
instance = true;
instance_database = true;
client = true;
client_proxy = true;
client_auth = true;
dev_open = true;
log = true;
};
description = mdDoc ''
API feature switches.
'';
};
logging = mkOption {
type = types.attrs;
description = mdDoc ''
Python logging configuration. See [section 16.7.2 of the Python
documentation](https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema)
for more info.
'';
default = {
version = 1;
formatters = {
colored = {
"()" = "maubot.lib.color_log.ColorFormatter";
format = "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s";
};
normal = {
format = "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s";
};
};
handlers = {
file = {
class = "logging.handlers.RotatingFileHandler";
formatter = "normal";
filename = "./maubot.log";
maxBytes = 10485760;
backupCount = 10;
};
console = {
class = "logging.StreamHandler";
formatter = "colored";
};
};
loggers = {
maubot = {
level = "DEBUG";
};
mau = {
level = "DEBUG";
};
aiohttp = {
level = "INFO";
};
};
root = {
level = "DEBUG";
handlers = [ "file" "console" ];
};
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
warnings = lib.optional (builtins.any (x: x.username != x.database) parsedLocalPostgresDBs) ''
The Maubot database username doesn't match the database name! This means the user won't be automatically
granted ownership of the database. Consider changing either the username or the database name.
'';
assertions = [
{
assertion = builtins.all (x: !lib.hasInfix ":" x.username) parsedPostgresDBs;
message = ''
Putting database passwords in your Nix config makes them world-readable. To securely put passwords
in your Maubot config, change /var/lib/maubot/config.yaml after running Maubot at least once as
described in the NixOS manual.
'';
}
{
assertion = hasLocalPostgresDB -> config.services.postgresql.enable;
message = ''
Cannot deploy maubot with a configuration for a local postgresql database and a missing postgresql service.
'';
}
];
services.postgresql = lib.mkIf hasLocalPostgresDB {
enable = true;
ensureDatabases = map (x: x.database) parsedLocalPostgresDBs;
ensureUsers = lib.flip map parsedLocalPostgresDBs (x: {
name = x.username;
ensureDBOwnership = lib.mkIf (x.username == x.database) true;
});
};
users.users.maubot = {
group = "maubot";
home = cfg.dataDir;
# otherwise StateDirectory is enough
createHome = lib.mkIf (cfg.dataDir != "/var/lib/maubot") true;
isSystemUser = true;
};
users.groups.maubot = { };
systemd.services.maubot = rec {
description = "maubot - a plugin-based Matrix bot system written in Python";
after = [ "network.target" ] ++ wants ++ lib.optional hasLocalPostgresDB "postgresql.service";
# all plugins get automatically disabled if maubot starts before synapse
wants = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
wantedBy = [ "multi-user.target" ];
preStart = ''
if [ ! -f "${cfg.extraConfigFile}" ]; then
echo "server:" > "${cfg.extraConfigFile}"
echo " unshared_secret: $(head -c40 /dev/random | base32 | ${pkgs.gawk}/bin/awk '{print tolower($0)}')" > "${cfg.extraConfigFile}"
chmod 640 "${cfg.extraConfigFile}"
fi
'';
serviceConfig = {
ExecStart = "${finalPackage}/bin/maubot --config ${cfg.extraConfigFile}" + lib.optionalString (!cfg.configMutable) " --no-update";
User = "maubot";
Group = "maubot";
Restart = "on-failure";
RestartSec = "10s";
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/maubot") "maubot";
WorkingDirectory = cfg.dataDir;
};
};
};
meta.maintainers = with lib.maintainers; [ chayleaf ];
meta.doc = ./maubot.md;
}