8 cfg = config.services.maubot;
13 else cfg.package.withPlugins (_: cfg.plugins);
16 if cfg.pythonPackages == [ ]
18 else wrapper1.withPythonPackages (_: cfg.pythonPackages);
20 settings = lib.recursiveUpdate cfg.settings {
21 plugin_directories.trash =
22 if cfg.settings.plugin_directories.trash == null
24 else cfg.settings.plugin_directories.trash;
25 server.unshared_secret = "generate";
28 finalPackage = wrapper2.withBaseConfig settings;
30 isPostgresql = db: builtins.isString db && lib.hasPrefix "postgresql://" db;
31 isLocalPostgresDB = db: isPostgresql db && builtins.any (x: lib.hasInfix x db) [
39 noSchema = lib.removePrefix "postgresql://" db;
41 username = builtins.head (lib.splitString "@" noSchema);
42 database = lib.last (lib.splitString "/" noSchema);
45 postgresDBs = builtins.filter isPostgresql [
47 cfg.settings.crypto_database
48 cfg.settings.plugin_databases.postgres
51 localPostgresDBs = builtins.filter isLocalPostgresDB postgresDBs;
53 parsedLocalPostgresDBs = map parsePostgresDB localPostgresDBs;
54 parsedPostgresDBs = map parsePostgresDB postgresDBs;
56 hasLocalPostgresDB = localPostgresDBs != [ ];
59 options.services.maubot = with lib; {
60 enable = mkEnableOption "maubot";
62 package = lib.mkPackageOption pkgs "maubot" { };
65 type = types.listOf types.package;
67 example = literalExpression ''
68 with config.services.maubot.package.plugins; [
74 List of additional maubot plugins to make available.
78 pythonPackages = mkOption {
79 type = types.listOf types.package;
81 example = literalExpression ''
82 with pkgs.python3Packages; [
87 List of additional Python packages to make available for maubot.
93 default = "/var/lib/maubot";
95 The directory where maubot stores its stateful data.
99 extraConfigFile = mkOption {
101 default = "./config.yaml";
102 defaultText = literalExpression ''"''${config.services.maubot.dataDir}/config.yaml"'';
104 A file for storing secrets. You can pass homeserver registration keys here.
105 If it already exists, **it must contain `server.unshared_secret`** which is used for signing API keys.
106 If `configMutable` is not set to true, **maubot user must have write access to this file**.
110 configMutable = mkOption {
114 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!**
118 settings = mkOption {
121 YAML settings for maubot. See the
122 [example configuration](https://github.com/maubot/maubot/blob/master/maubot/example-config.yaml)
125 Secrets should be passed in by using `extraConfigFile`.
127 type = with types; submodule {
129 database = mkOption {
131 default = "sqlite:maubot.db";
132 example = "postgresql://username:password@hostname/dbname";
134 The full URI to the database. SQLite and Postgres are fully supported.
135 Other DBMSes supported by SQLAlchemy may or may not work.
139 crypto_database = mkOption {
142 example = "postgresql://username:password@hostname/dbname";
144 Separate database URL for the crypto database. By default, the regular database is also used for crypto.
148 database_opts = mkOption {
152 Additional arguments for asyncpg.create_pool() or sqlite3.connect()
156 plugin_directories = mkOption {
158 description = "Plugin directory paths";
163 default = "./plugins";
164 defaultText = literalExpression ''"''${config.services.maubot.dataDir}/plugins"'';
166 The directory where uploaded new plugins should be stored.
170 type = types.listOf types.str;
171 default = [ "./plugins" ];
172 defaultText = literalExpression ''[ "''${config.services.maubot.dataDir}/plugins" ]'';
174 The directories from which plugins should be loaded. Duplicate plugin IDs will be moved to the trash.
178 type = with types; nullOr str;
180 defaultText = literalExpression ''"''${config.services.maubot.dataDir}/trash"'';
182 The directory where old plugin versions and conflicting plugins should be moved. Set to null to delete files immediately.
189 plugin_databases = mkOption {
190 description = "Plugin database settings";
196 default = "./plugins";
197 defaultText = literalExpression ''"''${config.services.maubot.dataDir}/plugins"'';
199 The directory where SQLite plugin databases should be stored.
203 postgres = mkOption {
204 type = types.nullOr types.str;
205 default = if isPostgresql cfg.settings.database then "default" else null;
206 defaultText = literalExpression ''if isPostgresql config.services.maubot.settings.database then "default" else null'';
208 The connection URL for plugin database. See [example config](https://github.com/maubot/maubot/blob/master/maubot/example-config.yaml) for exact format.
212 postgres_max_conns_per_plugin = mkOption {
213 type = types.nullOr types.int;
216 Maximum number of connections per plugin instance.
220 postgres_opts = mkOption {
224 Overrides for the default database_opts when using a non-default postgres connection URL.
233 description = "Listener config";
236 hostname = mkOption {
238 default = "127.0.0.1";
247 The port to listen on
250 public_url = mkOption {
252 default = "http://${cfg.settings.server.hostname}:${toString cfg.settings.server.port}";
253 defaultText = literalExpression ''"http://''${config.services.maubot.settings.server.hostname}:''${toString config.services.maubot.settings.server.port}"'';
255 Public base URL where the server is visible.
258 ui_base_path = mkOption {
260 default = "/_matrix/maubot";
262 The base path for the UI.
265 plugin_base_path = mkOption {
267 default = "${config.services.maubot.settings.server.ui_base_path}/plugin/";
268 defaultText = literalExpression ''
269 "''${config.services.maubot.settings.server.ui_base_path}/plugin/"
272 The base path for plugin endpoints. The instance ID will be appended directly.
275 override_resource_path = mkOption {
276 type = types.nullOr types.str;
279 Override path from where to load UI resources.
286 homeservers = mkOption {
287 type = types.attrsOf (types.submodule {
292 Client-server API URL
299 url = "https://matrix-client.matrix.org";
303 Known homeservers. This is required for the `mbc auth` command and also allows more convenient access from the management UI.
304 If you want to specify registration secrets, pass this via extraConfigFile instead.
309 type = types.attrsOf types.str;
310 default = { root = ""; };
312 List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password
313 to prevent normal login. Root is a special user that can't have a password and will always exist.
317 api_features = mkOption {
318 type = types.attrsOf bool;
322 plugin_upload = true;
324 instance_database = true;
332 API feature switches.
339 Python logging configuration. See [section 16.7.2 of the Python
340 documentation](https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema)
347 "()" = "maubot.lib.color_log.ColorFormatter";
348 format = "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s";
351 format = "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s";
356 class = "logging.handlers.RotatingFileHandler";
357 formatter = "normal";
358 filename = "./maubot.log";
363 class = "logging.StreamHandler";
364 formatter = "colored";
380 handlers = [ "file" "console" ];
389 config = lib.mkIf cfg.enable {
390 warnings = lib.optional (builtins.any (x: x.username != x.database) parsedLocalPostgresDBs) ''
391 The Maubot database username doesn't match the database name! This means the user won't be automatically
392 granted ownership of the database. Consider changing either the username or the database name.
396 assertion = builtins.all (x: !lib.hasInfix ":" x.username) parsedPostgresDBs;
398 Putting database passwords in your Nix config makes them world-readable. To securely put passwords
399 in your Maubot config, change /var/lib/maubot/config.yaml after running Maubot at least once as
400 described in the NixOS manual.
404 assertion = hasLocalPostgresDB -> config.services.postgresql.enable;
406 Cannot deploy maubot with a configuration for a local postgresql database and a missing postgresql service.
411 services.postgresql = lib.mkIf hasLocalPostgresDB {
413 ensureDatabases = map (x: x.database) parsedLocalPostgresDBs;
414 ensureUsers = lib.flip map parsedLocalPostgresDBs (x: {
416 ensureDBOwnership = lib.mkIf (x.username == x.database) true;
420 users.users.maubot = {
423 # otherwise StateDirectory is enough
424 createHome = lib.mkIf (cfg.dataDir != "/var/lib/maubot") true;
428 users.groups.maubot = { };
430 systemd.services.maubot = rec {
431 description = "maubot - a plugin-based Matrix bot system written in Python";
432 after = [ "network.target" ] ++ wants ++ lib.optional hasLocalPostgresDB "postgresql.service";
433 # all plugins get automatically disabled if maubot starts before synapse
434 wants = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
435 wantedBy = [ "multi-user.target" ];
438 if [ ! -f "${cfg.extraConfigFile}" ]; then
439 echo "server:" > "${cfg.extraConfigFile}"
440 echo " unshared_secret: $(head -c40 /dev/random | base32 | ${pkgs.gawk}/bin/awk '{print tolower($0)}')" > "${cfg.extraConfigFile}"
441 chmod 640 "${cfg.extraConfigFile}"
446 ExecStart = "${finalPackage}/bin/maubot --config ${cfg.extraConfigFile}" + lib.optionalString (!cfg.configMutable) " --no-update";
449 Restart = "on-failure";
451 StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/maubot") "maubot";
452 WorkingDirectory = cfg.dataDir;
457 meta.maintainers = with lib.maintainers; [ chayleaf ];
458 meta.doc = ./maubot.md;