summaryrefslogtreecommitdiffstats
path: root/nixos/modules/services/matrix
diff options
context:
space:
mode:
authorchayleaf <chayleaf-git@pavluk.org>2023-11-04 08:53:27 +0700
committerchayleaf <chayleaf-git@pavluk.org>2023-11-28 20:35:55 +0700
commit00070cf866af5945cefdb59803005de8a47abaf2 (patch)
tree75633fab0dd52adb9ced252a786ee9b5db6357f5 /nixos/modules/services/matrix
parente96b8fd970b947c71e559a5ddc89d64a829ab615 (diff)
nixos/maubot: init
Diffstat (limited to 'nixos/modules/services/matrix')
-rw-r--r--nixos/modules/services/matrix/maubot.md103
-rw-r--r--nixos/modules/services/matrix/maubot.nix459
2 files changed, 562 insertions, 0 deletions
diff --git a/nixos/modules/services/matrix/maubot.md b/nixos/modules/services/matrix/maubot.md
new file mode 100644
index 000000000000..f6a05db56caf
--- /dev/null
+++ b/nixos/modules/services/matrix/maubot.md
@@ -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`).
diff --git a/nixos/modules/services/matrix/maubot.nix b/nixos/modules/services/matrix/maubot.nix
new file mode 100644
index 000000000000..6cdb57fa72ef
--- /dev/null
+++ b/nixos/modules/services/matrix/maubot.nix
@@ -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;
+}