at 25.11-pre 11 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8with lib; 9 10let 11 cfg = config.services.syncplay; 12 13 cmdArgs = 14 [ 15 "--port" 16 cfg.port 17 ] 18 ++ optionals (cfg.isolateRooms) [ "--isolate-rooms" ] 19 ++ optionals (!cfg.ready) [ "--disable-ready" ] 20 ++ optionals (!cfg.chat) [ "--disable-chat" ] 21 ++ optionals (cfg.salt != null) [ 22 "--salt" 23 cfg.salt 24 ] 25 ++ optionals (cfg.motdFile != null) [ 26 "--motd-file" 27 cfg.motdFile 28 ] 29 ++ optionals (cfg.roomsDBFile != null) [ 30 "--rooms-db-file" 31 cfg.roomsDBFile 32 ] 33 ++ optionals (cfg.permanentRoomsFile != null) [ 34 "--permanent-rooms-file" 35 cfg.permanentRoomsFile 36 ] 37 ++ [ 38 "--max-chat-message-length" 39 cfg.maxChatMessageLength 40 ] 41 ++ [ 42 "--max-username-length" 43 cfg.maxUsernameLength 44 ] 45 ++ optionals (cfg.statsDBFile != null) [ 46 "--stats-db-file" 47 cfg.statsDBFile 48 ] 49 ++ optionals (cfg.certDir != null) [ 50 "--tls" 51 cfg.certDir 52 ] 53 ++ optionals cfg.ipv4Only [ "--ipv4-only" ] 54 ++ optionals cfg.ipv6Only [ "--ipv6-only" ] 55 ++ optionals (cfg.interfaceIpv4 != "") [ 56 "--interface-ipv4" 57 cfg.interfaceIpv4 58 ] 59 ++ optionals (cfg.interfaceIpv6 != "") [ 60 "--interface-ipv6" 61 cfg.interfaceIpv6 62 ] 63 ++ cfg.extraArgs; 64 65 useACMEHostDir = optionalString ( 66 cfg.useACMEHost != null 67 ) config.security.acme.certs.${cfg.useACMEHost}.directory; 68in 69{ 70 imports = [ 71 (mkRemovedOptionModule [ "services" "syncplay" "user" ] 72 "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality." 73 ) 74 (mkRemovedOptionModule [ "services" "syncplay" "group" ] 75 "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality." 76 ) 77 ]; 78 79 options = { 80 services.syncplay = { 81 enable = mkOption { 82 type = types.bool; 83 default = false; 84 description = '' 85 If enabled, start the Syncplay server. 86 ''; 87 }; 88 89 port = mkOption { 90 type = types.port; 91 default = 8999; 92 description = '' 93 TCP port to bind to. 94 ''; 95 }; 96 97 passwordFile = mkOption { 98 type = types.nullOr types.path; 99 default = null; 100 description = '' 101 Path to the file that contains the server password. If 102 `null`, the server doesn't require a password. 103 ''; 104 }; 105 106 isolateRooms = mkOption { 107 type = types.bool; 108 default = false; 109 description = '' 110 Enable room isolation. 111 ''; 112 }; 113 114 ready = mkOption { 115 type = types.bool; 116 default = true; 117 description = '' 118 Check readiness of users. 119 ''; 120 }; 121 122 chat = mkOption { 123 type = types.bool; 124 default = true; 125 description = '' 126 Chat with users in the same room. 127 ''; 128 }; 129 130 salt = mkOption { 131 type = types.nullOr types.str; 132 default = null; 133 description = '' 134 Salt to allow room operator passwords generated by this server 135 instance to still work when the server is restarted. The salt will be 136 readable in the nix store and the processlist. If this is not 137 intended use `saltFile` instead. Mutually exclusive with 138 {option}`services.syncplay.saltFile`. 139 ''; 140 }; 141 142 saltFile = mkOption { 143 type = types.nullOr types.path; 144 default = null; 145 description = '' 146 Path to the file that contains the server salt. This allows room 147 operator passwords generated by this server instance to still work 148 when the server is restarted. `null`, the server doesn't load the 149 salt from a file. Mutually exclusive with 150 {option}`services.syncplay.salt`. 151 ''; 152 }; 153 154 motd = mkOption { 155 type = types.nullOr types.str; 156 default = null; 157 description = '' 158 Text to display when users join. The motd will be readable in the nix store 159 and the processlist. If this is not intended use `motdFile` instead. 160 Will be overriden by {option}`services.syncplay.motdFile`. 161 ''; 162 }; 163 164 motdFile = mkOption { 165 type = types.nullOr types.str; 166 default = if cfg.motd != null then (builtins.toFile "motd" cfg.motd) else null; 167 defaultText = literalExpression ''if services.syncplay.motd != null then (builtins.toFile "motd" services.syncplay.motd) else null''; 168 description = '' 169 Path to text to display when users join. 170 Will override {option}`services.syncplay.motd`. 171 ''; 172 }; 173 174 roomsDBFile = mkOption { 175 type = types.nullOr types.str; 176 default = null; 177 example = "rooms.db"; 178 description = '' 179 Path to SQLite database file to store room states. 180 Relative to the working directory provided by systemd. 181 ''; 182 }; 183 184 permanentRooms = mkOption { 185 type = types.listOf types.str; 186 default = [ ]; 187 description = '' 188 List of rooms that will be listed even if the room is empty. 189 Will be overriden by {option}`services.syncplay.permanentRoomsFile`. 190 ''; 191 }; 192 193 permanentRoomsFile = mkOption { 194 type = types.nullOr types.str; 195 default = 196 if cfg.permanentRooms != [ ] then 197 (builtins.toFile "perm" (builtins.concatStringsSep "\n" cfg.permanentRooms)) 198 else 199 null; 200 defaultText = literalExpression ''if services.syncplay.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" services.syncplay.permanentRooms)) else null''; 201 description = '' 202 File with list of rooms that will be listed even if the room is empty, 203 newline delimited. 204 Will override {option}`services.syncplay.permanentRooms`. 205 ''; 206 }; 207 208 maxChatMessageLength = mkOption { 209 type = types.ints.unsigned; 210 default = 150; 211 description = '' 212 Maximum number of characters in a chat message. 213 ''; 214 }; 215 216 maxUsernameLength = mkOption { 217 type = types.ints.unsigned; 218 default = 16; 219 description = '' 220 Maximum number of characters in a username. 221 ''; 222 }; 223 224 statsDBFile = mkOption { 225 type = types.nullOr types.str; 226 default = null; 227 example = "stats.db"; 228 description = '' 229 Path to SQLite database file to store stats. 230 Relative to the working directory provided by systemd. 231 ''; 232 }; 233 234 certDir = mkOption { 235 type = types.nullOr types.path; 236 default = null; 237 description = '' 238 TLS certificates directory to use for encryption. See 239 <https://github.com/Syncplay/syncplay/wiki/TLS-support>. 240 ''; 241 }; 242 243 useACMEHost = mkOption { 244 type = types.nullOr types.str; 245 default = null; 246 example = "syncplay.example.com"; 247 description = '' 248 If set, use NixOS-generated ACME certificate with the specified name for TLS. 249 250 Note that it requires {option}`security.acme` to be setup, e.g., credentials provided if using DNS-01 validation. 251 ''; 252 }; 253 254 ipv4Only = mkOption { 255 type = types.bool; 256 default = false; 257 description = '' 258 Listen only on IPv4 when strting the server. 259 ''; 260 }; 261 262 ipv6Only = mkOption { 263 type = types.bool; 264 default = false; 265 description = '' 266 Listen only on IPv6 when strting the server. 267 ''; 268 }; 269 270 interfaceIpv4 = mkOption { 271 type = types.str; 272 default = ""; 273 description = '' 274 The IP address to bind to for IPv4. Leaving it empty defaults to using all. 275 ''; 276 }; 277 278 interfaceIpv6 = mkOption { 279 type = types.str; 280 default = ""; 281 description = '' 282 The IP address to bind to for IPv6. Leaving it empty defaults to using all. 283 ''; 284 }; 285 286 extraArgs = mkOption { 287 type = types.listOf types.str; 288 default = [ ]; 289 description = '' 290 Additional arguments to be passed to the service. 291 ''; 292 }; 293 294 package = mkOption { 295 type = types.package; 296 default = pkgs.syncplay-nogui; 297 defaultText = literalExpression "pkgs.syncplay-nogui"; 298 description = '' 299 Package to use for syncplay. 300 ''; 301 }; 302 }; 303 }; 304 305 config = mkIf cfg.enable { 306 assertions = [ 307 { 308 assertion = cfg.salt == null || cfg.saltFile == null; 309 message = "services.syncplay.salt and services.syncplay.saltFile are mutually exclusive."; 310 } 311 { 312 assertion = cfg.certDir == null || cfg.useACMEHost == null; 313 message = "services.syncplay.certDir and services.syncplay.useACMEHost are mutually exclusive."; 314 } 315 { 316 assertion = !cfg.ipv4Only || !cfg.ipv6Only; 317 message = "services.syncplay.ipv4Only and services.syncplay.ipv6Only are mutually exclusive."; 318 } 319 ]; 320 321 warnings = 322 optional (cfg.interfaceIpv4 != "" && cfg.ipv6Only) 323 "You have specified services.syncplay.interfaceIpv4 but IPv4 is disabled by services.syncplay.ipv6Only." 324 ++ optional (cfg.interfaceIpv6 != "" && cfg.ipv4Only) 325 "You have specified services.syncplay.interfaceIpv6 but IPv6 is disabled by services.syncplay.ipv4Only."; 326 327 security.acme.certs = mkIf (cfg.useACMEHost != null) { 328 "${cfg.useACMEHost}".reloadServices = [ "syncplay.service" ]; 329 }; 330 331 networking.firewall.allowedTCPPorts = [ cfg.port ]; 332 systemd.services.syncplay = { 333 description = "Syncplay Service"; 334 wantedBy = [ "multi-user.target" ]; 335 wants = [ "network-online.target" ]; 336 after = [ "network-online.target" ]; 337 338 serviceConfig = { 339 DynamicUser = true; 340 StateDirectory = "syncplay"; 341 WorkingDirectory = "%S/syncplay"; 342 LoadCredential = 343 optional (cfg.passwordFile != null) "password:${cfg.passwordFile}" 344 ++ optional (cfg.saltFile != null) "salt:${cfg.saltFile}" 345 ++ optionals (cfg.useACMEHost != null) [ 346 "cert.pem:${useACMEHostDir}/cert.pem" 347 "privkey.pem:${useACMEHostDir}/key.pem" 348 "chain.pem:${useACMEHostDir}/chain.pem" 349 ]; 350 }; 351 352 script = '' 353 ${optionalString (cfg.passwordFile != null) '' 354 export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password") 355 ''} 356 ${optionalString (cfg.saltFile != null) '' 357 export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt") 358 ''} 359 exec ${cfg.package}/bin/syncplay-server ${escapeShellArgs cmdArgs} ${ 360 optionalString (cfg.useACMEHost != null) "--tls $CREDENTIALS_DIRECTORY" 361 } 362 ''; 363 }; 364 }; 365}