1{ config, lib, pkgs, ...}:
2
3with lib;
4
5let
6 cfg = config.services.znc;
7
8 defaultUser = "znc"; # Default user to own process.
9
10 # Default user and pass:
11 # un=znc
12 # pw=nixospass
13
14 defaultUserName = "znc";
15 defaultPassBlock = "
16 <Pass password>
17 Method = sha256
18 Hash = e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93
19 Salt = l5Xryew4g*!oa(ECfX2o
20 </Pass>
21 ";
22
23 modules = pkgs.buildEnv {
24 name = "znc-modules";
25 paths = cfg.modulePackages;
26 };
27
28 # Keep znc.conf in nix store, then symlink or copy into `dataDir`, depending on `mutable`.
29 mkZncConf = confOpts: ''
30 Version = 1.6.3
31 ${concatMapStrings (n: "LoadModule = ${n}\n") confOpts.modules}
32
33 <Listener l>
34 Port = ${toString confOpts.port}
35 IPv4 = true
36 IPv6 = true
37 SSL = ${boolToString confOpts.useSSL}
38 ${lib.optionalString (confOpts.uriPrefix != null) "URIPrefix = ${confOpts.uriPrefix}"}
39 </Listener>
40
41 <User ${confOpts.userName}>
42 ${confOpts.passBlock}
43 Admin = true
44 Nick = ${confOpts.nick}
45 AltNick = ${confOpts.nick}_
46 Ident = ${confOpts.nick}
47 RealName = ${confOpts.nick}
48 ${concatMapStrings (n: "LoadModule = ${n}\n") confOpts.userModules}
49
50 ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (name: net: ''
51 <Network ${name}>
52 ${concatMapStrings (m: "LoadModule = ${m}\n") net.modules}
53 Server = ${net.server} ${lib.optionalString net.useSSL "+"}${toString net.port} ${net.password}
54 ${concatMapStrings (c: "<Chan #${c}>\n</Chan>\n") net.channels}
55 ${lib.optionalString net.hasBitlbeeControlChannel ''
56 <Chan &bitlbee>
57 </Chan>
58 ''}
59 ${net.extraConf}
60 </Network>
61 '') confOpts.networks) }
62 </User>
63 ${confOpts.extraZncConf}
64 '';
65
66 zncConfFile = pkgs.writeTextFile {
67 name = "znc.conf";
68 text = if cfg.zncConf != ""
69 then cfg.zncConf
70 else mkZncConf cfg.confOptions;
71 };
72
73 networkOpts = { ... }: {
74 options = {
75 server = mkOption {
76 type = types.str;
77 example = "chat.freenode.net";
78 description = ''
79 IRC server address.
80 '';
81 };
82
83 port = mkOption {
84 type = types.int;
85 default = 6697;
86 example = 6697;
87 description = ''
88 IRC server port.
89 '';
90 };
91
92 userName = mkOption {
93 default = "";
94 example = "johntron";
95 type = types.string;
96 description = ''
97 A nick identity specific to the IRC server.
98 '';
99 };
100
101 password = mkOption {
102 type = types.str;
103 default = "";
104 description = ''
105 IRC server password, such as for a Slack gateway.
106 '';
107 };
108
109 useSSL = mkOption {
110 type = types.bool;
111 default = true;
112 description = ''
113 Whether to use SSL to connect to the IRC server.
114 '';
115 };
116
117 modulePackages = mkOption {
118 type = types.listOf types.package;
119 default = [];
120 example = [ "pkgs.zncModules.push" "pkgs.zncModules.fish" ];
121 description = ''
122 External ZNC modules to build.
123 '';
124 };
125
126 modules = mkOption {
127 type = types.listOf types.str;
128 default = [ "simple_away" ];
129 example = literalExample "[ simple_away sasl ]";
130 description = ''
131 ZNC modules to load.
132 '';
133 };
134
135 channels = mkOption {
136 type = types.listOf types.str;
137 default = [];
138 example = [ "nixos" ];
139 description = ''
140 IRC channels to join.
141 '';
142 };
143
144 hasBitlbeeControlChannel = mkOption {
145 type = types.bool;
146 default = false;
147 description = ''
148 Whether to add the special Bitlbee operations channel.
149 '';
150 };
151
152 extraConf = mkOption {
153 default = "";
154 type = types.lines;
155 example = ''
156 Encoding = ^UTF-8
157 FloodBurst = 4
158 FloodRate = 1.00
159 IRCConnectEnabled = true
160 Ident = johntron
161 JoinDelay = 0
162 Nick = johntron
163 '';
164 description = ''
165 Extra config for the network.
166 '';
167 };
168 };
169 };
170
171in
172
173{
174
175 ###### Interface
176
177 options = {
178 services.znc = {
179 enable = mkOption {
180 default = false;
181 type = types.bool;
182 description = ''
183 Enable a ZNC service for a user.
184 '';
185 };
186
187 user = mkOption {
188 default = "znc";
189 example = "john";
190 type = types.string;
191 description = ''
192 The name of an existing user account to use to own the ZNC server process.
193 If not specified, a default user will be created to own the process.
194 '';
195 };
196
197 group = mkOption {
198 default = "";
199 example = "users";
200 type = types.string;
201 description = ''
202 Group to own the ZNCserver process.
203 '';
204 };
205
206 dataDir = mkOption {
207 default = "/var/lib/znc/";
208 example = "/home/john/.znc/";
209 type = types.path;
210 description = ''
211 The data directory. Used for configuration files and modules.
212 '';
213 };
214
215 openFirewall = mkOption {
216 type = types.bool;
217 default = false;
218 description = ''
219 Whether to open ports in the firewall for ZNC.
220 '';
221 };
222
223 zncConf = mkOption {
224 default = "";
225 example = "See: http://wiki.znc.in/Configuration";
226 type = types.lines;
227 description = ''
228 Config file as generated with `znc --makeconf` to use for the whole ZNC configuration.
229 If specified, `confOptions` will be ignored, and this value, as-is, will be used.
230 If left empty, a conf file with default values will be used.
231 '';
232 };
233
234 confOptions = {
235 modules = mkOption {
236 type = types.listOf types.str;
237 default = [ "webadmin" "adminlog" ];
238 example = [ "partyline" "webadmin" "adminlog" "log" ];
239 description = ''
240 A list of modules to include in the `znc.conf` file.
241 '';
242 };
243
244 userModules = mkOption {
245 type = types.listOf types.str;
246 default = [ "chansaver" "controlpanel" ];
247 example = [ "chansaver" "controlpanel" "fish" "push" ];
248 description = ''
249 A list of user modules to include in the `znc.conf` file.
250 '';
251 };
252
253 userName = mkOption {
254 default = defaultUserName;
255 example = "johntron";
256 type = types.string;
257 description = ''
258 The user name used to log in to the ZNC web admin interface.
259 '';
260 };
261
262 networks = mkOption {
263 default = { };
264 type = with types; attrsOf (submodule networkOpts);
265 description = ''
266 IRC networks to connect the user to.
267 '';
268 example = {
269 "freenode" = {
270 server = "chat.freenode.net";
271 port = 6697;
272 useSSL = true;
273 modules = [ "simple_away" ];
274 };
275 };
276 };
277
278 nick = mkOption {
279 default = "znc-user";
280 example = "john";
281 type = types.string;
282 description = ''
283 The IRC nick.
284 '';
285 };
286
287 passBlock = mkOption {
288 example = defaultPassBlock;
289 type = types.string;
290 description = ''
291 Generate with `nix-shell -p znc --command "znc --makepass"`.
292 This is the password used to log in to the ZNC web admin interface.
293 '';
294 };
295
296 port = mkOption {
297 default = 5000;
298 example = 5000;
299 type = types.int;
300 description = ''
301 Specifies the port on which to listen.
302 '';
303 };
304
305 useSSL = mkOption {
306 default = true;
307 type = types.bool;
308 description = ''
309 Indicates whether the ZNC server should use SSL when listening on the specified port. A self-signed certificate will be generated.
310 '';
311 };
312
313 uriPrefix = mkOption {
314 type = types.nullOr types.str;
315 default = null;
316 example = "/znc/";
317 description = ''
318 An optional URI prefix for the ZNC web interface. Can be
319 used to make ZNC available behind a reverse proxy.
320 '';
321 };
322
323 extraZncConf = mkOption {
324 default = "";
325 type = types.lines;
326 description = ''
327 Extra config to `znc.conf` file.
328 '';
329 };
330 };
331
332 modulePackages = mkOption {
333 type = types.listOf types.package;
334 default = [ ];
335 example = literalExample "[ pkgs.zncModules.fish pkgs.zncModules.push ]";
336 description = ''
337 A list of global znc module packages to add to znc.
338 '';
339 };
340
341 mutable = mkOption {
342 default = true;
343 type = types.bool;
344 description = ''
345 Indicates whether to allow the contents of the `dataDir` directory to be changed
346 by the user at run-time.
347 If true, modifications to the ZNC configuration after its initial creation are not
348 overwritten by a NixOS system rebuild.
349 If false, the ZNC configuration is rebuilt by every system rebuild.
350 If the user wants to manage the ZNC service using the web admin interface, this value
351 should be set to true.
352 '';
353 };
354
355 extraFlags = mkOption {
356 default = [ ];
357 example = [ "--debug" ];
358 type = types.listOf types.str;
359 description = ''
360 Extra flags to use when executing znc command.
361 '';
362 };
363 };
364 };
365
366
367 ###### Implementation
368
369 config = mkIf cfg.enable {
370
371 networking.firewall = mkIf cfg.openFirewall {
372 allowedTCPPorts = [ cfg.confOptions.port ];
373 };
374
375 systemd.services.znc = {
376 description = "ZNC Server";
377 wantedBy = [ "multi-user.target" ];
378 after = [ "network.service" ];
379 serviceConfig = {
380 User = cfg.user;
381 Group = cfg.group;
382 Restart = "always";
383 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
384 ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
385 };
386 preStart = ''
387 ${pkgs.coreutils}/bin/mkdir -p ${cfg.dataDir}/configs
388
389 # If mutable, regenerate conf file every time.
390 ${optionalString (!cfg.mutable) ''
391 ${pkgs.coreutils}/bin/echo "znc is set to be system-managed. Now deleting old znc.conf file to be regenerated."
392 ${pkgs.coreutils}/bin/rm -f ${cfg.dataDir}/configs/znc.conf
393 ''}
394
395 # Ensure essential files exist.
396 if [[ ! -f ${cfg.dataDir}/configs/znc.conf ]]; then
397 ${pkgs.coreutils}/bin/echo "No znc.conf file found in ${cfg.dataDir}. Creating one now."
398 ${pkgs.coreutils}/bin/cp --no-clobber ${zncConfFile} ${cfg.dataDir}/configs/znc.conf
399 ${pkgs.coreutils}/bin/chmod u+rw ${cfg.dataDir}/configs/znc.conf
400 ${pkgs.coreutils}/bin/chown ${cfg.user} ${cfg.dataDir}/configs/znc.conf
401 fi
402
403 if [[ ! -f ${cfg.dataDir}/znc.pem ]]; then
404 ${pkgs.coreutils}/bin/echo "No znc.pem file found in ${cfg.dataDir}. Creating one now."
405 ${pkgs.znc}/bin/znc --makepem --datadir ${cfg.dataDir}
406 fi
407
408 # Symlink modules
409 rm ${cfg.dataDir}/modules || true
410 ln -fs ${modules}/lib/znc ${cfg.dataDir}/modules
411 '';
412 script = "${pkgs.znc}/bin/znc --foreground --datadir ${cfg.dataDir} ${toString cfg.extraFlags}";
413 };
414
415 users.users = optional (cfg.user == defaultUser)
416 { name = defaultUser;
417 description = "ZNC server daemon owner";
418 group = defaultUser;
419 uid = config.ids.uids.znc;
420 home = cfg.dataDir;
421 createHome = true;
422 };
423
424 users.groups = optional (cfg.user == defaultUser)
425 { name = defaultUser;
426 gid = config.ids.gids.znc;
427 members = [ defaultUser ];
428 };
429
430 };
431}