base16-schemes: unstable-2024-06-21 -> unstable-2024-11-12
[NixPkgs.git] / nixos / modules / services / matrix / mautrix-meta.nix
blob79f7239df9e5df8719fe0308da5fd9958b532434
1 { config, pkgs, lib, ... }:
3 let
4   settingsFormat = pkgs.formats.yaml {};
6   upperConfig = config;
7   cfg = config.services.mautrix-meta;
8   upperCfg = cfg;
10   fullDataDir = cfg: "/var/lib/${cfg.dataDir}";
12   settingsFile = cfg: "${fullDataDir cfg}/config.yaml";
13   settingsFileUnsubstituted = cfg: settingsFormat.generate "mautrix-meta-config.yaml" cfg.settings;
15   metaName = name: "mautrix-meta-${name}";
17   enabledInstances = lib.filterAttrs (name: config: config.enable) config.services.mautrix-meta.instances;
18   registerToSynapseInstances = lib.filterAttrs (name: config: config.enable && config.registerToSynapse) config.services.mautrix-meta.instances;
19 in {
20   options = {
21     services.mautrix-meta = {
23       package = lib.mkPackageOption pkgs "mautrix-meta" { };
25       instances = lib.mkOption {
26         type = lib.types.attrsOf (lib.types.submodule ({ config, name, ... }: {
28           options = {
30             enable = lib.mkEnableOption "Mautrix-Meta, a Matrix <-> Facebook and Matrix <-> Instagram hybrid puppeting/relaybot bridge";
32             dataDir = lib.mkOption {
33               type = lib.types.str;
34               default = metaName name;
35               description = ''
36                 Path to the directory with database, registration, and other data for the bridge service.
37                 This path is relative to `/var/lib`, it cannot start with `../` (it cannot be outside of `/var/lib`).
38               '';
39             };
41             registrationFile = lib.mkOption {
42               type = lib.types.path;
43               readOnly = true;
44               description = ''
45                 Path to the yaml registration file of the appservice.
46               '';
47             };
49             registerToSynapse = lib.mkOption {
50               type = lib.types.bool;
51               default = true;
52               description = ''
53                 Whether to add registration file to `services.matrix-synapse.settings.app_service_config_files` and
54                 make Synapse wait for registration service.
55               '';
56             };
58             settings = lib.mkOption rec {
59               apply = lib.recursiveUpdate default;
60               inherit (settingsFormat) type;
61               default = {
62                 homeserver = {
63                   software = "standard";
65                   domain = "";
66                   address = "";
67                 };
69                 appservice = {
70                   id = "";
72                   bot = {
73                     username = "";
74                   };
76                   hostname = "localhost";
77                   port = 29319;
78                   address = "http://${config.settings.appservice.hostname}:${toString config.settings.appservice.port}";
79                 };
81                 bridge = {
82                   permissions = {};
83                 };
85                 database = {
86                   type = "sqlite3-fk-wal";
87                   uri = "file:${fullDataDir config}/mautrix-meta.db?_txlock=immediate";
88                 };
90                 # Enable encryption by default to make the bridge more secure
91                 encryption = {
92                   allow = true;
93                   default = true;
94                   require = true;
96                   # Recommended options from mautrix documentation
97                   # for additional security.
98                   delete_keys = {
99                     dont_store_outbound = true;
100                     ratchet_on_decrypt = true;
101                     delete_fully_used_on_decrypt = true;
102                     delete_prev_on_new_session = true;
103                     delete_on_device_delete = true;
104                     periodically_delete_expired = true;
105                     delete_outdated_inbound = true;
106                   };
108                   # TODO: This effectively disables encryption. But this is the value provided when a <0.4 config is migrated. Changing it will corrupt the database.
109                   # https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L24
110                   # If you wish to encrypt the local database you should set this to an environment variable substitution and reset the bridge or somehow migrate the DB.
111                   pickle_key = "mautrix.bridge.e2ee";
113                   verification_levels = {
114                     receive = "cross-signed-tofu";
115                     send = "cross-signed-tofu";
116                     share = "cross-signed-tofu";
117                   };
118                 };
120                 logging = {
121                   min_level = "info";
122                   writers = lib.singleton {
123                     type = "stdout";
124                     format = "pretty-colored";
125                     time_format = " ";
126                   };
127                 };
129                 network = {
130                   mode = "";
131                 };
132               };
133               defaultText = ''
134               {
135                 homeserver = {
136                   software = "standard";
137                   address = "https://''${config.settings.homeserver.domain}";
138                 };
140                 appservice = {
141                   database = {
142                     type = "sqlite3-fk-wal";
143                     uri = "file:''${fullDataDir config}/mautrix-meta.db?_txlock=immediate";
144                   };
146                   hostname = "localhost";
147                   port = 29319;
148                   address = "http://''${config.settings.appservice.hostname}:''${toString config.settings.appservice.port}";
149                 };
151                 bridge = {
152                   # Require encryption by default to make the bridge more secure
153                   encryption = {
154                     allow = true;
155                     default = true;
156                     require = true;
158                     # Recommended options from mautrix documentation
159                     # for optimal security.
160                     delete_keys = {
161                       dont_store_outbound = true;
162                       ratchet_on_decrypt = true;
163                       delete_fully_used_on_decrypt = true;
164                       delete_prev_on_new_session = true;
165                       delete_on_device_delete = true;
166                       periodically_delete_expired = true;
167                       delete_outdated_inbound = true;
168                     };
170                     verification_levels = {
171                       receive = "cross-signed-tofu";
172                       send = "cross-signed-tofu";
173                       share = "cross-signed-tofu";
174                     };
175                   };
176                 };
178                 logging = {
179                   min_level = "info";
180                   writers = lib.singleton {
181                     type = "stdout";
182                     format = "pretty-colored";
183                     time_format = " ";
184                   };
185                 };
186               };
187               '';
188               description = ''
189                 {file}`config.yaml` configuration as a Nix attribute set.
190                 Configuration options should match those described in
191                 [example-config.yaml](https://github.com/mautrix/meta/blob/main/example-config.yaml).
193                 Secret tokens should be specified using {option}`environmentFile`
194                 instead
195               '';
196             };
198             environmentFile = lib.mkOption {
199               type = lib.types.nullOr lib.types.path;
200               default = null;
201               description = ''
202                 File containing environment variables to substitute when copying the configuration
203                 out of Nix store to the `services.mautrix-meta.dataDir`.
205                 Can be used for storing the secrets without making them available in the Nix store.
207                 For example, you can set `services.mautrix-meta.settings.appservice.as_token = "$MAUTRIX_META_APPSERVICE_AS_TOKEN"`
208                 and then specify `MAUTRIX_META_APPSERVICE_AS_TOKEN="{token}"` in the environment file.
209                 This value will get substituted into the configuration file as as token.
210               '';
211             };
213             serviceDependencies = lib.mkOption {
214               type = lib.types.listOf lib.types.str;
215               default =
216                 [ config.registrationServiceUnit ] ++
217                 (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) ++
218                 (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") ++
219                 (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service");
221               defaultText = ''
222                 [ config.registrationServiceUnit ] ++
223                 (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) ++
224                 (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") ++
225                 (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service");
226               '';
227               description = ''
228                 List of Systemd services to require and wait for when starting the application service.
229               '';
230             };
232             serviceUnit = lib.mkOption {
233               type = lib.types.str;
234               readOnly = true;
235               description = ''
236                 The systemd unit (a service or a target) for other services to depend on if they
237                 need to be started after matrix-synapse.
239                 This option is useful as the actual parent unit for all matrix-synapse processes
240                 changes when configuring workers.
241               '';
242             };
244             registrationServiceUnit = lib.mkOption {
245               type = lib.types.str;
246               readOnly = true;
247               description = ''
248                 The registration service that generates the registration file.
250                 Systemd unit (a service or a target) for other services to depend on if they
251                 need to be started after mautrix-meta registration service.
253                 This option is useful as the actual parent unit for all matrix-synapse processes
254                 changes when configuring workers.
255               '';
256             };
257           };
259           config = {
260             serviceUnit = (metaName name) + ".service";
261             registrationServiceUnit = (metaName name) + "-registration.service";
262             registrationFile = (fullDataDir config) + "/meta-registration.yaml";
263           };
264         }));
266         description = ''
267           Configuration of multiple `mautrix-meta` instances.
268           `services.mautrix-meta.instances.facebook` and `services.mautrix-meta.instances.instagram`
269           come preconfigured with network.mode, appservice.id, bot username, display name and avatar.
270         '';
272         example = ''
273           {
274             facebook = {
275               enable = true;
276               settings = {
277                 homeserver.domain = "example.com";
278               };
279             };
281             instagram = {
282               enable = true;
283               settings = {
284                 homeserver.domain = "example.com";
285               };
286             };
288             messenger = {
289               enable = true;
290               settings = {
291                 network.mode = "messenger";
292                 homeserver.domain = "example.com";
293                 appservice = {
294                   id = "messenger";
295                   bot = {
296                     username = "messengerbot";
297                     displayname = "Messenger bridge bot";
298                     avatar = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
299                   };
300                 };
301               };
302             };
303           }
304         '';
305       };
306     };
307   };
309   config = lib.mkMerge [
310     (lib.mkIf (enabledInstances != {}) {
311       assertions = lib.mkMerge (lib.attrValues (lib.mapAttrs (name: cfg: [
312         {
313           assertion = cfg.settings.homeserver.domain != "" && cfg.settings.homeserver.address != "";
314           message = ''
315             The options with information about the homeserver:
316             `services.mautrix-meta.instances.${name}.settings.homeserver.domain` and
317             `services.mautrix-meta.instances.${name}.settings.homeserver.address` have to be set.
318           '';
319         }
320         {
321           assertion = builtins.elem cfg.settings.network.mode [ "facebook" "facebook-tor" "messenger" "instagram" ];
322           message = ''
323             The option `services.mautrix-meta.instances.${name}.settings.network.mode` has to be set
324             to one of: facebook, facebook-tor, messenger, instagram.
325             This configures the mode of the bridge.
326           '';
327         }
328         {
329           assertion = cfg.settings.bridge.permissions != {};
330           message = ''
331             The option `services.mautrix-meta.instances.${name}.settings.bridge.permissions` has to be set.
332           '';
333         }
334         {
335           assertion = cfg.settings.appservice.id != "";
336           message = ''
337             The option `services.mautrix-meta.instances.${name}.settings.appservice.id` has to be set.
338           '';
339         }
340         {
341           assertion = cfg.settings.appservice.bot.username != "";
342           message = ''
343             The option `services.mautrix-meta.instances.${name}.settings.appservice.bot.username` has to be set.
344           '';
345         }
346         {
347           assertion = !(cfg.settings ? bridge.disable_xma);
348           message = ''
349             The option `bridge.disable_xma` has been moved to `network.disable_xma_always`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference.
350           '';
351         }
352         {
353           assertion = !(cfg.settings ? bridge.displayname_template);
354           message = ''
355             The option `bridge.displayname_template` has been moved to `network.displayname_template`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference.
356           '';
357         }
358         {
359           assertion = !(cfg.settings ? meta);
360           message = ''
361             The options in `meta` have been moved to `network`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference.
362           '';
363         }
364       ]) enabledInstances));
366       users.users = lib.mapAttrs' (name: cfg: lib.nameValuePair "mautrix-meta-${name}" {
367         isSystemUser = true;
368         group = "mautrix-meta";
369         extraGroups = [ "mautrix-meta-registration" ];
370         description = "Mautrix-Meta-${name} bridge user";
371       }) enabledInstances;
373       users.groups.mautrix-meta = {};
374       users.groups.mautrix-meta-registration = {
375         members = lib.lists.optional config.services.matrix-synapse.enable "matrix-synapse";
376       };
378       services.matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) (let
379         registrationFiles = lib.attrValues
380           (lib.mapAttrs (name: cfg: cfg.registrationFile) registerToSynapseInstances);
381       in {
382         settings.app_service_config_files = registrationFiles;
383       });
385       systemd.services = lib.mkMerge [
386         {
387           matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) (let
388             registrationServices = lib.attrValues
389               (lib.mapAttrs (name: cfg: cfg.registrationServiceUnit) registerToSynapseInstances);
390           in {
391             wants = registrationServices;
392             after = registrationServices;
393           });
394         }
396         (lib.mapAttrs' (name: cfg: lib.nameValuePair "${metaName name}-registration" {
397           description = "Mautrix-Meta registration generation service - ${metaName name}";
399           path = [
400             pkgs.yq
401             pkgs.envsubst
402             upperCfg.package
403           ];
405           script = ''
406             # substitute the settings file by environment variables
407             # in this case read from EnvironmentFile
408             rm -f '${settingsFile cfg}'
409             old_umask=$(umask)
410             umask 0177
411             envsubst \
412               -o '${settingsFile cfg}' \
413               -i '${settingsFileUnsubstituted cfg}'
415             config_has_tokens=$(yq '.appservice | has("as_token") and has("hs_token")' '${settingsFile cfg}')
416             registration_already_exists=$([[ -f '${cfg.registrationFile}' ]] && echo "true" || echo "false")
418             echo "There are tokens in the config: $config_has_tokens"
419             echo "Registration already existed: $registration_already_exists"
421             # tokens not configured from config/environment file, and registration file
422             # is already generated, override tokens in config to make sure they are not lost
423             if [[ $config_has_tokens == "false" && $registration_already_exists == "true" ]]; then
424               echo "Copying as_token, hs_token from registration into configuration"
425               yq -sY '.[0].appservice.as_token = .[1].as_token
426                 | .[0].appservice.hs_token = .[1].hs_token
427                 | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \
428                 > '${settingsFile cfg}.tmp'
429               mv '${settingsFile cfg}.tmp' '${settingsFile cfg}'
430             fi
432             # make sure --generate-registration does not affect config.yaml
433             cp '${settingsFile cfg}' '${settingsFile cfg}.tmp'
435             echo "Generating registration file"
436             mautrix-meta \
437               --generate-registration \
438               --config='${settingsFile cfg}.tmp' \
439               --registration='${cfg.registrationFile}'
441             rm '${settingsFile cfg}.tmp'
443             # no tokens configured, and new were just generated by generate registration for first time
444             if [[ $config_has_tokens == "false" && $registration_already_exists == "false" ]]; then
445               echo "Copying newly generated as_token, hs_token from registration into configuration"
446               yq -sY '.[0].appservice.as_token = .[1].as_token
447                 | .[0].appservice.hs_token = .[1].hs_token
448                 | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \
449                 > '${settingsFile cfg}.tmp'
450               mv '${settingsFile cfg}.tmp' '${settingsFile cfg}'
451             fi
453             # Make sure correct tokens are in the registration file
454             if [[ $config_has_tokens == "true" || $registration_already_exists == "true" ]]; then
455               echo "Copying as_token, hs_token from configuration to the registration file"
456               yq -sY '.[1].as_token = .[0].appservice.as_token
457                 | .[1].hs_token = .[0].appservice.hs_token
458                 | .[1]' '${settingsFile cfg}' '${cfg.registrationFile}' \
459                 > '${cfg.registrationFile}.tmp'
460               mv '${cfg.registrationFile}.tmp' '${cfg.registrationFile}'
461             fi
463             umask $old_umask
465             chown :mautrix-meta-registration '${cfg.registrationFile}'
466             chmod 640 '${cfg.registrationFile}'
467           '';
469           serviceConfig = {
470             Type = "oneshot";
471             UMask = 0027;
473             User = "mautrix-meta-${name}";
474             Group = "mautrix-meta";
476             SystemCallFilter = [ "@system-service" ];
478             ProtectSystem = "strict";
479             ProtectHome = true;
481             ReadWritePaths = fullDataDir cfg;
482             StateDirectory = cfg.dataDir;
483             EnvironmentFile = cfg.environmentFile;
484           };
486           restartTriggers = [ (settingsFileUnsubstituted cfg) ];
487         }) enabledInstances)
489         (lib.mapAttrs' (name: cfg: lib.nameValuePair "${metaName name}" {
490           description = "Mautrix-Meta bridge - ${metaName name}";
491           wantedBy = [ "multi-user.target" ];
492           wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
493           after = [ "network-online.target" ] ++ cfg.serviceDependencies;
495           serviceConfig = {
496             Type = "simple";
498             User = "mautrix-meta-${name}";
499             Group = "mautrix-meta";
500             PrivateUsers = true;
502             LockPersonality = true;
503             MemoryDenyWriteExecute = true;
504             NoNewPrivileges = true;
505             PrivateDevices = true;
506             PrivateTmp = true;
507             ProtectClock = true;
508             ProtectControlGroups = true;
509             ProtectHome = true;
510             ProtectHostname = true;
511             ProtectKernelLogs = true;
512             ProtectKernelModules = true;
513             ProtectKernelTunables = true;
514             ProtectSystem = "strict";
515             Restart = "on-failure";
516             RestartSec = "30s";
517             RestrictRealtime = true;
518             RestrictSUIDSGID = true;
519             SystemCallArchitectures = "native";
520             SystemCallErrorNumber = "EPERM";
521             SystemCallFilter = ["@system-service"];
522             UMask = 0027;
524             WorkingDirectory = fullDataDir cfg;
525             ReadWritePaths = fullDataDir cfg;
526             StateDirectory = cfg.dataDir;
527             EnvironmentFile = cfg.environmentFile;
529             ExecStart = lib.escapeShellArgs [
530               (lib.getExe upperCfg.package)
531               "--config=${settingsFile cfg}"
532             ];
533           };
534           restartTriggers = [ (settingsFileUnsubstituted cfg) ];
535         }) enabledInstances)
536       ];
537     })
538     {
539       services.mautrix-meta.instances = let
540         inherit (lib.modules) mkDefault;
541       in {
542         instagram = {
543           settings = {
544             network.mode = mkDefault "instagram";
546             appservice = {
547               id = mkDefault "instagram";
548               port = mkDefault 29320;
549               bot = {
550                 username = mkDefault "instagrambot";
551                 displayname = mkDefault "Instagram bridge bot";
552                 avatar = mkDefault "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv";
553               };
554               username_template = mkDefault "instagram_{{.}}";
555             };
556           };
557         };
558         facebook = {
559           settings = {
560             network.mode = mkDefault "facebook";
562             appservice = {
563               id = mkDefault "facebook";
564               port = mkDefault 29321;
565               bot = {
566                 username = mkDefault "facebookbot";
567                 displayname = mkDefault "Facebook bridge bot";
568                 avatar = mkDefault "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
569               };
570               username_template = mkDefault "facebook_{{.}}";
571             };
572           };
573         };
574       };
575     }
576   ];
578   meta.maintainers = with lib.maintainers; [ ];