Commit 8119ec64 authored by Assistant's avatar Assistant
Browse files

nixos/syncplay: add missing options

Exposes all currently available command-line arguments that were
missing, including some that were impossible to use with the catch-all
option `extraArgs` alone, requiring changes to other parts of the
system.
Those are now all self-contained in the module.
The service now uses systemd's `DynamicUsers`.
parent e6c09b2d
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -368,6 +368,9 @@
- The `antennas` package and the `services.antennas` module have been
  removed as they only work with `tvheadend` (see above).

- The `services.syncplay` module now exposes all currently available command-line arguments for `syncplay-server` as options, as well as a `useACMEHost` option for easy TLS setup.
  The systemd service now uses `DynamicUser`/`StateDirectory` and the `user` and `group` options have been deprecated.

## Other Notable Changes {#sec-release-24.11-notable-changes}

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+207 −25
Original line number Diff line number Diff line
@@ -7,18 +7,41 @@ let

  cmdArgs =
    [ "--port" cfg.port ]
    ++ optionals (cfg.isolateRooms) [ "--isolate-rooms" ]
    ++ optionals (!cfg.ready) [ "--disable-ready" ]
    ++ optionals (!cfg.chat) [ "--disable-chat" ]
    ++ optionals (cfg.salt != null) [ "--salt" cfg.salt ]
    ++ optionals (cfg.motdFile != null) [ "--motd-file" cfg.motdFile ]
    ++ optionals (cfg.roomsDBFile != null) [ "--rooms-db-file" cfg.roomsDBFile ]
    ++ optionals (cfg.permanentRoomsFile != null) [ "--permanent-rooms-file" cfg.permanentRoomsFile ]
    ++ [ "--max-chat-message-length" cfg.maxChatMessageLength ]
    ++ [ "--max-username-length" cfg.maxUsernameLength ]
    ++ optionals (cfg.statsDBFile != null) [ "--stats-db-file" cfg.statsDBFile ]
    ++ optionals (cfg.certDir != null) [ "--tls" cfg.certDir ]
    ++ optionals cfg.ipv4Only [ "--ipv4-only" ]
    ++ optionals cfg.ipv6Only [ "--ipv6-only" ]
    ++ optionals (cfg.interfaceIpv4 != "") [ "--interface-ipv4" cfg.interfaceIpv4 ]
    ++ optionals (cfg.interfaceIpv6 != "") [ "--interface-ipv6" cfg.interfaceIpv6 ]
    ++ cfg.extraArgs;

  useACMEHostDir = optionalString (cfg.useACMEHost != null) config.security.acme.certs.${cfg.useACMEHost}.directory;
in
{
  imports = [
    (mkRemovedOptionModule [ "services" "syncplay" "user" ]
      "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality.")
    (mkRemovedOptionModule [ "services" "syncplay" "group" ]
      "The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality.")
  ];

  options = {
    services.syncplay = {
      enable = mkOption {
        type = types.bool;
        default = false;
        description = "If enabled, start the Syncplay server.";
        description = ''
          If enabled, start the Syncplay server.
        '';
      };

      port = mkOption {
@@ -29,6 +52,39 @@ in
        '';
      };

      passwordFile = mkOption {
        type = types.nullOr types.path;
        default = null;
        description = ''
          Path to the file that contains the server password. If
          `null`, the server doesn't require a password.
        '';
      };

      isolateRooms = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Enable room isolation.
        '';
      };

      ready = mkOption {
        type = types.bool;
        default = true;
        description = ''
          Check readiness of users.
        '';
      };

      chat = mkOption {
        type = types.bool;
        default = true;
        description = ''
          Chat with users in the same room.
        '';
      };

      salt = mkOption {
        type = types.nullOr types.str;
        default = null;
@@ -37,7 +93,7 @@ in
          instance to still work when the server is restarted.  The salt will be
          readable in the nix store and the processlist.  If this is not
          intended use `saltFile` instead.  Mutually exclusive with
          <option>services.syncplay.saltFile</option>.
          {option}`services.syncplay.saltFile`.
        '';
      };

@@ -49,7 +105,83 @@ in
          operator passwords generated by this server instance to still work
          when the server is restarted.  `null`, the server doesn't load the
          salt from a file.  Mutually exclusive with
          <option>services.syncplay.salt</option>.
          {option}`services.syncplay.salt`.
        '';
      };

      motd = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = ''
          Text to display when users join. The motd will be readable in the nix store
          and the processlist.  If this is not intended use `motdFile` instead.
          Will be overriden by {option}`services.syncplay.motdFile`.
        '';
      };

      motdFile = mkOption {
        type = types.nullOr types.str;
        default = if cfg.motd != null then (builtins.toFile "motd" cfg.motd) else null;
        defaultText = literalExpression ''if services.syncplay.motd != null then (builtins.toFile "motd" services.syncplay.motd) else null'';
        description = ''
          Path to text to display when users join.
          Will override {option}`services.syncplay.motd`.
        '';
      };

      roomsDBFile = mkOption {
        type = types.nullOr types.str;
        default = null;
        example = "rooms.db";
        description = ''
          Path to SQLite database file to store room states.
          Relative to the working directory provided by systemd.
        '';
      };

      permanentRooms = mkOption {
        type = types.listOf types.str;
        default = [ ];
        description = ''
          List of rooms that will be listed even if the room is empty.
          Will be overriden by {option}`services.syncplay.permanentRoomsFile`.
        '';
      };

      permanentRoomsFile = mkOption {
        type = types.nullOr types.str;
        default = if cfg.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" cfg.permanentRooms)) else null;
        defaultText = literalExpression ''if services.syncplay.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" services.syncplay.permanentRooms)) else null'';
        description = ''
          File with list of rooms that will be listed even if the room is empty,
          newline delimited.
          Will override {option}`services.syncplay.permanentRooms`.
        '';
      };

      maxChatMessageLength = mkOption {
        type = types.ints.unsigned;
        default = 150;
        description = ''
          Maximum number of characters in a chat message.
        '';
      };

      maxUsernameLength = mkOption {
        type = types.ints.unsigned;
        default = 16;
        description = ''
          Maximum number of characters in a username.
        '';
      };

      statsDBFile = mkOption {
        type = types.nullOr types.str;
        default = null;
        example = "stats.db";
        description = ''
          Path to SQLite database file to store stats.
          Relative to the working directory provided by systemd.
        '';
      };

@@ -62,36 +194,63 @@ in
        '';
      };

      extraArgs = mkOption {
        type = types.listOf types.str;
        default = [ ];
      useACMEHost = mkOption {
        type = types.nullOr types.str;
        default = null;
        example = "syncplay.example.com";
        description = ''
          Additional arguments to be passed to the service.
          If set, use NixOS-generated ACME certificate with the specified name for TLS.

          Note that it requires {option}`security.acme` to be setup, e.g., credentials provided if using DNS-01 validation.
        '';
      };

      ipv4Only = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Listen only on IPv4 when strting the server.
        '';
      };

      user = mkOption {
      ipv6Only = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Listen only on IPv6 when strting the server.
        '';
      };

      interfaceIpv4 = mkOption {
        type = types.str;
        default = "nobody";
        default = "";
        description = ''
          User to use when running Syncplay.
          The IP address to bind to for IPv4. Leaving it empty defaults to using all.
        '';
      };

      group = mkOption {
      interfaceIpv6 = mkOption {
        type = types.str;
        default = "nogroup";
        default = "";
        description = ''
          Group to use when running Syncplay.
          The IP address to bind to for IPv6. Leaving it empty defaults to using all.
        '';
      };

      passwordFile = mkOption {
        type = types.nullOr types.path;
        default = null;
      extraArgs = mkOption {
        type = types.listOf types.str;
        default = [ ];
        description = ''
          Path to the file that contains the server password. If
          `null`, the server doesn't require a password.
          Additional arguments to be passed to the service.
        '';
      };

      package = mkOption {
        type = types.package;
        default = pkgs.syncplay-nogui;
        defaultText = literalExpression "pkgs.syncplay-nogui";
        description = ''
          Package to use for syncplay.
        '';
      };
    };
@@ -103,7 +262,24 @@ in
        assertion = cfg.salt == null || cfg.saltFile == null;
        message = "services.syncplay.salt and services.syncplay.saltFile are mutually exclusive.";
      }
      {
        assertion = cfg.certDir == null || cfg.useACMEHost == null;
        message = "services.syncplay.certDir and services.syncplay.useACMEHost are mutually exclusive.";
      }
      {
        assertion = !cfg.ipv4Only || !cfg.ipv6Only;
        message = "services.syncplay.ipv4Only and services.syncplay.ipv6Only are mutually exclusive.";
      }
    ];

    warnings = optional (cfg.interfaceIpv4 != "" && cfg.ipv6Only) "You have specified services.syncplay.interfaceIpv4 but IPv4 is disabled by services.syncplay.ipv6Only."
      ++ optional (cfg.interfaceIpv6 != "" && cfg.ipv4Only) "You have specified services.syncplay.interfaceIpv6 but IPv6 is disabled by services.syncplay.ipv4Only.";

    security.acme.certs = mkIf (cfg.useACMEHost != null) {
      "${cfg.useACMEHost}".reloadServices = [ "syncplay.service" ];
    };

    networking.firewall.allowedTCPPorts = [ cfg.port ];
    systemd.services.syncplay = {
      description = "Syncplay Service";
      wantedBy = [ "multi-user.target" ];
@@ -111,20 +287,26 @@ in
      after = [ "network-online.target" ];

      serviceConfig = {
        User = cfg.user;
        Group = cfg.group;
        LoadCredential = lib.optional (cfg.passwordFile != null) "password:${cfg.passwordFile}"
          ++ lib.optional (cfg.saltFile != null) "salt:${cfg.saltFile}";
        DynamicUser = true;
        StateDirectory = "syncplay";
        WorkingDirectory = "%S/syncplay";
        LoadCredential = optional (cfg.passwordFile != null) "password:${cfg.passwordFile}"
          ++ optional (cfg.saltFile != null) "salt:${cfg.saltFile}"
          ++ optionals (cfg.useACMEHost != null) [
          "cert.pem:${useACMEHostDir}/cert.pem"
          "privkey.pem:${useACMEHostDir}/key.pem"
          "chain.pem:${useACMEHostDir}/chain.pem"
        ];
      };

      script = ''
        ${lib.optionalString (cfg.passwordFile != null) ''
        ${optionalString (cfg.passwordFile != null) ''
          export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password")
        ''}
        ${lib.optionalString (cfg.saltFile != null) ''
        ${optionalString (cfg.saltFile != null) ''
          export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt")
        ''}
        exec ${pkgs.syncplay-nogui}/bin/syncplay-server ${escapeShellArgs cmdArgs}
        exec ${cfg.package}/bin/syncplay-server ${escapeShellArgs cmdArgs} ${optionalString (cfg.useACMEHost != null) "--tls $CREDENTIALS_DIRECTORY"}
      '';
    };
  };