1{
2 config,
3 options,
4 lib,
5 pkgs,
6 ...
7}:
8
9let
10 cfg = config.services.draupnir;
11 opt = options.services.draupnir;
12
13 format = pkgs.formats.yaml { };
14 configFile = format.generate "draupnir.yaml" cfg.settings;
15
16 inherit (lib)
17 literalExpression
18 mkEnableOption
19 mkOption
20 mkPackageOption
21 mkRemovedOptionModule
22 mkRenamedOptionModule
23 types
24 ;
25in
26{
27 imports = [
28 # Removed options for those migrating from the Mjolnir module
29 (mkRenamedOptionModule
30 [ "services" "draupnir" "dataPath" ]
31 [ "services" "draupnir" "settings" "dataPath" ]
32 )
33 (mkRenamedOptionModule
34 [ "services" "draupnir" "homeserverUrl" ]
35 [ "services" "draupnir" "settings" "homeserverUrl" ]
36 )
37 (mkRenamedOptionModule
38 [ "services" "draupnir" "managementRoom" ]
39 [ "services" "draupnir" "settings" "managementRoom" ]
40 )
41 (mkRenamedOptionModule
42 [ "services" "draupnir" "accessTokenFile" ]
43 [ "services" "draupnir" "secrets" "accessToken" ]
44 )
45 (mkRemovedOptionModule [ "services" "draupnir" "pantalaimon" ] ''
46 `services.draupnir.pantalaimon.*` has been removed because it depends on the deprecated and vulnerable
47 libolm library for end-to-end encryption and upstream support for Pantalaimon in Draupnir is limited.
48 See <https://the-draupnir-project.github.io/draupnir-documentation/bot/encryption> for details.
49 If you nontheless require E2EE via Pantalaimon, you can configure `services.pantalaimon-headless.instances`
50 yourself and use that with `services.draupnir.settings.pantalaimon` and `services.draupnir.secrets.pantalaimon.password`.
51 '')
52 ];
53
54 options.services.draupnir = {
55 enable = mkEnableOption "Draupnir, a moderations bot for Matrix";
56
57 package = mkPackageOption pkgs "draupnir" { };
58
59 settings = mkOption {
60 example = literalExpression ''
61 {
62 homeserverUrl = "https://matrix.org";
63 managementRoom = "#moderators:example.org";
64
65 autojoinOnlyIfManager = true;
66 automaticallyRedactForReasons = [ "spam" "advertising" ];
67 }
68 '';
69 description = ''
70 Free-form settings written to Draupnir's configuration file.
71 See [Draupnir's default configuration](https://github.com/the-draupnir-project/Draupnir/blob/main/config/default.yaml) for available settings.
72 '';
73 default = { };
74 type = types.submodule {
75 freeformType = format.type;
76 options = {
77 homeserverUrl = mkOption {
78 type = types.str;
79 example = "https://matrix.org";
80 description = ''
81 Base URL of the Matrix homeserver that provides the Client-Server API.
82
83 ::: {.note}
84 When using Pantalaimon, set this to the Pantalaimon URL and
85 {option}`${opt.settings}.rawHomeserverUrl` to the public URL.
86 :::
87 '';
88 };
89
90 rawHomeserverUrl = mkOption {
91 type = types.str;
92 example = "https://matrix.org";
93 default = cfg.settings.homeserverUrl;
94 defaultText = literalExpression "config.${opt.settings}.homeserverUrl";
95 description = ''
96 Public base URL of the Matrix homeserver that provides the Client-Server API when using the Draupnir's
97 [Report forwarding feature](https://the-draupnir-project.github.io/draupnir-documentation/bot/homeserver-administration#report-forwarding).
98
99 ::: {.warning}
100 When using Pantalaimon, do not set this to the Pantalaimon URL!
101 :::
102 '';
103 };
104
105 managementRoom = mkOption {
106 type = types.str;
107 example = "#moderators:example.org";
108 description = ''
109 The room ID or alias where moderators can use the bot's functionality.
110
111 The bot has no access controls, so anyone in this room can use the bot - secure this room!
112 Do not enable end-to-end encryption for this room, unless set up with Pantalaimon.
113
114 ::: {.warning}
115 When using a room alias, make sure the alias used is on the local homeserver!
116 This prevents an issue where the control room becomes undefined when the alias can't be resolved.
117 :::
118 '';
119 };
120
121 dataPath = mkOption {
122 type = types.path;
123 readOnly = true;
124 default = "/var/lib/draupnir";
125 description = ''
126 The path Draupnir will store its state/data in.
127
128 ::: {.warning}
129 This option is read-only.
130 :::
131
132 ::: {.note}
133 If you want to customize where this data is stored, use a bind mount.
134 :::
135 '';
136 };
137 };
138 };
139 };
140
141 secrets = {
142 accessToken = mkOption {
143 type = types.nullOr types.path;
144 default = null;
145 description = ''
146 File containing the access token for Draupnir's Matrix account
147 to be used in place of {option}`${opt.settings}.accessToken`.
148 '';
149 };
150
151 pantalaimon.password = mkOption {
152 type = types.nullOr types.path;
153 default = null;
154 description = ''
155 File containing the password for Draupnir's Matrix account when used in
156 conjunction with Pantalaimon to be used in place of
157 {option}`${opt.settings}.pantalaimon.password`.
158
159 ::: {.warning}
160 Take note that upstream has limited Pantalaimon and E2EE support:
161 <https://the-draupnir-project.github.io/draupnir-documentation/bot/encryption> and
162 <https://the-draupnir-project.github.io/draupnir-documentation/shared/dogfood#e2ee-support>.
163 :::
164 '';
165 };
166
167 web.synapseHTTPAntispam.authorization = mkOption {
168 type = types.nullOr types.path;
169 default = null;
170 description = ''
171 File containing the secret token when using the Synapse HTTP Antispam module
172 to be used in place of
173 {option}`${opt.settings}.web.synapseHTTPAntispam.authorization`.
174
175 See <https://the-draupnir-project.github.io/draupnir-documentation/bot/synapse-http-antispam> for details.
176 '';
177 };
178 };
179 };
180
181 config = lib.mkIf cfg.enable {
182 assertions = [
183 {
184 # Removed option for those migrating from the Mjolnir module - mkRemovedOption module does *not* work with submodules.
185 assertion = !(cfg.settings ? protectedRooms);
186 message = "Unset ${opt.settings}.protectedRooms, as it is unsupported on Draupnir. Add these rooms via `!draupnir rooms add` instead.";
187 }
188 ];
189
190 systemd.services.draupnir = {
191 description = "Draupnir - a moderation bot for Matrix";
192 wants = [
193 "network-online.target"
194 "matrix-synapse.service"
195 "conduit.service"
196 "dendrite.service"
197 ];
198 after = [
199 "network-online.target"
200 "matrix-synapse.service"
201 "conduit.service"
202 "dendrite.service"
203 ];
204 wantedBy = [ "multi-user.target" ];
205
206 startLimitIntervalSec = 0;
207 serviceConfig = {
208 ExecStart = toString (
209 [
210 (lib.getExe cfg.package)
211 "--draupnir-config"
212 configFile
213 ]
214 ++ lib.optionals (cfg.secrets.accessToken != null) [
215 "--access-token-path"
216 "%d/access_token"
217 ]
218 ++ lib.optionals (cfg.secrets.pantalaimon.password != null) [
219 "--pantalaimon-password-path"
220 "%d/pantalaimon_password"
221 ]
222 ++ lib.optionals (cfg.secrets.web.synapseHTTPAntispam.authorization != null) [
223 "--http-antispam-authorization-path"
224 "%d/http_antispam_authorization"
225 ]
226 );
227
228 WorkingDirectory = "/var/lib/draupnir";
229 StateDirectory = "draupnir";
230 StateDirectoryMode = "0700";
231 ProtectHome = true;
232 PrivateDevices = true;
233 Restart = "on-failure";
234 RestartSec = "5s";
235 DynamicUser = true;
236 LoadCredential =
237 lib.optionals (cfg.secrets.accessToken != null) [
238 "access_token:${cfg.secrets.accessToken}"
239 ]
240 ++ lib.optionals (cfg.secrets.pantalaimon.password != null) [
241 "pantalaimon_password:${cfg.secrets.pantalaimon.password}"
242 ]
243 ++ lib.optionals (cfg.secrets.web.synapseHTTPAntispam.authorization != null) [
244 "http_antispam_authorization:${cfg.secrets.web.synapseHTTPAntispam.authorization}"
245 ];
246 };
247 };
248 };
249
250 meta = {
251 doc = ./draupnir.md;
252 maintainers = with lib.maintainers; [
253 RorySys
254 emilylange
255 ];
256 };
257}