1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 dataDir = "/var/lib/mautrix-telegram";
9 registrationFile = "${dataDir}/telegram-registration.yaml";
10 cfg = config.services.mautrix-telegram;
11 settingsFormat = pkgs.formats.json { };
12 settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config.json" cfg.settings;
13 settingsFile = "${dataDir}/config.json";
14
15in
16{
17 options = {
18 services.mautrix-telegram = {
19 enable = lib.mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
20
21 package = lib.mkPackageOption pkgs "mautrix-telegram" { };
22
23 settings = lib.mkOption rec {
24 apply = lib.recursiveUpdate default;
25 inherit (settingsFormat) type;
26 default = {
27 homeserver = {
28 software = "standard";
29 };
30
31 appservice = rec {
32 database = "sqlite:///${dataDir}/mautrix-telegram.db";
33 database_opts = { };
34 hostname = "0.0.0.0";
35 port = 8080;
36 address = "http://localhost:${toString port}";
37 };
38
39 bridge = {
40 permissions."*" = "relaybot";
41 relaybot.whitelist = [ ];
42 double_puppet_server_map = { };
43 login_shared_secret_map = { };
44 };
45
46 logging = {
47 version = 1;
48
49 formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
50
51 handlers.console = {
52 class = "logging.StreamHandler";
53 formatter = "precise";
54 };
55
56 loggers = {
57 mau.level = "INFO";
58 telethon.level = "INFO";
59
60 # prevent tokens from leaking in the logs:
61 # https://github.com/tulir/mautrix-telegram/issues/351
62 aiohttp.level = "WARNING";
63 };
64
65 # log to console/systemd instead of file
66 root = {
67 level = "INFO";
68 handlers = [ "console" ];
69 };
70 };
71 };
72 example = lib.literalExpression ''
73 {
74 homeserver = {
75 address = "http://localhost:8008";
76 domain = "public-domain.tld";
77 };
78
79 appservice.public = {
80 prefix = "/public";
81 external = "https://public-appservice-address/public";
82 };
83
84 bridge.permissions = {
85 "example.com" = "full";
86 "@admin:example.com" = "admin";
87 };
88 telegram = {
89 connection.use_ipv6 = true;
90 };
91 }
92 '';
93 description = ''
94 {file}`config.yaml` configuration as a Nix attribute set.
95 Configuration options should match those described in
96 [example-config.yaml](https://github.com/mautrix/telegram/blob/master/mautrix_telegram/example-config.yaml).
97
98 Secret tokens should be specified using {option}`environmentFile`
99 instead of this world-readable attribute set.
100 '';
101 };
102
103 environmentFile = lib.mkOption {
104 type = lib.types.nullOr lib.types.path;
105 default = null;
106 description = ''
107 File containing environment variables to be passed to the mautrix-telegram service,
108 in which secret tokens can be specified securely by defining values for e.g.
109 `MAUTRIX_TELEGRAM_APPSERVICE_AS_TOKEN`,
110 `MAUTRIX_TELEGRAM_APPSERVICE_HS_TOKEN`,
111 `MAUTRIX_TELEGRAM_TELEGRAM_API_ID`,
112 `MAUTRIX_TELEGRAM_TELEGRAM_API_HASH` and optionally
113 `MAUTRIX_TELEGRAM_TELEGRAM_BOT_TOKEN`.
114
115 These environment variables can also be used to set other options by
116 replacing hierarchy levels by `.`, converting the name to uppercase
117 and prepending `MAUTRIX_TELEGRAM_`.
118 For example, the first value above maps to
119 {option}`settings.appservice.as_token`.
120
121 The environment variable values can be prefixed with `json::` to have
122 them be parsed as JSON. For example, `login_shared_secret_map` can be
123 set as follows:
124 `MAUTRIX_TELEGRAM_BRIDGE_LOGIN_SHARED_SECRET_MAP=json::{"example.com":"secret"}`.
125 '';
126 };
127
128 serviceDependencies = lib.mkOption {
129 type = with lib.types; listOf str;
130 default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
131 defaultText = lib.literalExpression ''
132 lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit
133 '';
134 description = ''
135 List of Systemd services to require and wait for when starting the application service.
136 '';
137 };
138
139 registerToSynapse = lib.mkOption {
140 type = lib.types.bool;
141 default = config.services.matrix-synapse.enable;
142 defaultText = lib.literalExpression "config.services.matrix-synapse.enable";
143 description = ''
144 Whether to add the bridge's app service registration file to
145 `services.matrix-synapse.settings.app_service_config_files`.
146 '';
147 };
148 };
149 };
150
151 config = lib.mkIf cfg.enable {
152
153 users.users.mautrix-telegram = {
154 isSystemUser = true;
155 group = "mautrix-telegram";
156 home = dataDir;
157 description = "Mautrix-Telegram bridge user";
158 };
159
160 users.groups.mautrix-telegram = { };
161
162 services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
163 settings.app_service_config_files = [ registrationFile ];
164 };
165 systemd.services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
166 serviceConfig.SupplementaryGroups = [ "mautrix-telegram" ];
167 };
168
169 systemd.services.mautrix-telegram = {
170 description = "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge.";
171
172 wantedBy = [ "multi-user.target" ];
173 wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
174 after = [ "network-online.target" ] ++ cfg.serviceDependencies;
175 path = [
176 pkgs.lottieconverter
177 pkgs.ffmpeg-headless
178 ];
179
180 # mautrix-telegram tries to generate a dotfile in the home directory of
181 # the running user if using a postgresql database:
182 #
183 # File "python3.10/site-packages/asyncpg/connect_utils.py", line 257, in _dot_postgre>
184 # return (pathlib.Path.home() / '.postgresql' / filename).resolve()
185 # File "python3.10/pathlib.py", line 1000, in home
186 # return cls("~").expanduser()
187 # File "python3.10/pathlib.py", line 1440, in expanduser
188 # raise RuntimeError("Could not determine home directory.")
189 # RuntimeError: Could not determine home directory.
190 environment.HOME = dataDir;
191
192 preStart =
193 ''
194 # substitute the settings file by environment variables
195 # in this case read from EnvironmentFile
196 test -f '${settingsFile}' && rm -f '${settingsFile}'
197 old_umask=$(umask)
198 umask 0177
199 ${pkgs.envsubst}/bin/envsubst \
200 -o '${settingsFile}' \
201 -i '${settingsFileUnsubstituted}'
202 umask $old_umask
203
204 # generate the appservice's registration file if absent
205 if [ ! -f '${registrationFile}' ]; then
206 ${cfg.package}/bin/mautrix-telegram \
207 --generate-registration \
208 --config='${settingsFile}' \
209 --registration='${registrationFile}'
210 fi
211
212 old_umask=$(umask)
213 umask 0177
214 # 1. Overwrite registration tokens in config
215 # is set, set it as the login shared secret value for the configured
216 # homeserver domain.
217 ${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
218 | .[0].appservice.hs_token = .[1].hs_token
219 | .[0]' \
220 '${settingsFile}' '${registrationFile}' > '${settingsFile}.tmp'
221 mv '${settingsFile}.tmp' '${settingsFile}'
222
223 umask $old_umask
224 ''
225 + lib.optionalString (cfg.package ? alembic) ''
226 # run automatic database init and migration scripts
227 ${cfg.package.alembic}/bin/alembic -x config='${settingsFile}' upgrade head
228 '';
229
230 serviceConfig = {
231 User = "mautrix-telegram";
232 Group = "mautrix-telegram";
233 Type = "simple";
234 Restart = "always";
235
236 ProtectSystem = "strict";
237 ProtectHome = true;
238 ProtectKernelTunables = true;
239 ProtectKernelModules = true;
240 ProtectControlGroups = true;
241
242 PrivateTmp = true;
243 WorkingDirectory = cfg.package; # necessary for the database migration scripts to be found
244 StateDirectory = baseNameOf dataDir;
245 UMask = "0027";
246 EnvironmentFile = cfg.environmentFile;
247
248 ExecStart = ''
249 ${cfg.package}/bin/mautrix-telegram \
250 --config='${settingsFile}'
251 '';
252 };
253 };
254 };
255
256 meta.maintainers = with lib.maintainers; [
257 euxane
258 vskilet
259 ];
260}