Unverified Commit 299e0b95 authored by Alexander Bantyev's avatar Alexander Bantyev Committed by GitHub
Browse files

Merge pull request #255033 from AleXoundOS/castopod

castopod: init at 1.6.4
parents 09ff92e2 c220d280
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -759,6 +759,12 @@
    github = "Alexnortung";
    githubId = 1552267;
  };
  alexoundos = {
    email = "alexoundos@gmail.com";
    github = "AleXoundOS";
    githubId = 464913;
    name = "Alexander Tomokhov";
  };
  alexshpilkin = {
    email = "ashpilkin@gmail.com";
    github = "alexshpilkin";
+2 −0
Original line number Diff line number Diff line
@@ -41,6 +41,8 @@

- [GoToSocial](https://gotosocial.org/), an ActivityPub social network server, written in Golang. Available as [services.gotosocial](#opt-services.gotosocial.enable).

- [Castopod](https://castopod.org/), an open-source hosting platform made for podcasters who want to engage and interact with their audience. Available as [services.castopod](#opt-services.castopod.enable).

- [Typesense](https://github.com/typesense/typesense), a fast, typo-tolerant search engine for building delightful search experiences. Available as [services.typesense](#opt-services.typesense.enable).

* [NS-USBLoader](https://github.com/developersu/ns-usbloader/), an all-in-one tool for managing Nintendo Switch homebrew. Available as [programs.ns-usbloader](#opt-programs.ns-usbloader.enable).
+1 −0
Original line number Diff line number Diff line
@@ -324,6 +324,7 @@
  ./services/amqp/rabbitmq.nix
  ./services/audio/alsa.nix
  ./services/audio/botamusique.nix
  ./services/audio/castopod.nix
  ./services/audio/gmediarender.nix
  ./services/audio/gonic.nix
  ./services/audio/goxlr-utility.nix
+22 −0
Original line number Diff line number Diff line
# Castopod {#module-services-castopod}

Castopod is an open-source hosting platform made for podcasters who want to engage and interact with their audience.

## Quickstart {#module-services-castopod-quickstart}

Use the following configuration to start a public instance of Castopod on `castopod.example.com` domain:

```nix
networking.firewall.allowedTCPPorts = [ 80 443 ];
services.castopod = {
  enable = true;
  database.createLocally = true;
  nginx.virtualHost = {
    serverName = "castopod.example.com";
    enableACME = true;
    forceSSL = true;
  };
};
```

Go to `https://castopod.example.com/cp-install` to create superadmin account after applying the above configuration.
+287 −0
Original line number Diff line number Diff line
{ config, lib, pkgs, ... }:
let
  cfg = config.services.castopod;
  fpm = config.services.phpfpm.pools.castopod;

  user = "castopod";
  stateDirectory = "/var/lib/castopod";

  # https://docs.castopod.org/getting-started/install.html#requirements
  phpPackage = pkgs.php.withExtensions ({ enabled, all }: with all; [
    intl
    curl
    mbstring
    gd
    exif
    mysqlnd
  ] ++ enabled);
in
{
  meta.doc = ./castopod.md;
  meta.maintainers = with lib.maintainers; [ alexoundos misuzu ];

  options.services = {
    castopod = {
      enable = lib.mkEnableOption (lib.mdDoc "Castopod");
      package = lib.mkOption {
        type = lib.types.package;
        default = pkgs.castopod;
        defaultText = lib.literalMD "pkgs.castopod";
        description = lib.mdDoc "Which Castopod package to use.";
      };
      database = {
        createLocally = lib.mkOption {
          type = lib.types.bool;
          default = true;
          description = lib.mdDoc ''
            Create the database and database user locally.
          '';
        };
        hostname = lib.mkOption {
          type = lib.types.str;
          default = "localhost";
          description = lib.mdDoc "Database hostname.";
        };
        name = lib.mkOption {
          type = lib.types.str;
          default = "castopod";
          description = lib.mdDoc "Database name.";
        };
        user = lib.mkOption {
          type = lib.types.str;
          default = user;
          description = lib.mdDoc "Database user.";
        };
        passwordFile = lib.mkOption {
          type = lib.types.nullOr lib.types.path;
          default = null;
          example = "/run/keys/castopod-dbpassword";
          description = lib.mdDoc ''
            A file containing the password corresponding to
            [](#opt-services.castopod.database.user).
          '';
        };
      };
      settings = lib.mkOption {
        type = with lib.types; attrsOf (oneOf [ str int bool ]);
        default = { };
        example = {
          "email.protocol" = "smtp";
          "email.SMTPHost" = "localhost";
          "email.SMTPUser" = "myuser";
          "email.fromEmail" = "castopod@example.com";
        };
        description = lib.mdDoc ''
          Environment variables used for Castopod.
          See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
          for available environment variables.
        '';
      };
      environmentFile = lib.mkOption {
        type = lib.types.nullOr lib.types.path;
        default = null;
        example = "/run/keys/castopod-env";
        description = lib.mdDoc ''
          Environment file to inject e.g. secrets into the configuration.
          See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example)
          for available environment variables.
        '';
      };
      configureNginx = lib.mkOption {
        type = lib.types.bool;
        default = true;
        description = lib.mdDoc "Configure nginx as a reverse proxy for CastoPod.";
      };
      localDomain = lib.mkOption {
        type = lib.types.str;
        example = "castopod.example.org";
        description = lib.mdDoc "The domain serving your CastoPod instance.";
      };
      poolSettings = lib.mkOption {
        type = with lib.types; attrsOf (oneOf [ str int bool ]);
        default = {
          "pm" = "dynamic";
          "pm.max_children" = "32";
          "pm.start_servers" = "2";
          "pm.min_spare_servers" = "2";
          "pm.max_spare_servers" = "4";
          "pm.max_requests" = "500";
        };
        description = lib.mdDoc ''
          Options for Castopod's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives.
        '';
      };
    };
  };

  config = lib.mkIf cfg.enable {
    services.castopod.settings =
      let
        sslEnabled = with config.services.nginx.virtualHosts.${cfg.localDomain}; addSSL || forceSSL || onlySSL || enableACME || useACMEHost != null;
        baseURL = "http${lib.optionalString sslEnabled "s"}://${cfg.localDomain}";
      in
      lib.mapAttrs (name: lib.mkDefault) {
        "app.forceGlobalSecureRequests" = sslEnabled;
        "app.baseURL" = baseURL;

        "media.baseURL" = "/";
        "media.root" = "media";
        "media.storage" = stateDirectory;

        "admin.gateway" = "admin";
        "auth.gateway" = "auth";

        "database.default.hostname" = cfg.database.hostname;
        "database.default.database" = cfg.database.name;
        "database.default.username" = cfg.database.user;
        "database.default.DBPrefix" = "cp_";

        "cache.handler" = "file";
      };

    services.phpfpm.pools.castopod = {
      inherit user;
      group = config.services.nginx.group;
      phpPackage = phpPackage;
      phpOptions = ''
        # https://code.castopod.org/adaures/castopod/-/blob/main/docker/production/app/uploads.ini
        file_uploads = On
        memory_limit = 512M
        upload_max_filesize = 500M
        post_max_size = 512M
        max_execution_time = 300
        max_input_time = 300
      '';
      settings = {
        "listen.owner" = config.services.nginx.user;
        "listen.group" = config.services.nginx.group;
      } // cfg.poolSettings;
    };

    systemd.services.castopod-setup = {
      after = lib.optional config.services.mysql.enable "mysql.service";
      requires = lib.optional config.services.mysql.enable "mysql.service";
      wantedBy = [ "multi-user.target" ];
      path = [ pkgs.openssl phpPackage ];
      script =
        let
          envFile = "${stateDirectory}/.env";
          media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}";
        in
        ''
          mkdir -p ${stateDirectory}/writable/{cache,logs,session,temp,uploads}

          if [ ! -d ${lib.escapeShellArg media} ]; then
            cp --no-preserve=mode,ownership -r ${cfg.package}/share/castopod/public/media ${lib.escapeShellArg media}
          fi

          if [ ! -f ${stateDirectory}/salt ]; then
            openssl rand -base64 33 > ${stateDirectory}/salt
          fi

          cat <<'EOF' > ${envFile}
          ${lib.generators.toKeyValue { } cfg.settings}
          EOF

          echo "analytics.salt=$(cat ${stateDirectory}/salt)" >> ${envFile}

          ${if (cfg.database.passwordFile != null) then ''
            echo "database.default.password=$(cat ${lib.escapeShellArg cfg.database.passwordFile})" >> ${envFile}
          '' else ''
            echo "database.default.password=" >> ${envFile}
          ''}

          ${lib.optionalString (cfg.environmentFile != null) ''
            cat ${lib.escapeShellArg cfg.environmentFile}) >> ${envFile}
          ''}

          php spark castopod:database-update
        '';
      serviceConfig = {
        StateDirectory = "castopod";
        WorkingDirectory = "${cfg.package}/share/castopod";
        Type = "oneshot";
        RemainAfterExit = true;
        User = user;
        Group = config.services.nginx.group;
      };
    };

    systemd.services.castopod-scheduled = {
      after = [ "castopod-setup.service" ];
      wantedBy = [ "multi-user.target" ];
      path = [ phpPackage ];
      script = ''
        php public/index.php scheduled-activities
        php public/index.php scheduled-websub-publish
        php public/index.php scheduled-video-clips
      '';
      serviceConfig = {
        StateDirectory = "castopod";
        WorkingDirectory = "${cfg.package}/share/castopod";
        Type = "oneshot";
        User = user;
        Group = config.services.nginx.group;
      };
    };

    systemd.timers.castopod-scheduled = {
      wantedBy = [ "timers.target" ];
      timerConfig = {
        OnCalendar = "*-*-* *:*:00";
        Unit = "castopod-scheduled.service";
      };
    };

    services.mysql = lib.mkIf cfg.database.createLocally {
      enable = true;
      package = lib.mkDefault pkgs.mariadb;
      ensureDatabases = [ cfg.database.name ];
      ensureUsers = [{
        name = cfg.database.user;
        ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
      }];
    };

    services.nginx = lib.mkIf cfg.configureNginx {
      enable = true;
      virtualHosts."${cfg.localDomain}" = {
        root = lib.mkForce "${cfg.package}/share/castopod/public";

        extraConfig = ''
          try_files $uri $uri/ /index.php?$args;
          index index.php index.html;
        '';

        locations."^~ /${cfg.settings."media.root"}/" = {
          root = cfg.settings."media.storage";
          extraConfig = ''
            add_header Access-Control-Allow-Origin "*";
            expires max;
            access_log off;
          '';
        };

        locations."~ \.php$" = {
          fastcgiParams = {
            SERVER_NAME = "$host";
          };
          extraConfig = ''
            fastcgi_intercept_errors on;
            fastcgi_index index.php;
            fastcgi_pass unix:${fpm.socket};
            try_files $uri =404;
            fastcgi_read_timeout 3600;
            fastcgi_send_timeout 3600;
          '';
        };
      };
    };

    users.users.${user} = lib.mapAttrs (name: lib.mkDefault) {
      description = "Castopod user";
      isSystemUser = true;
      group = config.services.nginx.group;
    };
  };
}
Loading