1036 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			1036 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
{ config, options, lib, pkgs, utils, ... }:
 | 
						|
 | 
						|
let
 | 
						|
  json = pkgs.formats.json {};
 | 
						|
 | 
						|
  cfg = config.services.discourse;
 | 
						|
 | 
						|
  postgresqlPackage = if config.services.postgresql.enable then
 | 
						|
                        config.services.postgresql.package
 | 
						|
                      else
 | 
						|
                        pkgs.postgresql;
 | 
						|
 | 
						|
  # We only want to create a database if we're actually going to connect to it.
 | 
						|
  databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == null;
 | 
						|
 | 
						|
  tlsEnabled = (cfg.enableACME
 | 
						|
                || cfg.sslCertificate != null
 | 
						|
                || cfg.sslCertificateKey != null);
 | 
						|
in
 | 
						|
{
 | 
						|
  options = {
 | 
						|
    services.discourse = {
 | 
						|
      enable = lib.mkEnableOption "Discourse, an open source discussion platform";
 | 
						|
 | 
						|
      package = lib.mkOption {
 | 
						|
        type = lib.types.package;
 | 
						|
        default = pkgs.discourse;
 | 
						|
        defaultText = "pkgs.discourse";
 | 
						|
        description = ''
 | 
						|
          The discourse package to use.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      hostname = lib.mkOption {
 | 
						|
        type = lib.types.str;
 | 
						|
        default = if config.networking.domain != null then
 | 
						|
                    config.networking.fqdn
 | 
						|
                  else
 | 
						|
                    config.networking.hostName;
 | 
						|
        defaultText = "config.networking.fqdn";
 | 
						|
        example = "discourse.example.com";
 | 
						|
        description = ''
 | 
						|
          The hostname to serve Discourse on.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      secretKeyBaseFile = lib.mkOption {
 | 
						|
        type = with lib.types; nullOr path;
 | 
						|
        default = null;
 | 
						|
        example = "/run/keys/secret_key_base";
 | 
						|
        description = ''
 | 
						|
          The path to a file containing the
 | 
						|
          <literal>secret_key_base</literal> secret.
 | 
						|
 | 
						|
          Discourse uses <literal>secret_key_base</literal> to encrypt
 | 
						|
          the cookie store, which contains session data, and to digest
 | 
						|
          user auth tokens.
 | 
						|
 | 
						|
          Needs to be a 64 byte long string of hexadecimal
 | 
						|
          characters. You can generate one by running
 | 
						|
 | 
						|
          <screen>
 | 
						|
          <prompt>$ </prompt>openssl rand -hex 64 >/path/to/secret_key_base_file
 | 
						|
          </screen>
 | 
						|
 | 
						|
          This should be a string, not a nix path, since nix paths are
 | 
						|
          copied into the world-readable nix store.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      sslCertificate = lib.mkOption {
 | 
						|
        type = with lib.types; nullOr path;
 | 
						|
        default = null;
 | 
						|
        example = "/run/keys/ssl.cert";
 | 
						|
        description = ''
 | 
						|
          The path to the server SSL certificate. Set this to enable
 | 
						|
          SSL.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      sslCertificateKey = lib.mkOption {
 | 
						|
        type = with lib.types; nullOr path;
 | 
						|
        default = null;
 | 
						|
        example = "/run/keys/ssl.key";
 | 
						|
        description = ''
 | 
						|
          The path to the server SSL certificate key. Set this to
 | 
						|
          enable SSL.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      enableACME = lib.mkOption {
 | 
						|
        type = lib.types.bool;
 | 
						|
        default = cfg.sslCertificate == null && cfg.sslCertificateKey == null;
 | 
						|
        defaultText = "true, unless services.discourse.sslCertificate and services.discourse.sslCertificateKey are set.";
 | 
						|
        description = ''
 | 
						|
          Whether an ACME certificate should be used to secure
 | 
						|
          connections to the server.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      backendSettings = lib.mkOption {
 | 
						|
        type = with lib.types; attrsOf (nullOr (oneOf [ str int bool float ]));
 | 
						|
        default = {};
 | 
						|
        example = lib.literalExample ''
 | 
						|
          {
 | 
						|
            max_reqs_per_ip_per_minute = 300;
 | 
						|
            max_reqs_per_ip_per_10_seconds = 60;
 | 
						|
            max_asset_reqs_per_ip_per_10_seconds = 250;
 | 
						|
            max_reqs_per_ip_mode = "warn+block";
 | 
						|
          };
 | 
						|
        '';
 | 
						|
        description = ''
 | 
						|
          Additional settings to put in the
 | 
						|
          <filename>discourse.conf</filename> file.
 | 
						|
 | 
						|
          Look in the
 | 
						|
          <link xlink:href="https://github.com/discourse/discourse/blob/master/config/discourse_defaults.conf">discourse_defaults.conf</link>
 | 
						|
          file in the upstream distribution to find available options.
 | 
						|
 | 
						|
          Setting an option to <literal>null</literal> means
 | 
						|
          <quote>define variable, but leave right-hand side
 | 
						|
          empty</quote>.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      siteSettings = lib.mkOption {
 | 
						|
        type = json.type;
 | 
						|
        default = {};
 | 
						|
        example = lib.literalExample ''
 | 
						|
          {
 | 
						|
            required = {
 | 
						|
              title = "My Cats";
 | 
						|
              site_description = "Discuss My Cats (and be nice plz)";
 | 
						|
            };
 | 
						|
            login = {
 | 
						|
              enable_github_logins = true;
 | 
						|
              github_client_id = "a2f6dfe838cb3206ce20";
 | 
						|
              github_client_secret._secret = /run/keys/discourse_github_client_secret;
 | 
						|
            };
 | 
						|
          };
 | 
						|
        '';
 | 
						|
        description = ''
 | 
						|
          Discourse site settings. These are the settings that can be
 | 
						|
          changed from the UI. This only defines their default values:
 | 
						|
          they can still be overridden from the UI.
 | 
						|
 | 
						|
          Available settings can be found by looking in the
 | 
						|
          <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">site_settings.yml</link>
 | 
						|
          file of the upstream distribution. To find a setting's path,
 | 
						|
          you only need to care about the first two levels; i.e. its
 | 
						|
          category and name. See the example.
 | 
						|
 | 
						|
          Settings containing secret data should be set to an
 | 
						|
          attribute set containing the attribute
 | 
						|
          <literal>_secret</literal> - a string pointing to a file
 | 
						|
          containing the value the option should be set to. See the
 | 
						|
          example to get a better picture of this: in the resulting
 | 
						|
          <filename>config/nixos_site_settings.json</filename> file,
 | 
						|
          the <literal>login.github_client_secret</literal> key will
 | 
						|
          be set to the contents of the
 | 
						|
          <filename>/run/keys/discourse_github_client_secret</filename>
 | 
						|
          file.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      admin = {
 | 
						|
        email = lib.mkOption {
 | 
						|
          type = lib.types.str;
 | 
						|
          example = "admin@example.com";
 | 
						|
          description = ''
 | 
						|
            The admin user email address.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        username = lib.mkOption {
 | 
						|
          type = lib.types.str;
 | 
						|
          example = "admin";
 | 
						|
          description = ''
 | 
						|
            The admin user username.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        fullName = lib.mkOption {
 | 
						|
          type = lib.types.str;
 | 
						|
          description = ''
 | 
						|
            The admin user's full name.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        passwordFile = lib.mkOption {
 | 
						|
          type = lib.types.path;
 | 
						|
          description = ''
 | 
						|
            A path to a file containing the admin user's password.
 | 
						|
 | 
						|
            This should be a string, not a nix path, since nix paths are
 | 
						|
            copied into the world-readable nix store.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
      };
 | 
						|
 | 
						|
      nginx.enable = lib.mkOption {
 | 
						|
        type = lib.types.bool;
 | 
						|
        default = true;
 | 
						|
        description = ''
 | 
						|
          Whether an <literal>nginx</literal> virtual host should be
 | 
						|
          set up to serve Discourse. Only disable if you're planning
 | 
						|
          to use a different web server, which is not recommended.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      database = {
 | 
						|
        pool = lib.mkOption {
 | 
						|
          type = lib.types.int;
 | 
						|
          default = 8;
 | 
						|
          description = ''
 | 
						|
            Database connection pool size.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        host = lib.mkOption {
 | 
						|
          type = with lib.types; nullOr str;
 | 
						|
          default = null;
 | 
						|
          description = ''
 | 
						|
            Discourse database hostname. <literal>null</literal> means <quote>prefer
 | 
						|
            local unix socket connection</quote>.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        passwordFile = lib.mkOption {
 | 
						|
          type = with lib.types; nullOr path;
 | 
						|
          default = null;
 | 
						|
          description = ''
 | 
						|
            File containing the Discourse database user password.
 | 
						|
 | 
						|
            This should be a string, not a nix path, since nix paths are
 | 
						|
            copied into the world-readable nix store.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        createLocally = lib.mkOption {
 | 
						|
          type = lib.types.bool;
 | 
						|
          default = true;
 | 
						|
          description = ''
 | 
						|
            Whether a database should be automatically created on the
 | 
						|
            local host. Set this to <literal>false</literal> if you plan
 | 
						|
            on provisioning a local database yourself. This has no effect
 | 
						|
            if <option>services.discourse.database.host</option> is customized.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        name = lib.mkOption {
 | 
						|
          type = lib.types.str;
 | 
						|
          default = "discourse";
 | 
						|
          description = ''
 | 
						|
            Discourse database name.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        username = lib.mkOption {
 | 
						|
          type = lib.types.str;
 | 
						|
          default = "discourse";
 | 
						|
          description = ''
 | 
						|
            Discourse database user.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
      };
 | 
						|
 | 
						|
      redis = {
 | 
						|
        host = lib.mkOption {
 | 
						|
          type = lib.types.str;
 | 
						|
          default = "localhost";
 | 
						|
          description = ''
 | 
						|
            Redis server hostname.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        passwordFile = lib.mkOption {
 | 
						|
          type = with lib.types; nullOr path;
 | 
						|
          default = null;
 | 
						|
          description = ''
 | 
						|
            File containing the Redis password.
 | 
						|
 | 
						|
            This should be a string, not a nix path, since nix paths are
 | 
						|
            copied into the world-readable nix store.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        dbNumber = lib.mkOption {
 | 
						|
          type = lib.types.int;
 | 
						|
          default = 0;
 | 
						|
          description = ''
 | 
						|
            Redis database number.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        useSSL = lib.mkOption {
 | 
						|
          type = lib.types.bool;
 | 
						|
          default = cfg.redis.host != "localhost";
 | 
						|
          description = ''
 | 
						|
            Connect to Redis with SSL.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
      };
 | 
						|
 | 
						|
      mail = {
 | 
						|
        notificationEmailAddress = lib.mkOption {
 | 
						|
          type = lib.types.str;
 | 
						|
          default = "${if cfg.mail.incoming.enable then "notifications" else "noreply"}@${cfg.hostname}";
 | 
						|
          defaultText = ''
 | 
						|
            "notifications@`config.services.discourse.hostname`" if
 | 
						|
            config.services.discourse.mail.incoming.enable is "true",
 | 
						|
            otherwise "noreply`config.services.discourse.hostname`"
 | 
						|
          '';
 | 
						|
          description = ''
 | 
						|
            The <literal>from:</literal> email address used when
 | 
						|
            sending all essential system emails. The domain specified
 | 
						|
            here must have SPF, DKIM and reverse PTR records set
 | 
						|
            correctly for email to arrive.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        contactEmailAddress = lib.mkOption {
 | 
						|
          type = lib.types.str;
 | 
						|
          default = "";
 | 
						|
          description = ''
 | 
						|
            Email address of key contact responsible for this
 | 
						|
            site. Used for critical notifications, as well as on the
 | 
						|
            <literal>/about</literal> contact form for urgent matters.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        outgoing = {
 | 
						|
          serverAddress = lib.mkOption {
 | 
						|
            type = lib.types.str;
 | 
						|
            default = "localhost";
 | 
						|
            description = ''
 | 
						|
              The address of the SMTP server Discourse should use to
 | 
						|
              send email.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          port = lib.mkOption {
 | 
						|
            type = lib.types.int;
 | 
						|
            default = 25;
 | 
						|
            description = ''
 | 
						|
              The port of the SMTP server Discourse should use to
 | 
						|
              send email.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          username = lib.mkOption {
 | 
						|
            type = with lib.types; nullOr str;
 | 
						|
            default = null;
 | 
						|
            description = ''
 | 
						|
              The username of the SMTP server.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          passwordFile = lib.mkOption {
 | 
						|
            type = lib.types.nullOr lib.types.path;
 | 
						|
            default = null;
 | 
						|
            description = ''
 | 
						|
              A file containing the password of the SMTP server account.
 | 
						|
 | 
						|
              This should be a string, not a nix path, since nix paths
 | 
						|
              are copied into the world-readable nix store.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          domain = lib.mkOption {
 | 
						|
            type = lib.types.str;
 | 
						|
            default = cfg.hostname;
 | 
						|
            description = ''
 | 
						|
              HELO domain to use for outgoing mail.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          authentication = lib.mkOption {
 | 
						|
            type = with lib.types; nullOr (enum ["plain" "login" "cram_md5"]);
 | 
						|
            default = null;
 | 
						|
            description = ''
 | 
						|
              Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          enableStartTLSAuto = lib.mkOption {
 | 
						|
            type = lib.types.bool;
 | 
						|
            default = true;
 | 
						|
            description = ''
 | 
						|
              Whether to try to use StartTLS.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          opensslVerifyMode = lib.mkOption {
 | 
						|
            type = lib.types.str;
 | 
						|
            default = "peer";
 | 
						|
            description = ''
 | 
						|
              How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html
 | 
						|
            '';
 | 
						|
          };
 | 
						|
        };
 | 
						|
 | 
						|
        incoming = {
 | 
						|
          enable = lib.mkOption {
 | 
						|
            type = lib.types.bool;
 | 
						|
            default = false;
 | 
						|
            description = ''
 | 
						|
              Whether to set up Postfix to receive incoming mail.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          replyEmailAddress = lib.mkOption {
 | 
						|
            type = lib.types.str;
 | 
						|
            default = "%{reply_key}@${cfg.hostname}";
 | 
						|
            defaultText = "%{reply_key}@`config.services.discourse.hostname`";
 | 
						|
            description = ''
 | 
						|
              Template for reply by email incoming email address, for
 | 
						|
              example: %{reply_key}@reply.example.com or
 | 
						|
              replies+%{reply_key}@example.com
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          mailReceiverPackage = lib.mkOption {
 | 
						|
            type = lib.types.package;
 | 
						|
            default = pkgs.discourse-mail-receiver;
 | 
						|
            defaultText = "pkgs.discourse-mail-receiver";
 | 
						|
            description = ''
 | 
						|
              The discourse-mail-receiver package to use.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
 | 
						|
          apiKeyFile = lib.mkOption {
 | 
						|
            type = lib.types.nullOr lib.types.path;
 | 
						|
            default = null;
 | 
						|
            description = ''
 | 
						|
              A file containing the Discourse API key used to add
 | 
						|
              posts and messages from mail. If left at its default
 | 
						|
              value <literal>null</literal>, one will be automatically
 | 
						|
              generated.
 | 
						|
 | 
						|
              This should be a string, not a nix path, since nix paths
 | 
						|
              are copied into the world-readable nix store.
 | 
						|
            '';
 | 
						|
          };
 | 
						|
        };
 | 
						|
      };
 | 
						|
 | 
						|
      plugins = lib.mkOption {
 | 
						|
        type = lib.types.listOf lib.types.package;
 | 
						|
        default = [];
 | 
						|
        example = ''
 | 
						|
          [
 | 
						|
            (pkgs.fetchFromGitHub {
 | 
						|
              owner = "discourse";
 | 
						|
              repo = "discourse-spoiler-alert";
 | 
						|
              rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
 | 
						|
              sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
 | 
						|
            })
 | 
						|
          ];
 | 
						|
        '';
 | 
						|
        description = ''
 | 
						|
          <productname>Discourse</productname> plugins to install as a
 | 
						|
          list of derivations. As long as a plugin supports the
 | 
						|
          standard install method, packaging it should only require
 | 
						|
          fetching its source with an appropriate fetcher.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      sidekiqProcesses = lib.mkOption {
 | 
						|
        type = lib.types.int;
 | 
						|
        default = 1;
 | 
						|
        description = ''
 | 
						|
          How many Sidekiq processes should be spawned.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      unicornTimeout = lib.mkOption {
 | 
						|
        type = lib.types.int;
 | 
						|
        default = 30;
 | 
						|
        description = ''
 | 
						|
          Time in seconds before a request to Unicorn times out.
 | 
						|
 | 
						|
          This can be raised if the system Discourse is running on is
 | 
						|
          too slow to handle many requests within 30 seconds.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
    };
 | 
						|
  };
 | 
						|
 | 
						|
  config = lib.mkIf cfg.enable {
 | 
						|
    assertions = [
 | 
						|
      {
 | 
						|
        assertion = (cfg.database.host != null) -> (cfg.database.passwordFile != null);
 | 
						|
        message = "When services.gitlab.database.host is customized, services.discourse.database.passwordFile must be set!";
 | 
						|
      }
 | 
						|
      {
 | 
						|
        assertion = cfg.hostname != "";
 | 
						|
        message = "Could not automatically determine hostname, set service.discourse.hostname manually.";
 | 
						|
      }
 | 
						|
    ];
 | 
						|
 | 
						|
 | 
						|
    # Default config values are from `config/discourse_defaults.conf`
 | 
						|
    # upstream.
 | 
						|
    services.discourse.backendSettings = lib.mapAttrs (_: lib.mkDefault) {
 | 
						|
      db_pool = cfg.database.pool;
 | 
						|
      db_timeout = 5000;
 | 
						|
      db_connect_timeout = 5;
 | 
						|
      db_socket = null;
 | 
						|
      db_host = cfg.database.host;
 | 
						|
      db_backup_host = null;
 | 
						|
      db_port = null;
 | 
						|
      db_backup_port = 5432;
 | 
						|
      db_name = cfg.database.name;
 | 
						|
      db_username = if databaseActuallyCreateLocally then "discourse" else cfg.database.username;
 | 
						|
      db_password = cfg.database.passwordFile;
 | 
						|
      db_prepared_statements = false;
 | 
						|
      db_replica_host = null;
 | 
						|
      db_replica_port = null;
 | 
						|
      db_advisory_locks = true;
 | 
						|
 | 
						|
      inherit (cfg) hostname;
 | 
						|
      backup_hostname = null;
 | 
						|
 | 
						|
      smtp_address = cfg.mail.outgoing.serverAddress;
 | 
						|
      smtp_port = cfg.mail.outgoing.port;
 | 
						|
      smtp_domain = cfg.mail.outgoing.domain;
 | 
						|
      smtp_user_name = cfg.mail.outgoing.username;
 | 
						|
      smtp_password = cfg.mail.outgoing.passwordFile;
 | 
						|
      smtp_authentication = cfg.mail.outgoing.authentication;
 | 
						|
      smtp_enable_start_tls = cfg.mail.outgoing.enableStartTLSAuto;
 | 
						|
      smtp_openssl_verify_mode = cfg.mail.outgoing.opensslVerifyMode;
 | 
						|
 | 
						|
      load_mini_profiler = true;
 | 
						|
      mini_profiler_snapshots_period = 0;
 | 
						|
      mini_profiler_snapshots_transport_url = null;
 | 
						|
      mini_profiler_snapshots_transport_auth_key = null;
 | 
						|
 | 
						|
      cdn_url = null;
 | 
						|
      cdn_origin_hostname = null;
 | 
						|
      developer_emails = null;
 | 
						|
 | 
						|
      redis_host = cfg.redis.host;
 | 
						|
      redis_port = 6379;
 | 
						|
      redis_slave_host = null;
 | 
						|
      redis_slave_port = 6379;
 | 
						|
      redis_db = cfg.redis.dbNumber;
 | 
						|
      redis_password = cfg.redis.passwordFile;
 | 
						|
      redis_skip_client_commands = false;
 | 
						|
      redis_use_ssl = cfg.redis.useSSL;
 | 
						|
 | 
						|
      message_bus_redis_enabled = false;
 | 
						|
      message_bus_redis_host = "localhost";
 | 
						|
      message_bus_redis_port = 6379;
 | 
						|
      message_bus_redis_slave_host = null;
 | 
						|
      message_bus_redis_slave_port = 6379;
 | 
						|
      message_bus_redis_db = 0;
 | 
						|
      message_bus_redis_password = null;
 | 
						|
      message_bus_redis_skip_client_commands = false;
 | 
						|
 | 
						|
      enable_cors = false;
 | 
						|
      cors_origin = "";
 | 
						|
      serve_static_assets = false;
 | 
						|
      sidekiq_workers = 5;
 | 
						|
      rtl_css = false;
 | 
						|
      connection_reaper_age = 30;
 | 
						|
      connection_reaper_interval = 30;
 | 
						|
      relative_url_root = null;
 | 
						|
      message_bus_max_backlog_size = 100;
 | 
						|
      secret_key_base = cfg.secretKeyBaseFile;
 | 
						|
      fallback_assets_path = null;
 | 
						|
 | 
						|
      s3_bucket = null;
 | 
						|
      s3_region = null;
 | 
						|
      s3_access_key_id = null;
 | 
						|
      s3_secret_access_key = null;
 | 
						|
      s3_use_iam_profile = null;
 | 
						|
      s3_cdn_url = null;
 | 
						|
      s3_endpoint = null;
 | 
						|
      s3_http_continue_timeout = null;
 | 
						|
      s3_install_cors_rule = null;
 | 
						|
 | 
						|
      max_user_api_reqs_per_minute = 20;
 | 
						|
      max_user_api_reqs_per_day = 2880;
 | 
						|
      max_admin_api_reqs_per_key_per_minute = 60;
 | 
						|
      max_reqs_per_ip_per_minute = 200;
 | 
						|
      max_reqs_per_ip_per_10_seconds = 50;
 | 
						|
      max_asset_reqs_per_ip_per_10_seconds = 200;
 | 
						|
      max_reqs_per_ip_mode = "block";
 | 
						|
      max_reqs_rate_limit_on_private = false;
 | 
						|
      force_anonymous_min_queue_seconds = 1;
 | 
						|
      force_anonymous_min_per_10_seconds = 3;
 | 
						|
      background_requests_max_queue_length = 0.5;
 | 
						|
      reject_message_bus_queue_seconds = 0.1;
 | 
						|
      disable_search_queue_threshold = 1;
 | 
						|
      max_old_rebakes_per_15_minutes = 300;
 | 
						|
      max_logster_logs = 1000;
 | 
						|
      refresh_maxmind_db_during_precompile_days = 2;
 | 
						|
      maxmind_backup_path = null;
 | 
						|
      maxmind_license_key = null;
 | 
						|
      enable_performance_http_headers = false;
 | 
						|
      enable_js_error_reporting = true;
 | 
						|
      mini_scheduler_workers = 5;
 | 
						|
      compress_anon_cache = false;
 | 
						|
      anon_cache_store_threshold = 2;
 | 
						|
      allowed_theme_repos = null;
 | 
						|
      enable_email_sync_demon = false;
 | 
						|
      max_digests_enqueued_per_30_mins_per_site = 10000;
 | 
						|
    };
 | 
						|
 | 
						|
    services.redis.enable = lib.mkDefault (cfg.redis.host == "localhost");
 | 
						|
 | 
						|
    services.postgresql = lib.mkIf databaseActuallyCreateLocally {
 | 
						|
      enable = true;
 | 
						|
      ensureUsers = [{ name = "discourse"; }];
 | 
						|
    };
 | 
						|
 | 
						|
    # The postgresql module doesn't currently support concepts like
 | 
						|
    # objects owners and extensions; for now we tack on what's needed
 | 
						|
    # here.
 | 
						|
    systemd.services.discourse-postgresql =
 | 
						|
      let
 | 
						|
        pgsql = config.services.postgresql;
 | 
						|
      in
 | 
						|
        lib.mkIf databaseActuallyCreateLocally {
 | 
						|
          after = [ "postgresql.service" ];
 | 
						|
          bindsTo = [ "postgresql.service" ];
 | 
						|
          wantedBy = [ "discourse.service" ];
 | 
						|
          partOf = [ "discourse.service" ];
 | 
						|
          path = [
 | 
						|
            pgsql.package
 | 
						|
          ];
 | 
						|
          script = ''
 | 
						|
            set -o errexit -o pipefail -o nounset -o errtrace
 | 
						|
            shopt -s inherit_errexit
 | 
						|
 | 
						|
            psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'discourse'" | grep -q 1 || psql -tAc 'CREATE DATABASE "discourse" OWNER "discourse"'
 | 
						|
            psql '${cfg.database.name}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
 | 
						|
            psql '${cfg.database.name}' -tAc "CREATE EXTENSION IF NOT EXISTS hstore"
 | 
						|
          '';
 | 
						|
 | 
						|
          serviceConfig = {
 | 
						|
            User = pgsql.superUser;
 | 
						|
            Type = "oneshot";
 | 
						|
            RemainAfterExit = true;
 | 
						|
          };
 | 
						|
        };
 | 
						|
 | 
						|
    systemd.services.discourse = {
 | 
						|
      wantedBy = [ "multi-user.target" ];
 | 
						|
      after = [
 | 
						|
        "redis.service"
 | 
						|
        "postgresql.service"
 | 
						|
        "discourse-postgresql.service"
 | 
						|
      ];
 | 
						|
      bindsTo = [
 | 
						|
        "redis.service"
 | 
						|
      ] ++ lib.optionals (cfg.database.host == null) [
 | 
						|
        "postgresql.service"
 | 
						|
        "discourse-postgresql.service"
 | 
						|
      ];
 | 
						|
      path = cfg.package.runtimeDeps ++ [
 | 
						|
        postgresqlPackage
 | 
						|
        pkgs.replace
 | 
						|
        cfg.package.rake
 | 
						|
      ];
 | 
						|
      environment = cfg.package.runtimeEnv // {
 | 
						|
        UNICORN_TIMEOUT = builtins.toString cfg.unicornTimeout;
 | 
						|
        UNICORN_SIDEKIQS = builtins.toString cfg.sidekiqProcesses;
 | 
						|
      };
 | 
						|
 | 
						|
      preStart =
 | 
						|
        let
 | 
						|
          discourseKeyValue = lib.generators.toKeyValue {
 | 
						|
            mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " = " {
 | 
						|
              mkValueString = v: with builtins;
 | 
						|
                if isInt           v then toString v
 | 
						|
                else if isString   v then ''"${v}"''
 | 
						|
                else if true  ==   v then "true"
 | 
						|
                else if false ==   v then "false"
 | 
						|
                else if null  ==   v then ""
 | 
						|
                else if isFloat    v then lib.strings.floatToString v
 | 
						|
                else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
 | 
						|
            };
 | 
						|
          };
 | 
						|
 | 
						|
          discourseConf = pkgs.writeText "discourse.conf" (discourseKeyValue cfg.backendSettings);
 | 
						|
 | 
						|
          mkSecretReplacement = file:
 | 
						|
            lib.optionalString (file != null) ''
 | 
						|
              (
 | 
						|
                  password=$(<'${file}')
 | 
						|
                  replace-literal -fe '${file}' "$password" /run/discourse/config/discourse.conf
 | 
						|
              )
 | 
						|
            '';
 | 
						|
        in ''
 | 
						|
          set -o errexit -o pipefail -o nounset -o errtrace
 | 
						|
          shopt -s inherit_errexit
 | 
						|
 | 
						|
          umask u=rwx,g=rx,o=
 | 
						|
 | 
						|
          cp -r ${cfg.package}/share/discourse/config.dist/* /run/discourse/config/
 | 
						|
          cp -r ${cfg.package}/share/discourse/public.dist/* /run/discourse/public/
 | 
						|
          cp -r ${cfg.package}/share/discourse/plugins.dist/* /run/discourse/plugins/
 | 
						|
          ${lib.concatMapStrings (p: "ln -sf ${p} /run/discourse/plugins/") cfg.plugins}
 | 
						|
          ln -sf /var/lib/discourse/uploads /run/discourse/public/uploads
 | 
						|
          ln -sf /var/lib/discourse/backups /run/discourse/public/backups
 | 
						|
 | 
						|
          (
 | 
						|
              umask u=rwx,g=,o=
 | 
						|
 | 
						|
              ${utils.genJqSecretsReplacementSnippet
 | 
						|
                  cfg.siteSettings
 | 
						|
                  "/run/discourse/config/nixos_site_settings.json"
 | 
						|
              }
 | 
						|
              install -T -m 0400 -o discourse ${discourseConf} /run/discourse/config/discourse.conf
 | 
						|
              ${mkSecretReplacement cfg.database.passwordFile}
 | 
						|
              ${mkSecretReplacement cfg.mail.outgoing.passwordFile}
 | 
						|
              ${mkSecretReplacement cfg.redis.passwordFile}
 | 
						|
              ${mkSecretReplacement cfg.secretKeyBaseFile}
 | 
						|
          )
 | 
						|
 | 
						|
          discourse-rake db:migrate >>/var/log/discourse/db_migration.log
 | 
						|
          chmod -R u+w /run/discourse/tmp/
 | 
						|
 | 
						|
          export ADMIN_EMAIL="${cfg.admin.email}"
 | 
						|
          export ADMIN_NAME="${cfg.admin.fullName}"
 | 
						|
          export ADMIN_USERNAME="${cfg.admin.username}"
 | 
						|
          export ADMIN_PASSWORD="$(<${cfg.admin.passwordFile})"
 | 
						|
          discourse-rake admin:create_noninteractively
 | 
						|
 | 
						|
          discourse-rake themes:update
 | 
						|
          discourse-rake uploads:regenerate_missing_optimized
 | 
						|
        '';
 | 
						|
 | 
						|
      serviceConfig = {
 | 
						|
        Type = "simple";
 | 
						|
        User = "discourse";
 | 
						|
        Group = "discourse";
 | 
						|
        RuntimeDirectory = map (p: "discourse/" + p) [
 | 
						|
          "config"
 | 
						|
          "home"
 | 
						|
          "tmp"
 | 
						|
          "assets/javascripts/plugins"
 | 
						|
          "public"
 | 
						|
          "plugins"
 | 
						|
          "sockets"
 | 
						|
        ];
 | 
						|
        RuntimeDirectoryMode = 0750;
 | 
						|
        StateDirectory = map (p: "discourse/" + p) [
 | 
						|
          "uploads"
 | 
						|
          "backups"
 | 
						|
        ];
 | 
						|
        StateDirectoryMode = 0750;
 | 
						|
        LogsDirectory = "discourse";
 | 
						|
        TimeoutSec = "infinity";
 | 
						|
        Restart = "on-failure";
 | 
						|
        WorkingDirectory = "${cfg.package}/share/discourse";
 | 
						|
 | 
						|
        RemoveIPC = true;
 | 
						|
        PrivateTmp = true;
 | 
						|
        NoNewPrivileges = true;
 | 
						|
        RestrictSUIDSGID = true;
 | 
						|
        ProtectSystem = "strict";
 | 
						|
        ProtectHome = "read-only";
 | 
						|
 | 
						|
        ExecStart = "${cfg.package.rubyEnv}/bin/bundle exec config/unicorn_launcher -E production -c config/unicorn.conf.rb";
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    services.nginx = lib.mkIf cfg.nginx.enable {
 | 
						|
      enable = true;
 | 
						|
      additionalModules = [ pkgs.nginxModules.brotli ];
 | 
						|
 | 
						|
      recommendedTlsSettings = true;
 | 
						|
      recommendedOptimisation = true;
 | 
						|
      recommendedGzipSettings = true;
 | 
						|
      recommendedProxySettings = true;
 | 
						|
 | 
						|
      upstreams.discourse.servers."unix:/run/discourse/sockets/unicorn.sock" = {};
 | 
						|
 | 
						|
      appendHttpConfig = ''
 | 
						|
        # inactive means we keep stuff around for 1440m minutes regardless of last access (1 week)
 | 
						|
        # levels means it is a 2 deep heirarchy cause we can have lots of files
 | 
						|
        # max_size limits the size of the cache
 | 
						|
        proxy_cache_path /var/cache/nginx inactive=1440m levels=1:2 keys_zone=discourse:10m max_size=600m;
 | 
						|
 | 
						|
        # see: https://meta.discourse.org/t/x/74060
 | 
						|
        proxy_buffer_size 8k;
 | 
						|
      '';
 | 
						|
 | 
						|
      virtualHosts.${cfg.hostname} = {
 | 
						|
        inherit (cfg) sslCertificate sslCertificateKey enableACME;
 | 
						|
        forceSSL = lib.mkDefault tlsEnabled;
 | 
						|
 | 
						|
        root = "/run/discourse/public";
 | 
						|
 | 
						|
        locations =
 | 
						|
          let
 | 
						|
            proxy = { extraConfig ? "" }: {
 | 
						|
              proxyPass = "http://discourse";
 | 
						|
              extraConfig = extraConfig + ''
 | 
						|
                proxy_set_header X-Request-Start "t=''${msec}";
 | 
						|
              '';
 | 
						|
            };
 | 
						|
            cache = time: ''
 | 
						|
              expires ${time};
 | 
						|
              add_header Cache-Control public,immutable;
 | 
						|
            '';
 | 
						|
            cache_1y = cache "1y";
 | 
						|
            cache_1d = cache "1d";
 | 
						|
          in
 | 
						|
            {
 | 
						|
              "/".tryFiles = "$uri @discourse";
 | 
						|
              "@discourse" = proxy {};
 | 
						|
              "^~ /backups/".extraConfig = ''
 | 
						|
                internal;
 | 
						|
              '';
 | 
						|
              "/favicon.ico" = {
 | 
						|
                return = "204";
 | 
						|
                extraConfig = ''
 | 
						|
                  access_log off;
 | 
						|
                  log_not_found off;
 | 
						|
                '';
 | 
						|
              };
 | 
						|
              "~ ^/uploads/short-url/" = proxy {};
 | 
						|
              "~ ^/secure-media-uploads/" = proxy {};
 | 
						|
              "~* (fonts|assets|plugins|uploads)/.*\.(eot|ttf|woff|woff2|ico|otf)$".extraConfig = cache_1y + ''
 | 
						|
                add_header Access-Control-Allow-Origin *;
 | 
						|
              '';
 | 
						|
              "/srv/status" = proxy {
 | 
						|
                extraConfig = ''
 | 
						|
                  access_log off;
 | 
						|
                  log_not_found off;
 | 
						|
                '';
 | 
						|
              };
 | 
						|
              "~ ^/javascripts/".extraConfig = cache_1d;
 | 
						|
              "~ ^/assets/(?<asset_path>.+)$".extraConfig = cache_1y + ''
 | 
						|
                # asset pipeline enables this
 | 
						|
                brotli_static on;
 | 
						|
                gzip_static on;
 | 
						|
              '';
 | 
						|
              "~ ^/plugins/".extraConfig = cache_1y;
 | 
						|
              "~ /images/emoji/".extraConfig = cache_1y;
 | 
						|
              "~ ^/uploads/" = proxy {
 | 
						|
                extraConfig = cache_1y + ''
 | 
						|
                  proxy_set_header X-Sendfile-Type X-Accel-Redirect;
 | 
						|
                  proxy_set_header X-Accel-Mapping /run/discourse/public/=/downloads/;
 | 
						|
 | 
						|
                  # custom CSS
 | 
						|
                  location ~ /stylesheet-cache/ {
 | 
						|
                      try_files $uri =404;
 | 
						|
                  }
 | 
						|
                  # this allows us to bypass rails
 | 
						|
                  location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp)$ {
 | 
						|
                      try_files $uri =404;
 | 
						|
                  }
 | 
						|
                  # SVG needs an extra header attached
 | 
						|
                  location ~* \.(svg)$ {
 | 
						|
                  }
 | 
						|
                  # thumbnails & optimized images
 | 
						|
                  location ~ /_?optimized/ {
 | 
						|
                      try_files $uri =404;
 | 
						|
                  }
 | 
						|
                '';
 | 
						|
              };
 | 
						|
              "~ ^/admin/backups/" = proxy {
 | 
						|
                extraConfig = ''
 | 
						|
                  proxy_set_header X-Sendfile-Type X-Accel-Redirect;
 | 
						|
                  proxy_set_header X-Accel-Mapping /run/discourse/public/=/downloads/;
 | 
						|
                '';
 | 
						|
              };
 | 
						|
              "~ ^/(svg-sprite/|letter_avatar/|letter_avatar_proxy/|user_avatar|highlight-js|stylesheets|theme-javascripts|favicon/proxied|service-worker)" = proxy {
 | 
						|
                extraConfig = ''
 | 
						|
                  # if Set-Cookie is in the response nothing gets cached
 | 
						|
                  # this is double bad cause we are not passing last modified in
 | 
						|
                  proxy_ignore_headers "Set-Cookie";
 | 
						|
                  proxy_hide_header "Set-Cookie";
 | 
						|
                  proxy_hide_header "X-Discourse-Username";
 | 
						|
                  proxy_hide_header "X-Runtime";
 | 
						|
 | 
						|
                  # note x-accel-redirect can not be used with proxy_cache
 | 
						|
                  proxy_cache discourse;
 | 
						|
                  proxy_cache_key "$scheme,$host,$request_uri";
 | 
						|
                  proxy_cache_valid 200 301 302 7d;
 | 
						|
                  proxy_cache_valid any 1m;
 | 
						|
                '';
 | 
						|
              };
 | 
						|
              "/message-bus/" = proxy {
 | 
						|
                extraConfig = ''
 | 
						|
                  proxy_http_version 1.1;
 | 
						|
                  proxy_buffering off;
 | 
						|
                '';
 | 
						|
              };
 | 
						|
              "/downloads/".extraConfig = ''
 | 
						|
                internal;
 | 
						|
                alias /run/discourse/public/;
 | 
						|
              '';
 | 
						|
            };
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    systemd.services.discourse-mail-receiver-setup = lib.mkIf cfg.mail.incoming.enable (
 | 
						|
      let
 | 
						|
        mail-receiver-environment = {
 | 
						|
          MAIL_DOMAIN = cfg.hostname;
 | 
						|
          DISCOURSE_BASE_URL = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
 | 
						|
          DISCOURSE_API_KEY = "@api-key@";
 | 
						|
          DISCOURSE_API_USERNAME = "system";
 | 
						|
        };
 | 
						|
        mail-receiver-json = json.generate "mail-receiver.json" mail-receiver-environment;
 | 
						|
      in
 | 
						|
        {
 | 
						|
          before = [ "postfix.service" ];
 | 
						|
          after = [ "discourse.service" ];
 | 
						|
          wantedBy = [ "discourse.service" ];
 | 
						|
          partOf = [ "discourse.service" ];
 | 
						|
          path = [
 | 
						|
            cfg.package.rake
 | 
						|
            pkgs.jq
 | 
						|
          ];
 | 
						|
          preStart = lib.optionalString (cfg.mail.incoming.apiKeyFile == null) ''
 | 
						|
            set -o errexit -o pipefail -o nounset -o errtrace
 | 
						|
            shopt -s inherit_errexit
 | 
						|
 | 
						|
            if [[ ! -e /var/lib/discourse-mail-receiver/api_key ]]; then
 | 
						|
                discourse-rake api_key:create_master[email-receiver] >/var/lib/discourse-mail-receiver/api_key
 | 
						|
            fi
 | 
						|
          '';
 | 
						|
          script =
 | 
						|
            let
 | 
						|
              apiKeyPath =
 | 
						|
                if cfg.mail.incoming.apiKeyFile == null then
 | 
						|
                  "/var/lib/discourse-mail-receiver/api_key"
 | 
						|
                else
 | 
						|
                  cfg.mail.incoming.apiKeyFile;
 | 
						|
            in ''
 | 
						|
              set -o errexit -o pipefail -o nounset -o errtrace
 | 
						|
              shopt -s inherit_errexit
 | 
						|
 | 
						|
              export api_key=$(<'${apiKeyPath}')
 | 
						|
 | 
						|
              jq <${mail-receiver-json} \
 | 
						|
                 '.DISCOURSE_API_KEY = $ENV.api_key' \
 | 
						|
                 >'/run/discourse-mail-receiver/mail-receiver-environment.json'
 | 
						|
            '';
 | 
						|
 | 
						|
          serviceConfig = {
 | 
						|
            Type = "oneshot";
 | 
						|
            RemainAfterExit = true;
 | 
						|
            RuntimeDirectory = "discourse-mail-receiver";
 | 
						|
            RuntimeDirectoryMode = "0700";
 | 
						|
            StateDirectory = "discourse-mail-receiver";
 | 
						|
            User = "discourse";
 | 
						|
            Group = "discourse";
 | 
						|
          };
 | 
						|
        });
 | 
						|
 | 
						|
    services.discourse.siteSettings = {
 | 
						|
      required = {
 | 
						|
        notification_email = cfg.mail.notificationEmailAddress;
 | 
						|
        contact_email = cfg.mail.contactEmailAddress;
 | 
						|
      };
 | 
						|
      email = {
 | 
						|
        manual_polling_enabled = cfg.mail.incoming.enable;
 | 
						|
        reply_by_email_enabled = cfg.mail.incoming.enable;
 | 
						|
        reply_by_email_address = cfg.mail.incoming.replyEmailAddress;
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    services.postfix = lib.mkIf cfg.mail.incoming.enable {
 | 
						|
      enable = true;
 | 
						|
      sslCert = if cfg.sslCertificate != null then cfg.sslCertificate else "";
 | 
						|
      sslKey = if cfg.sslCertificateKey != null then cfg.sslCertificateKey else "";
 | 
						|
 | 
						|
      origin = cfg.hostname;
 | 
						|
      relayDomains = [ cfg.hostname ];
 | 
						|
      config = {
 | 
						|
        smtpd_recipient_restrictions = "check_policy_service unix:private/discourse-policy";
 | 
						|
        append_dot_mydomain = lib.mkDefault false;
 | 
						|
        compatibility_level = "2";
 | 
						|
        smtputf8_enable = false;
 | 
						|
        smtpd_banner = lib.mkDefault "ESMTP server";
 | 
						|
        myhostname = lib.mkDefault cfg.hostname;
 | 
						|
        mydestination = lib.mkDefault "localhost";
 | 
						|
      };
 | 
						|
      transport = ''
 | 
						|
        ${cfg.hostname} discourse-mail-receiver:
 | 
						|
      '';
 | 
						|
      masterConfig = {
 | 
						|
        "discourse-mail-receiver" = {
 | 
						|
          type = "unix";
 | 
						|
          privileged = true;
 | 
						|
          chroot = false;
 | 
						|
          command = "pipe";
 | 
						|
          args = [
 | 
						|
            "user=discourse"
 | 
						|
            "argv=${cfg.mail.incoming.mailReceiverPackage}/bin/receive-mail"
 | 
						|
            "\${recipient}"
 | 
						|
          ];
 | 
						|
        };
 | 
						|
        "discourse-policy" = {
 | 
						|
          type = "unix";
 | 
						|
          privileged = true;
 | 
						|
          chroot = false;
 | 
						|
          command = "spawn";
 | 
						|
          args = [
 | 
						|
            "user=discourse"
 | 
						|
            "argv=${cfg.mail.incoming.mailReceiverPackage}/bin/discourse-smtp-fast-rejection"
 | 
						|
          ];
 | 
						|
        };
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    users.users = {
 | 
						|
      discourse = {
 | 
						|
        group = "discourse";
 | 
						|
        isSystemUser = true;
 | 
						|
      };
 | 
						|
    } // (lib.optionalAttrs cfg.nginx.enable {
 | 
						|
      ${config.services.nginx.user}.extraGroups = [ "discourse" ];
 | 
						|
    });
 | 
						|
 | 
						|
    users.groups = {
 | 
						|
      discourse = {};
 | 
						|
    };
 | 
						|
 | 
						|
    environment.systemPackages = [
 | 
						|
      cfg.package.rake
 | 
						|
    ];
 | 
						|
  };
 | 
						|
 | 
						|
  meta.doc = ./discourse.xml;
 | 
						|
  meta.maintainers = [ lib.maintainers.talyz ];
 | 
						|
}
 |