1{
2 lib,
3 config,
4 pkgs,
5 ...
6}: let
7 cfg = config.services.mautrix-whatsapp;
8 dataDir = "/var/lib/mautrix-whatsapp";
9 registrationFile = "${dataDir}/whatsapp-registration.yaml";
10 settingsFile = "${dataDir}/config.json";
11 settingsFileUnsubstituted = settingsFormat.generate "mautrix-whatsapp-config-unsubstituted.json" cfg.settings;
12 settingsFormat = pkgs.formats.json {};
13 appservicePort = 29318;
14
15 mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
16 defaultConfig = {
17 homeserver.address = "http://localhost:8448";
18 appservice = {
19 hostname = "[::]";
20 port = appservicePort;
21 database.type = "sqlite3";
22 database.uri = "${dataDir}/mautrix-whatsapp.db";
23 id = "whatsapp";
24 bot.username = "whatsappbot";
25 bot.displayname = "WhatsApp Bridge Bot";
26 as_token = "";
27 hs_token = "";
28 };
29 bridge = {
30 username_template = "whatsapp_{{.}}";
31 displayname_template = "{{if .BusinessName}}{{.BusinessName}}{{else if .PushName}}{{.PushName}}{{else}}{{.JID}}{{end}} (WA)";
32 double_puppet_server_map = {};
33 login_shared_secret_map = {};
34 command_prefix = "!wa";
35 permissions."*" = "relay";
36 relay.enabled = true;
37 };
38 logging = {
39 min_level = "info";
40 writers = lib.singleton {
41 type = "stdout";
42 format = "pretty-colored";
43 time_format = " ";
44 };
45 };
46 };
47
48in {
49 options.services.mautrix-whatsapp = {
50 enable = lib.mkEnableOption (lib.mdDoc "mautrix-whatsapp, a puppeting/relaybot bridge between Matrix and WhatsApp.");
51
52 settings = lib.mkOption {
53 type = settingsFormat.type;
54 default = defaultConfig;
55 description = lib.mdDoc ''
56 {file}`config.yaml` configuration as a Nix attribute set.
57 Configuration options should match those described in
58 [example-config.yaml](https://github.com/mautrix/whatsapp/blob/master/example-config.yaml).
59 Secret tokens should be specified using {option}`environmentFile`
60 instead of this world-readable attribute set.
61 '';
62 example = {
63 appservice = {
64 database = {
65 type = "postgres";
66 uri = "postgresql:///mautrix_whatsapp?host=/run/postgresql";
67 };
68 id = "whatsapp";
69 ephemeral_events = false;
70 };
71 bridge = {
72 history_sync = {
73 request_full_sync = true;
74 };
75 private_chat_portal_meta = true;
76 mute_bridging = true;
77 encryption = {
78 allow = true;
79 default = true;
80 require = true;
81 };
82 provisioning = {
83 shared_secret = "disable";
84 };
85 permissions = {
86 "example.com" = "user";
87 };
88 };
89 };
90 };
91 environmentFile = lib.mkOption {
92 type = lib.types.nullOr lib.types.path;
93 default = null;
94 description = lib.mdDoc ''
95 File containing environment variables to be passed to the mautrix-whatsapp service,
96 in which secret tokens can be specified securely by optionally defining a value for
97 `MAUTRIX_WHATSAPP_BRIDGE_LOGIN_SHARED_SECRET`.
98 '';
99 };
100
101 serviceDependencies = lib.mkOption {
102 type = with lib.types; listOf str;
103 default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
104 defaultText = lib.literalExpression ''
105 optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnits
106 '';
107 description = lib.mdDoc ''
108 List of Systemd services to require and wait for when starting the application service.
109 '';
110 };
111 };
112
113 config = lib.mkIf cfg.enable {
114
115 users.users.mautrix-whatsapp = {
116 isSystemUser = true;
117 group = "mautrix-whatsapp";
118 home = dataDir;
119 description = "Mautrix-WhatsApp bridge user";
120 };
121
122 users.groups.mautrix-whatsapp = {};
123
124 services.mautrix-whatsapp.settings = lib.mkMerge (map mkDefaults [
125 defaultConfig
126 # Note: this is defined here to avoid the docs depending on `config`
127 { homeserver.domain = config.services.matrix-synapse.settings.server_name; }
128 ]);
129
130 systemd.services.mautrix-whatsapp = {
131 description = "Mautrix-WhatsApp Service - A WhatsApp bridge for Matrix";
132
133 wantedBy = ["multi-user.target"];
134 wants = ["network-online.target"] ++ cfg.serviceDependencies;
135 after = ["network-online.target"] ++ cfg.serviceDependencies;
136
137 preStart = ''
138 # substitute the settings file by environment variables
139 # in this case read from EnvironmentFile
140 test -f '${settingsFile}' && rm -f '${settingsFile}'
141 old_umask=$(umask)
142 umask 0177
143 ${pkgs.envsubst}/bin/envsubst \
144 -o '${settingsFile}' \
145 -i '${settingsFileUnsubstituted}'
146 umask $old_umask
147
148 # generate the appservice's registration file if absent
149 if [ ! -f '${registrationFile}' ]; then
150 ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \
151 --generate-registration \
152 --config='${settingsFile}' \
153 --registration='${registrationFile}'
154 fi
155 chmod 640 ${registrationFile}
156
157 umask 0177
158 ${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
159 | .[0].appservice.hs_token = .[1].hs_token
160 | .[0]' '${settingsFile}' '${registrationFile}' \
161 > '${settingsFile}.tmp'
162 mv '${settingsFile}.tmp' '${settingsFile}'
163 umask $old_umask
164 '';
165
166 serviceConfig = {
167 User = "mautrix-whatsapp";
168 Group = "mautrix-whatsapp";
169 EnvironmentFile = cfg.environmentFile;
170 StateDirectory = baseNameOf dataDir;
171 WorkingDirectory = dataDir;
172 ExecStart = ''
173 ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \
174 --config='${settingsFile}' \
175 --registration='${registrationFile}'
176 '';
177 LockPersonality = true;
178 MemoryDenyWriteExecute = true;
179 NoNewPrivileges = true;
180 PrivateDevices = true;
181 PrivateTmp = true;
182 PrivateUsers = true;
183 ProtectClock = true;
184 ProtectControlGroups = true;
185 ProtectHome = true;
186 ProtectHostname = true;
187 ProtectKernelLogs = true;
188 ProtectKernelModules = true;
189 ProtectKernelTunables = true;
190 ProtectSystem = "strict";
191 Restart = "on-failure";
192 RestartSec = "30s";
193 RestrictRealtime = true;
194 RestrictSUIDSGID = true;
195 SystemCallArchitectures = "native";
196 SystemCallErrorNumber = "EPERM";
197 SystemCallFilter = ["@system-service"];
198 Type = "simple";
199 UMask = 0027;
200 };
201 restartTriggers = [settingsFileUnsubstituted];
202 };
203 };
204 meta.maintainers = with lib.maintainers; [frederictobiasc];
205}