1 { config, lib, pkgs, ... }:
6 cfg = config.services.syncplay;
10 ++ optionals (cfg.isolateRooms) [ "--isolate-rooms" ]
11 ++ optionals (!cfg.ready) [ "--disable-ready" ]
12 ++ optionals (!cfg.chat) [ "--disable-chat" ]
13 ++ optionals (cfg.salt != null) [ "--salt" cfg.salt ]
14 ++ optionals (cfg.motdFile != null) [ "--motd-file" cfg.motdFile ]
15 ++ optionals (cfg.roomsDBFile != null) [ "--rooms-db-file" cfg.roomsDBFile ]
16 ++ optionals (cfg.permanentRoomsFile != null) [ "--permanent-rooms-file" cfg.permanentRoomsFile ]
17 ++ [ "--max-chat-message-length" cfg.maxChatMessageLength ]
18 ++ [ "--max-username-length" cfg.maxUsernameLength ]
19 ++ optionals (cfg.statsDBFile != null) [ "--stats-db-file" cfg.statsDBFile ]
20 ++ optionals (cfg.certDir != null) [ "--tls" cfg.certDir ]
21 ++ optionals cfg.ipv4Only [ "--ipv4-only" ]
22 ++ optionals cfg.ipv6Only [ "--ipv6-only" ]
23 ++ optionals (cfg.interfaceIpv4 != "") [ "--interface-ipv4" cfg.interfaceIpv4 ]
24 ++ optionals (cfg.interfaceIpv6 != "") [ "--interface-ipv6" cfg.interfaceIpv6 ]
27 useACMEHostDir = optionalString (cfg.useACMEHost != null) config.security.acme.certs.${cfg.useACMEHost}.directory;
31 (mkRemovedOptionModule [ "services" "syncplay" "user" ]
32 "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality.")
33 (mkRemovedOptionModule [ "services" "syncplay" "group" ]
34 "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality.")
43 If enabled, start the Syncplay server.
55 passwordFile = mkOption {
56 type = types.nullOr types.path;
59 Path to the file that contains the server password. If
60 `null`, the server doesn't require a password.
64 isolateRooms = mkOption {
68 Enable room isolation.
76 Check readiness of users.
84 Chat with users in the same room.
89 type = types.nullOr types.str;
92 Salt to allow room operator passwords generated by this server
93 instance to still work when the server is restarted. The salt will be
94 readable in the nix store and the processlist. If this is not
95 intended use `saltFile` instead. Mutually exclusive with
96 {option}`services.syncplay.saltFile`.
100 saltFile = mkOption {
101 type = types.nullOr types.path;
104 Path to the file that contains the server salt. This allows room
105 operator passwords generated by this server instance to still work
106 when the server is restarted. `null`, the server doesn't load the
107 salt from a file. Mutually exclusive with
108 {option}`services.syncplay.salt`.
113 type = types.nullOr types.str;
116 Text to display when users join. The motd will be readable in the nix store
117 and the processlist. If this is not intended use `motdFile` instead.
118 Will be overriden by {option}`services.syncplay.motdFile`.
122 motdFile = mkOption {
123 type = types.nullOr types.str;
124 default = if cfg.motd != null then (builtins.toFile "motd" cfg.motd) else null;
125 defaultText = literalExpression ''if services.syncplay.motd != null then (builtins.toFile "motd" services.syncplay.motd) else null'';
127 Path to text to display when users join.
128 Will override {option}`services.syncplay.motd`.
132 roomsDBFile = mkOption {
133 type = types.nullOr types.str;
135 example = "rooms.db";
137 Path to SQLite database file to store room states.
138 Relative to the working directory provided by systemd.
142 permanentRooms = mkOption {
143 type = types.listOf types.str;
146 List of rooms that will be listed even if the room is empty.
147 Will be overriden by {option}`services.syncplay.permanentRoomsFile`.
151 permanentRoomsFile = mkOption {
152 type = types.nullOr types.str;
153 default = if cfg.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" cfg.permanentRooms)) else null;
154 defaultText = literalExpression ''if services.syncplay.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" services.syncplay.permanentRooms)) else null'';
156 File with list of rooms that will be listed even if the room is empty,
158 Will override {option}`services.syncplay.permanentRooms`.
162 maxChatMessageLength = mkOption {
163 type = types.ints.unsigned;
166 Maximum number of characters in a chat message.
170 maxUsernameLength = mkOption {
171 type = types.ints.unsigned;
174 Maximum number of characters in a username.
178 statsDBFile = mkOption {
179 type = types.nullOr types.str;
181 example = "stats.db";
183 Path to SQLite database file to store stats.
184 Relative to the working directory provided by systemd.
189 type = types.nullOr types.path;
192 TLS certificates directory to use for encryption. See
193 <https://github.com/Syncplay/syncplay/wiki/TLS-support>.
197 useACMEHost = mkOption {
198 type = types.nullOr types.str;
200 example = "syncplay.example.com";
202 If set, use NixOS-generated ACME certificate with the specified name for TLS.
204 Note that it requires {option}`security.acme` to be setup, e.g., credentials provided if using DNS-01 validation.
208 ipv4Only = mkOption {
212 Listen only on IPv4 when strting the server.
216 ipv6Only = mkOption {
220 Listen only on IPv6 when strting the server.
224 interfaceIpv4 = mkOption {
228 The IP address to bind to for IPv4. Leaving it empty defaults to using all.
232 interfaceIpv6 = mkOption {
236 The IP address to bind to for IPv6. Leaving it empty defaults to using all.
240 extraArgs = mkOption {
241 type = types.listOf types.str;
244 Additional arguments to be passed to the service.
249 type = types.package;
250 default = pkgs.syncplay-nogui;
251 defaultText = literalExpression "pkgs.syncplay-nogui";
253 Package to use for syncplay.
259 config = mkIf cfg.enable {
262 assertion = cfg.salt == null || cfg.saltFile == null;
263 message = "services.syncplay.salt and services.syncplay.saltFile are mutually exclusive.";
266 assertion = cfg.certDir == null || cfg.useACMEHost == null;
267 message = "services.syncplay.certDir and services.syncplay.useACMEHost are mutually exclusive.";
270 assertion = !cfg.ipv4Only || !cfg.ipv6Only;
271 message = "services.syncplay.ipv4Only and services.syncplay.ipv6Only are mutually exclusive.";
275 warnings = optional (cfg.interfaceIpv4 != "" && cfg.ipv6Only) "You have specified services.syncplay.interfaceIpv4 but IPv4 is disabled by services.syncplay.ipv6Only."
276 ++ optional (cfg.interfaceIpv6 != "" && cfg.ipv4Only) "You have specified services.syncplay.interfaceIpv6 but IPv6 is disabled by services.syncplay.ipv4Only.";
278 security.acme.certs = mkIf (cfg.useACMEHost != null) {
279 "${cfg.useACMEHost}".reloadServices = [ "syncplay.service" ];
282 networking.firewall.allowedTCPPorts = [ cfg.port ];
283 systemd.services.syncplay = {
284 description = "Syncplay Service";
285 wantedBy = [ "multi-user.target" ];
286 wants = [ "network-online.target" ];
287 after = [ "network-online.target" ];
291 StateDirectory = "syncplay";
292 WorkingDirectory = "%S/syncplay";
293 LoadCredential = optional (cfg.passwordFile != null) "password:${cfg.passwordFile}"
294 ++ optional (cfg.saltFile != null) "salt:${cfg.saltFile}"
295 ++ optionals (cfg.useACMEHost != null) [
296 "cert.pem:${useACMEHostDir}/cert.pem"
297 "privkey.pem:${useACMEHostDir}/key.pem"
298 "chain.pem:${useACMEHostDir}/chain.pem"
303 ${optionalString (cfg.passwordFile != null) ''
304 export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password")
306 ${optionalString (cfg.saltFile != null) ''
307 export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt")
309 exec ${cfg.package}/bin/syncplay-server ${escapeShellArgs cmdArgs} ${optionalString (cfg.useACMEHost != null) "--tls $CREDENTIALS_DIRECTORY"}