diff --git a/machines/web01/_configuration.nix b/machines/web01/_configuration.nix index 66fc366..f284ef2 100644 --- a/machines/web01/_configuration.nix +++ b/machines/web01/_configuration.nix @@ -8,6 +8,7 @@ lib.extra.mkConfig { enabledServices = [ # List of services to enable + "castopod" "metis" "linkal" "ntfy-sh" diff --git a/machines/web01/castopod.nix b/machines/web01/castopod.nix new file mode 100644 index 0000000..9f69e14 --- /dev/null +++ b/machines/web01/castopod.nix @@ -0,0 +1,36 @@ +{ config, pkgs, ...}: +let + host = "podcasts.dgnum.eu"; +in +{ + # Notes: + # le paramètre analytics.salt est créé par le service + services.castopod = { + enable = true; + localDomain = host; + environmentFile = config.age.secrets.castopod-environment_file.path; + settings = { + "email.fromEmail"="podcasts@infra.dgnum.eu"; + "email.SMTPHost"="kurisu.lahfa.xyz"; + "email.SMTPUser"="web-services@infra.dgnum.eu"; + "email.SMTPPort"="465"; + + "media.fileManager"="s3"; + "media.s3.endpoint"="https://s3.dgnum.eu/"; + "media.s3.region"="garage"; + "media.s3.bucket"="castopod"; + "media.s3.pathStyleEndpoint"=true; + + "restapi.enabled"=true; + "restapi.basicAuthUsername"="castopod"; + "restapi.basicAuth"=true; + }; + database.createLocally = true; + configureNginx = true; + }; + services.mysql.package = pkgs.mariadb; + services.nginx.virtualHosts.${host} = { + forceSSL = true; + enableACME = true; + }; +} diff --git a/machines/web01/secrets/castopod-environment_file b/machines/web01/secrets/castopod-environment_file new file mode 100644 index 0000000..1ecf60e Binary files /dev/null and b/machines/web01/secrets/castopod-environment_file differ diff --git a/machines/web01/secrets/secrets.nix b/machines/web01/secrets/secrets.nix index 75cdc98..87eaede 100644 --- a/machines/web01/secrets/secrets.nix +++ b/machines/web01/secrets/secrets.nix @@ -9,4 +9,5 @@ in lib.setDefault { inherit publicKeys; } [ "plausible_secret-key-base-file" "plausible_release-cookie-file" "_smtp-password-file" + "castopod-environment_file" ] diff --git a/patches/castopod.patch b/patches/castopod.patch new file mode 100644 index 0000000..73df444 --- /dev/null +++ b/patches/castopod.patch @@ -0,0 +1,556 @@ +From 9d7860c7e7830d9bf82733cecd443ff167dc2174 Mon Sep 17 00:00:00 2001 +From: Alexander Tomokhov +Date: Thu, 14 Sep 2023 02:19:40 +0400 +Subject: [PATCH 1/4] maintainers: add alexoundos + +--- + maintainers/maintainer-list.nix | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix +index e54123ac9e4907..bc28b303afaad5 100644 +--- a/maintainers/maintainer-list.nix ++++ b/maintainers/maintainer-list.nix +@@ -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"; + +From 17f036f4fbdd1024ebc91ff58686b9b8ce62990b Mon Sep 17 00:00:00 2001 +From: misuzu +Date: Mon, 13 Mar 2023 20:53:59 +0200 +Subject: [PATCH 2/4] castopod: init at 1.6.4 + +--- + pkgs/applications/audio/castopod/default.nix | 51 +++++++++++ + pkgs/applications/audio/castopod/update.sh | 89 ++++++++++++++++++++ + pkgs/top-level/all-packages.nix | 2 + + 3 files changed, 142 insertions(+) + create mode 100644 pkgs/applications/audio/castopod/default.nix + create mode 100755 pkgs/applications/audio/castopod/update.sh + +diff --git a/pkgs/applications/audio/castopod/default.nix b/pkgs/applications/audio/castopod/default.nix +new file mode 100644 +index 00000000000000..e4cdb6025f313f +--- /dev/null ++++ b/pkgs/applications/audio/castopod/default.nix +@@ -0,0 +1,55 @@ ++{ stdenv ++, fetchurl ++, ffmpeg-headless ++, lib ++, stateDirectory ? "/var/lib/castopod" ++}: ++stdenv.mkDerivation { ++ pname = "castopod"; ++ version = "1.6.5"; ++ ++ src = fetchurl { ++ url = "https://code.castopod.org/adaures/castopod/uploads/5aaaa6cf2edaed25bd7253449e5f8584/castopod-1.6.5.tar.gz"; ++ sha256 = "04gcq2vmfy5aa2fmsm1qqv1k8g024nikmysdrhy33wj460d529b5"; ++ }; ++ ++ dontBuild = true; ++ dontFixup = true; ++ ++ postPatch = '' ++ # not configurable at runtime unfortunately: ++ substituteInPlace app/Config/Paths.php \ ++ --replace "__DIR__ . '/../../writable'" "'${stateDirectory}/writable'" ++ ++ substituteInPlace modules/Admin/Controllers/DashboardController.php \ ++ --replace "disk_total_space('./')" "disk_total_space('${stateDirectory}')" ++ ++ # configuration file must be writable, place it to ${stateDirectory} ++ substituteInPlace modules/Install/Controllers/InstallController.php \ ++ --replace "ROOTPATH" "'${stateDirectory}/'" ++ substituteInPlace public/index.php spark \ ++ --replace "DotEnv(ROOTPATH)" "DotEnv('${stateDirectory}')" ++ ++ # ffmpeg is required for Video Clips feature ++ substituteInPlace modules/MediaClipper/VideoClipper.php \ ++ --replace "ffmpeg" "${ffmpeg-headless}/bin/ffmpeg" ++ substituteInPlace modules/Admin/Controllers/VideoClipsController.php \ ++ --replace "which ffmpeg" "echo ${ffmpeg-headless}/bin/ffmpeg" ++ ''; ++ ++ installPhase = '' ++ mkdir -p $out/share/castopod ++ cp -r . $out/share/castopod ++ ''; ++ ++ passthru.stateDirectory = stateDirectory; ++ passthru.updateScript = ./update.sh; ++ ++ meta = with lib; { ++ description = "An open-source hosting platform made for podcasters who want to engage and interact with their audience"; ++ homepage = "https://castopod.org"; ++ license = licenses.agpl3Only; ++ maintainers = with maintainers; [ alexoundos misuzu ]; ++ platforms = platforms.all; ++ }; ++} +diff --git a/pkgs/applications/audio/castopod/update.sh b/pkgs/applications/audio/castopod/update.sh +new file mode 100755 +index 00000000000000..742788dc8ddfdd +--- /dev/null ++++ b/pkgs/applications/audio/castopod/update.sh +@@ -0,0 +1,89 @@ ++#! /usr/bin/env nix-shell ++#! nix-shell -i bash -p curl jq ++set -euo pipefail ++ ++nixpkgs="$(git rev-parse --show-toplevel)" ++castopod_nix="$nixpkgs/pkgs/applications/audio/castopod/default.nix" ++ ++# https://www.meetup.com/api/guide/#p02-querying-section ++query=' ++query allReleases($fullPath: ID!, $first: Int, $last: Int, $before: String, $after: String, $sort: ReleaseSort) { ++ project(fullPath: $fullPath) { ++ id ++ releases( ++ first: $first ++ last: $last ++ before: $before ++ after: $after ++ sort: $sort ++ ) { ++ nodes { ++ ...Release ++ __typename ++ } ++ __typename ++ } ++ __typename ++ } ++} ++ ++fragment Release on Release { ++ id ++ name ++ tagName ++ releasedAt ++ createdAt ++ upcomingRelease ++ historicalRelease ++ assets { ++ links { ++ nodes { ++ id ++ name ++ url ++ directAssetUrl ++ linkType ++ __typename ++ } ++ __typename ++ } ++ __typename ++ } ++ __typename ++} ++' ++variables='{ ++ "fullPath": "adaures/castopod", ++ "first": 1, ++ "sort": "RELEASED_AT_DESC" ++}' ++ ++post=$(cat < +Date: Fri, 7 Apr 2023 15:59:08 +0300 +Subject: [PATCH 3/4] nixos/castopod: init + +--- + nixos/modules/module-list.nix | 1 + + nixos/modules/services/audio/castopod.md | 22 ++ + nixos/modules/services/audio/castopod.nix | 287 ++++++++++++++++++ + 3 files changed, 312 insertions(+) + create mode 100644 nixos/modules/services/audio/castopod.md + create mode 100644 nixos/modules/services/audio/castopod.nix + +diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix +index 206d5eaf75dedc..54fd5c7b040314 100644 +--- a/nixos/modules/module-list.nix ++++ b/nixos/modules/module-list.nix +@@ -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/hqplayerd.nix +diff --git a/nixos/modules/services/audio/castopod.md b/nixos/modules/services/audio/castopod.md +new file mode 100644 +index 00000000000000..ee8590737a7c73 +--- /dev/null ++++ b/nixos/modules/services/audio/castopod.md +@@ -0,0 +1,22 @@ ++# 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. +diff --git a/nixos/modules/services/audio/castopod.nix b/nixos/modules/services/audio/castopod.nix +new file mode 100644 +index 00000000000000..b782b548914795 +--- /dev/null ++++ b/nixos/modules/services/audio/castopod.nix +@@ -0,0 +1,287 @@ ++{ config, lib, pkgs, ... }: ++let ++ cfg = config.services.castopod; ++ fpm = config.services.phpfpm.pools.castopod; ++ ++ user = "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" = baseURL; ++ "media.root" = "media"; ++ "media.storage" = cfg.package.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 = "${cfg.package.stateDirectory}/.env"; ++ media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}"; ++ in ++ '' ++ mkdir -p ${cfg.package.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 ${cfg.package.stateDirectory}/salt ]; then ++ openssl rand -base64 33 > ${cfg.package.stateDirectory}/salt ++ fi ++ ++ cat <<'EOF' > ${envFile} ++ ${lib.generators.toKeyValue { } cfg.settings} ++ EOF ++ ++ echo "analytics.salt=$(cat ${cfg.package.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 "$CREDENTIALS_DIRECTORY/envfile" >> ${envFile} ++ ''} ++ ++ php spark castopod:database-update ++ ''; ++ serviceConfig = { ++ StateDirectory = "castopod"; ++ LoadCredential = lib.mkIf (cfg.environmentFile != null) [ ++ "envfile:${cfg.environmentFile}" ++ ]; ++ 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 ${cfg.package}/share/castopod/spark tasks:run ++ ''; ++ 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; ++ }; ++ }; ++} diff --git a/patches/default.nix b/patches/default.nix index a818d00..5930666 100644 --- a/patches/default.nix +++ b/patches/default.nix @@ -109,5 +109,12 @@ id = 234811; hash = "sha256-Yz007dCmGl5OxRDMSHv63Ww+LzoQISm9Ttiw0p/6spY="; } + + # castopod: init + # Ne pas mettre à jour sans savoir ce qu'on fait (patch un peu customisé par rapport à upstream) + { + _type = "static"; + path = ./castopod.patch; + } ]; }