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