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 ]
231 ++ (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit)
232 ++ (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service")
233 ++ (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service");
234
235 defaultText = ''
236 [ config.registrationServiceUnit ] ++
237 (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) ++
238 (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") ++
239 (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service");
240 '';
241 description = ''
242 List of Systemd services to require and wait for when starting the application service.
243 '';
244 };
245
246 serviceUnit = lib.mkOption {
247 type = lib.types.str;
248 readOnly = true;
249 description = ''
250 The systemd unit (a service or a target) for other services to depend on if they
251 need to be started after matrix-synapse.
252
253 This option is useful as the actual parent unit for all matrix-synapse processes
254 changes when configuring workers.
255 '';
256 };
257
258 registrationServiceUnit = lib.mkOption {
259 type = lib.types.str;
260 readOnly = true;
261 description = ''
262 The registration service that generates the registration file.
263
264 Systemd unit (a service or a target) for other services to depend on if they
265 need to be started after mautrix-meta registration service.
266
267 This option is useful as the actual parent unit for all matrix-synapse processes
268 changes when configuring workers.
269 '';
270 };
271 };
272
273 config = {
274 serviceUnit = (metaName name) + ".service";
275 registrationServiceUnit = (metaName name) + "-registration.service";
276 registrationFile = (fullDataDir config) + "/meta-registration.yaml";
277 };
278 }
279 )
280 );
281
282 description = ''
283 Configuration of multiple `mautrix-meta` instances.
284 `services.mautrix-meta.instances.facebook` and `services.mautrix-meta.instances.instagram`
285 come preconfigured with network.mode, appservice.id, bot username, display name and avatar.
286 '';
287
288 example = ''
289 {
290 facebook = {
291 enable = true;
292 settings = {
293 homeserver.domain = "example.com";
294 };
295 };
296
297 instagram = {
298 enable = true;
299 settings = {
300 homeserver.domain = "example.com";
301 };
302 };
303
304 messenger = {
305 enable = true;
306 settings = {
307 network.mode = "messenger";
308 homeserver.domain = "example.com";
309 appservice = {
310 id = "messenger";
311 bot = {
312 username = "messengerbot";
313 displayname = "Messenger bridge bot";
314 avatar = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
315 };
316 };
317 };
318 };
319 }
320 '';
321 };
322 };
323 };
324
325 config = lib.mkMerge [
326 (lib.mkIf (enabledInstances != { }) {
327 assertions = lib.mkMerge (
328 lib.attrValues (
329 lib.mapAttrs (name: cfg: [
330 {
331 assertion = cfg.settings.homeserver.domain != "" && cfg.settings.homeserver.address != "";
332 message = ''
333 The options with information about the homeserver:
334 `services.mautrix-meta.instances.${name}.settings.homeserver.domain` and
335 `services.mautrix-meta.instances.${name}.settings.homeserver.address` have to be set.
336 '';
337 }
338 {
339 assertion = builtins.elem cfg.settings.network.mode [
340 "facebook"
341 "facebook-tor"
342 "messenger"
343 "instagram"
344 ];
345 message = ''
346 The option `services.mautrix-meta.instances.${name}.settings.network.mode` has to be set
347 to one of: facebook, facebook-tor, messenger, instagram.
348 This configures the mode of the bridge.
349 '';
350 }
351 {
352 assertion = cfg.settings.bridge.permissions != { };
353 message = ''
354 The option `services.mautrix-meta.instances.${name}.settings.bridge.permissions` has to be set.
355 '';
356 }
357 {
358 assertion = cfg.settings.appservice.id != "";
359 message = ''
360 The option `services.mautrix-meta.instances.${name}.settings.appservice.id` has to be set.
361 '';
362 }
363 {
364 assertion = cfg.settings.appservice.bot.username != "";
365 message = ''
366 The option `services.mautrix-meta.instances.${name}.settings.appservice.bot.username` has to be set.
367 '';
368 }
369 {
370 assertion = !(cfg.settings ? bridge.disable_xma);
371 message = ''
372 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.
373 '';
374 }
375 {
376 assertion = !(cfg.settings ? bridge.displayname_template);
377 message = ''
378 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.
379 '';
380 }
381 {
382 assertion = !(cfg.settings ? meta);
383 message = ''
384 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.
385 '';
386 }
387 ]) enabledInstances
388 )
389 );
390
391 users.users = lib.mapAttrs' (
392 name: cfg:
393 lib.nameValuePair "mautrix-meta-${name}" {
394 isSystemUser = true;
395 group = "mautrix-meta";
396 extraGroups = [ "mautrix-meta-registration" ];
397 description = "Mautrix-Meta-${name} bridge user";
398 }
399 ) enabledInstances;
400
401 users.groups.mautrix-meta = { };
402 users.groups.mautrix-meta-registration = {
403 members = lib.lists.optional config.services.matrix-synapse.enable "matrix-synapse";
404 };
405
406 services.matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) (
407 let
408 registrationFiles = lib.attrValues (
409 lib.mapAttrs (name: cfg: cfg.registrationFile) registerToSynapseInstances
410 );
411 in
412 {
413 settings.app_service_config_files = registrationFiles;
414 }
415 );
416
417 systemd.services = lib.mkMerge [
418 {
419 matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) (
420 let
421 registrationServices = lib.attrValues (
422 lib.mapAttrs (name: cfg: cfg.registrationServiceUnit) registerToSynapseInstances
423 );
424 in
425 {
426 wants = registrationServices;
427 after = registrationServices;
428 }
429 );
430 }
431
432 (lib.mapAttrs' (
433 name: cfg:
434 lib.nameValuePair "${metaName name}-registration" {
435 description = "Mautrix-Meta registration generation service - ${metaName name}";
436
437 path = [
438 pkgs.yq
439 pkgs.envsubst
440 upperCfg.package
441 ];
442
443 script = ''
444 # substitute the settings file by environment variables
445 # in this case read from EnvironmentFile
446 rm -f '${settingsFile cfg}'
447 old_umask=$(umask)
448 umask 0177
449 envsubst \
450 -o '${settingsFile cfg}' \
451 -i '${settingsFileUnsubstituted cfg}'
452
453 config_has_tokens=$(yq '.appservice | has("as_token") and has("hs_token")' '${settingsFile cfg}')
454 registration_already_exists=$([[ -f '${cfg.registrationFile}' ]] && echo "true" || echo "false")
455
456 echo "There are tokens in the config: $config_has_tokens"
457 echo "Registration already existed: $registration_already_exists"
458
459 # tokens not configured from config/environment file, and registration file
460 # is already generated, override tokens in config to make sure they are not lost
461 if [[ $config_has_tokens == "false" && $registration_already_exists == "true" ]]; then
462 echo "Copying as_token, hs_token from registration into configuration"
463 yq -sY '.[0].appservice.as_token = .[1].as_token
464 | .[0].appservice.hs_token = .[1].hs_token
465 | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \
466 > '${settingsFile cfg}.tmp'
467 mv '${settingsFile cfg}.tmp' '${settingsFile cfg}'
468 fi
469
470 # make sure --generate-registration does not affect config.yaml
471 cp '${settingsFile cfg}' '${settingsFile cfg}.tmp'
472
473 echo "Generating registration file"
474 mautrix-meta \
475 --generate-registration \
476 --config='${settingsFile cfg}.tmp' \
477 --registration='${cfg.registrationFile}'
478
479 rm '${settingsFile cfg}.tmp'
480
481 # no tokens configured, and new were just generated by generate registration for first time
482 if [[ $config_has_tokens == "false" && $registration_already_exists == "false" ]]; then
483 echo "Copying newly generated as_token, hs_token from registration into configuration"
484 yq -sY '.[0].appservice.as_token = .[1].as_token
485 | .[0].appservice.hs_token = .[1].hs_token
486 | .[0]' '${settingsFile cfg}' '${cfg.registrationFile}' \
487 > '${settingsFile cfg}.tmp'
488 mv '${settingsFile cfg}.tmp' '${settingsFile cfg}'
489 fi
490
491 # Make sure correct tokens are in the registration file
492 if [[ $config_has_tokens == "true" || $registration_already_exists == "true" ]]; then
493 echo "Copying as_token, hs_token from configuration to the registration file"
494 yq -sY '.[1].as_token = .[0].appservice.as_token
495 | .[1].hs_token = .[0].appservice.hs_token
496 | .[1]' '${settingsFile cfg}' '${cfg.registrationFile}' \
497 > '${cfg.registrationFile}.tmp'
498 mv '${cfg.registrationFile}.tmp' '${cfg.registrationFile}'
499 fi
500
501 umask $old_umask
502
503 chown :mautrix-meta-registration '${cfg.registrationFile}'
504 chmod 640 '${cfg.registrationFile}'
505 '';
506
507 serviceConfig = {
508 Type = "oneshot";
509 UMask = 27;
510
511 User = "mautrix-meta-${name}";
512 Group = "mautrix-meta";
513
514 SystemCallFilter = [ "@system-service" ];
515
516 ProtectSystem = "strict";
517 ProtectHome = true;
518
519 ReadWritePaths = fullDataDir cfg;
520 StateDirectory = cfg.dataDir;
521 EnvironmentFile = cfg.environmentFile;
522 };
523
524 restartTriggers = [ (settingsFileUnsubstituted cfg) ];
525 }
526 ) enabledInstances)
527
528 (lib.mapAttrs' (
529 name: cfg:
530 lib.nameValuePair "${metaName name}" {
531 description = "Mautrix-Meta bridge - ${metaName name}";
532 wantedBy = [ "multi-user.target" ];
533 wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
534 after = [ "network-online.target" ] ++ cfg.serviceDependencies;
535
536 serviceConfig = {
537 Type = "simple";
538
539 User = "mautrix-meta-${name}";
540 Group = "mautrix-meta";
541 PrivateUsers = true;
542
543 LockPersonality = true;
544 MemoryDenyWriteExecute = true;
545 NoNewPrivileges = true;
546 PrivateDevices = true;
547 PrivateTmp = true;
548 ProtectClock = true;
549 ProtectControlGroups = true;
550 ProtectHome = true;
551 ProtectHostname = true;
552 ProtectKernelLogs = true;
553 ProtectKernelModules = true;
554 ProtectKernelTunables = true;
555 ProtectSystem = "strict";
556 Restart = "on-failure";
557 RestartSec = "30s";
558 RestrictRealtime = true;
559 RestrictSUIDSGID = true;
560 SystemCallArchitectures = "native";
561 SystemCallErrorNumber = "EPERM";
562 SystemCallFilter = [ "@system-service" ];
563 UMask = 27;
564
565 WorkingDirectory = fullDataDir cfg;
566 ReadWritePaths = fullDataDir cfg;
567 StateDirectory = cfg.dataDir;
568 EnvironmentFile = cfg.environmentFile;
569
570 ExecStart = lib.escapeShellArgs [
571 (lib.getExe upperCfg.package)
572 "--config=${settingsFile cfg}"
573 ];
574 };
575 restartTriggers = [ (settingsFileUnsubstituted cfg) ];
576 }
577 ) enabledInstances)
578 ];
579 })
580 {
581 services.mautrix-meta.instances =
582 let
583 inherit (lib.modules) mkDefault;
584 in
585 {
586 instagram = {
587 settings = {
588 network.mode = mkDefault "instagram";
589
590 appservice = {
591 id = mkDefault "instagram";
592 port = mkDefault 29320;
593 bot = {
594 username = mkDefault "instagrambot";
595 displayname = mkDefault "Instagram bridge bot";
596 avatar = mkDefault "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv";
597 };
598 username_template = mkDefault "instagram_{{.}}";
599 };
600 };
601 };
602 facebook = {
603 settings = {
604 network.mode = mkDefault "facebook";
605
606 appservice = {
607 id = mkDefault "facebook";
608 port = mkDefault 29321;
609 bot = {
610 username = mkDefault "facebookbot";
611 displayname = mkDefault "Facebook bridge bot";
612 avatar = mkDefault "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
613 };
614 username_template = mkDefault "facebook_{{.}}";
615 };
616 };
617 };
618 };
619 }
620 ];
621
622 meta.maintainers = with lib.maintainers; [ ];
623}