diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index e87f59bdfc24..50bcabe4a891 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -23891,6 +23891,14 @@ githubId = 18124752; email = "m@rvinvogt.com"; }; + srxl = { + name = "Ruby Iris Juric"; + email = "ruby@srxl.me"; + matrix = "@ruby:isincredibly.gay"; + github = "Sorixelle"; + githubId = 38685302; + keys = [ { fingerprint = "2D76 76C7 A28E 16FC 75C7 268D 1B55 6ED8 4B0E 303A"; } ]; + }; Srylax = { name = "Srylax"; email = "srylax+nixpkgs@srylax.dev"; diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 6aeb5980f97a..36277c5782cf 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -52,7 +52,9 @@ - [SuiteNumérique Docs](https://github.com/suitenumerique/docs), a collaborative note taking, wiki and documentation web platform and alternative to Notion or Outline. Available as [services.lasuite-docs](#opt-services.lasuite-docs.enable). -[dwl](https://codeberg.org/dwl/dwl), a compact, hackable compositor for Wayland based on wlroots. Available as [programs.dwl](#opt-programs.dwl.enable). +- [dwl](https://codeberg.org/dwl/dwl), a compact, hackable compositor for Wayland based on wlroots. Available as [programs.dwl](#opt-programs.dwl.enable). + +- [Sharkey](https://joinsharkey.org), a Sharkish microblogging platform. Available as [services.sharkey](#opt-services.sharkey.enable). - [mautrix-discord](https://github.com/mautrix/discord), a Matrix-Discord puppeting/relay bridge. Available as [services.mautrix-discord](#opt-services.mautrix-discord.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 1877ccb20044..d0831c02e6d7 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1660,6 +1660,7 @@ ./services/web-apps/screego.nix ./services/web-apps/selfoss.nix ./services/web-apps/sftpgo.nix + ./services/web-apps/sharkey.nix ./services/web-apps/shiori.nix ./services/web-apps/silverbullet.nix ./services/web-apps/simplesamlphp.nix diff --git a/nixos/modules/services/web-apps/sharkey.nix b/nixos/modules/services/web-apps/sharkey.nix new file mode 100644 index 000000000000..3c891ff5e892 --- /dev/null +++ b/nixos/modules/services/web-apps/sharkey.nix @@ -0,0 +1,300 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.sharkey; + + settingsFormat = pkgs.formats.yaml { }; + configFile = settingsFormat.generate "config.yml" cfg.settings; +in +{ + options.services.sharkey = + let + inherit (lib) + mkEnableOption + mkOption + mkPackageOption + types + ; + in + { + enable = mkEnableOption "Sharkey, a Sharkish microblogging platform"; + package = mkPackageOption pkgs "sharkey" { }; + + environmentFiles = mkOption { + type = types.listOf types.path; + default = [ ]; + example = [ "/run/secrets/sharkey-env" ]; + description = '' + List of paths to files containing environment variables for Sharkey to use at runtime. + + This is useful for keeping secrets out of the Nix store. See + https://docs.joinsharkey.org/docs/install/configuration/ for how to configure Sharkey using environment + variables. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to open ports in the NixOS firewall for Sharkey. + ''; + }; + + setupMeilisearch = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to automatically set up a local Meilisearch instance and configure Sharkey to use it. + + You need to ensure `services.meilisearch.masterKeyEnvironmentFile` is correctly configured for a working + Meilisearch setup. You also need to configure Sharkey to use an API key obtained from Meilisearch with the + `MK_CONFIG_MEILISEARCH_APIKEY` environment variable, and set `services.sharkey.settings.meilisearch.index` to + the created index. See https://docs.joinsharkey.org/docs/customisation/search/meilisearch/ for how to create + an API key and index. + ''; + }; + + setupPostgresql = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to automatically set up a local PostgreSQL database and configure Sharkey to use it. + ''; + }; + + setupRedis = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to automatically set up a local Redis cache and configure Sharkey to use it. + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + url = mkOption { + type = types.str; + example = "https://blahaj.social/"; + description = '' + The full URL that the Sharkey instance will be publically accessible on. + + Do NOT change this after initial setup! + ''; + }; + + port = mkOption { + type = types.port; + default = 3000; + description = '' + The port that Sharkey will listen on. + ''; + }; + + address = mkOption { + type = types.str; + default = "0.0.0.0"; + example = "127.0.0.1"; + description = '' + The address that Sharkey binds to. + ''; + }; + + socket = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/sharkey/sharkey.sock"; + description = '' + If specified, creates a UNIX socket at the given path that Sharkey listens on. + ''; + }; + + mediaDirectory = mkOption { + type = types.path; + default = "/var/lib/sharkey"; + description = '' + Path to the folder where Sharkey stores uploaded media such as images and attachments. + ''; + }; + + fulltextSearch.provider = mkOption { + type = types.enum [ + "sqlLike" + "sqlPgroonga" + "sqlTsvector" + "meilisearch" + ]; + default = "sqlLike"; + example = "sqlPgroonga"; + description = '' + Which provider to use for full text search. + + All options other than `sqlLike` require extra setup - see the comments in + https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/.config/example.yml for details. + + If `sqlPgroonga` is set, and `services.sharkey.setupPostgres` is `true`, the pgroonga extension will + automatically be setup. You still need to create an index manually. + + If using Meilisearch, consider setting `services.sharkey.setupMeilisearch` instead, which will + configure Meilisearch for you. + ''; + }; + + id = mkOption { + type = types.enum [ + "aid" + "aidx" + "meid" + "ulid" + "objectid" + ]; + default = "aidx"; + description = '' + The ID generation method for Sharkey to use. + + Do NOT change this after initial setup! + ''; + }; + }; + }; + default = { }; + description = '' + Configuration options for Sharkey. + + See https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/.config/example.yml for a list of all + available configuration options. + ''; + }; + }; + + config = + let + inherit (lib) mkDefault mkIf mkMerge; + in + mkIf cfg.enable (mkMerge [ + { + systemd.services.sharkey = { + description = "Sharkey"; + documentation = [ "https://docs.joinsharkey.org/" ]; + wantedBy = [ "multi-user.target" ]; + startLimitBurst = 5; + startLimitIntervalSec = 60; + environment.MISSKEY_CONFIG_DIR = "/etc/sharkey"; + + serviceConfig = { + Type = "simple"; + ExecStart = "${lib.getExe cfg.package} migrateandstart"; + EnvironmentFile = cfg.environmentFiles; + DynamicUser = true; + TimeoutSec = 60; + Restart = "always"; + SyslogIdentifier = "sharkey"; + ConfigurationDirectory = "sharkey"; + RuntimeDirectory = "sharkey"; + StateDirectory = "sharkey"; + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateUsers = true; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + ReadWritePaths = [ cfg.settings.mediaDirectory ]; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK"; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "~@cpu-emulation @debug @mount @obsolete @privileged @resources" + "@chown" + ]; + UMask = "0077"; + }; + }; + + environment.etc."sharkey/default.yml".source = configFile; + } + (mkIf cfg.openFirewall { + networking.firewall.allowedTCPPorts = [ cfg.settings.port ]; + }) + (mkIf cfg.setupMeilisearch { + services.meilisearch = { + enable = mkDefault true; + environment = mkDefault "production"; + }; + + services.sharkey.settings = { + fulltextSearch.provider = "meilisearch"; + meilisearch = { + host = config.services.meilisearch.listenAddress; + port = config.services.meilisearch.listenPort; + }; + }; + + systemd.services.sharkey = { + after = [ "meilisearch.service" ]; + wants = [ "meilisearch.service" ]; + }; + }) + (mkIf cfg.setupPostgresql { + services.postgresql = { + enable = mkDefault true; + ensureDatabases = [ "sharkey" ]; + ensureUsers = [ + { + name = "sharkey"; + ensureDBOwnership = true; + } + ]; + + extensions = mkIf (cfg.settings.fulltextSearch.provider == "sqlPgroonga") (ps: [ ps.pgroonga ]); + }; + + services.sharkey.settings.db = { + host = "/run/postgresql"; + db = "sharkey"; + }; + + systemd.services.sharkey = { + after = [ "postgresql.target" ]; + bindsTo = [ "postgresql.target" ]; + }; + }) + (mkIf cfg.setupRedis { + services.redis.servers.sharkey.enable = mkDefault true; + + services.sharkey.settings.redis.path = config.services.redis.servers.sharkey.unixSocket; + + systemd.services.sharkey = { + after = [ "redis-sharkey.service" ]; + bindsTo = [ "redis-sharkey.service" ]; + + serviceConfig.SupplementaryGroups = [ + config.services.redis.servers.sharkey.group + ]; + }; + }) + ]); + + meta.maintainers = with lib.maintainers; [ srxl ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 82aef7fdea8a..d370a4180286 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1277,6 +1277,7 @@ in sgt-puzzles = runTest ./sgt-puzzles.nix; shadow = runTest ./shadow.nix; shadowsocks = handleTest ./shadowsocks { }; + sharkey = runTest ./web-apps/sharkey.nix; shattered-pixel-dungeon = runTest ./shattered-pixel-dungeon.nix; shiori = runTest ./shiori.nix; signal-desktop = runTest ./signal-desktop.nix; diff --git a/nixos/tests/web-apps/sharkey.nix b/nixos/tests/web-apps/sharkey.nix new file mode 100644 index 000000000000..b10fec68744e --- /dev/null +++ b/nixos/tests/web-apps/sharkey.nix @@ -0,0 +1,55 @@ +{ lib, ... }: + +let + meilisearchKey = "TESTKEY-naXRkVX7nhvLaGOmGGuicDKxZAj0khEaoOZPeEZafv8w9j8V6aKb0NVdXRChL5kR"; +in +{ + name = "sharkey"; + + nodes.machine = + { pkgs, ... }: + { + services.sharkey = { + enable = true; + setupMeilisearch = true; + environmentFiles = [ "/run/secrets/sharkey-env" ]; + settings = { + url = "http://exampleurl.invalid"; + meilisearch.index = "exampleurl_invalid"; + }; + }; + + services.meilisearch.masterKeyEnvironmentFile = pkgs.writeText "meilisearch-key" '' + MEILI_MASTER_KEY=${meilisearchKey} + ''; + }; + + testScript = + let + createIndexPayload = builtins.toJSON { + description = "Sharkey API key"; + actions = [ "*" ]; + indexes = [ "exampleurl_invalid---notes" ]; + expiresAt = null; + }; + in + '' + import json + + with subtest("Setting up Meilisearch API key and index"): + machine.wait_for_unit("meilisearch.service") + machine.wait_for_open_port(7700) + + json_body = '${createIndexPayload}' + create_index_result = json.loads(machine.succeed(f"curl -s -X POST 'http://localhost:7700/keys' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${meilisearchKey}' --data-binary '{json_body}'")) + machine.succeed(f"mkdir /run/secrets; echo 'MK_CONFIG_MEILISEARCH_APIKEY={create_index_result["key"]}' > /run/secrets/sharkey-env") + + with subtest("Testing Sharkey is running and listening to HTTP requests"): + machine.systemctl("restart sharkey") + machine.wait_for_open_port(3000) + + machine.succeed("curl --fail http://localhost:3000") + ''; + + meta.maintainers = with lib.maintainers; [ srxl ]; +} diff --git a/pkgs/by-name/sh/sharkey/package.nix b/pkgs/by-name/sh/sharkey/package.nix new file mode 100644 index 000000000000..2db89f5d1838 --- /dev/null +++ b/pkgs/by-name/sh/sharkey/package.nix @@ -0,0 +1,166 @@ +{ + stdenv, + lib, + fetchFromGitLab, + nixosTests, + bash, + cairo, + cctools, + ffmpeg-headless, + jemalloc, + makeWrapper, + nodejs, + pango, + pixman, + pkg-config, + pnpm_9, + python3, + vips, + xcbuild, +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "sharkey"; + version = "2025.4.3"; + + src = fetchFromGitLab { + domain = "activitypub.software"; + owner = "TransFem-org"; + repo = "Sharkey"; + tag = finalAttrs.version; + hash = "sha256-B268bSR5VFyJ/TaWg3xxpnP4oRj07XUpikJZ2Tb9FEY="; + fetchSubmodules = true; + }; + + pnpmDeps = pnpm_9.fetchDeps { + inherit (finalAttrs) pname version src; + hash = "sha256-S8LxawbtguFOEZyYbS1FQWw/TcRm4Z6mG7dUhfXbf1c="; + }; + + nativeBuildInputs = + [ + makeWrapper + nodejs + pkg-config + pnpm_9.configHook + python3 + ] + ++ (lib.optionals stdenv.hostPlatform.isDarwin [ + cctools + xcbuild + ]); + + buildInputs = [ + cairo + pango + pixman + vips + ]; + + buildPhase = '' + runHook preBuild + + # pnpm.fetchDeps doesn't run build scripts, so we need to run postinstall for this package otherwise the frontend + # build fails + pushd node_modules/.pnpm/node_modules/v-code-diff + pnpm postinstall + popd + + # rebuild some node modules that have native dependencies + export npm_config_nodedir=${nodejs} + + pushd node_modules/.pnpm/node_modules/re2 + pnpm rebuild + popd + + pushd node_modules/.pnpm/node_modules/sharp + pnpm run install + popd + + pushd node_modules/.pnpm/node_modules/canvas + pnpm run install + popd + + pnpm build + node scripts/trim-deps.mjs + pnpm prune --prod --ignore-scripts + + runHook postBuild + ''; + + installPhase = + let + binPath = lib.makeBinPath [ + bash + nodejs + pnpm_9 + ]; + libPath = lib.makeLibraryPath [ + ffmpeg-headless + jemalloc + stdenv.cc.cc + ]; + in + '' + runHook preInstall + + # adapted from repo dockerfile + # https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/Dockerfile?ref_type=heads + mkdir -p $out/sharkey + cp -r built fluent-emojis node_modules package.json pnpm-workspace.yaml $out/sharkey/ + + mkdir -p $out/sharkey/packages/backend + cp -r packages/backend/{assets,built,migration,node_modules,ormconfig.js,package.json} $out/sharkey/packages/backend/ + mkdir -p $out/sharkey/packages/backend/scripts + cp -r packages/backend/scripts/check_connect.js $out/sharkey/packages/backend/scripts/ + + mkdir -p $out/sharkey/packages/megalodon + cp -r packages/megalodon/{lib,node_modules,package.json} $out/sharkey/packages/megalodon/ + + mkdir -p $out/sharkey/packages/misskey-{bubble-game,js,reversi} + cp -r packages/misskey-bubble-game/{built,node_modules,package.json} $out/sharkey/packages/misskey-bubble-game/ + cp -r packages/misskey-js/{built,node_modules,package.json} $out/sharkey/packages/misskey-js/ + cp -r packages/misskey-reversi/{built,node_modules,package.json} $out/sharkey/packages/misskey-reversi/ + + mkdir -p $out/sharkey/packages/frontend{,-embed} + cp -r packages/frontend/assets $out/sharkey/packages/frontend/ + cp -r packages/frontend-embed/assets $out/sharkey/packages/frontend-embed/ + + mkdir -p $out/sharkey/tossface-emojis + cp -r tossface-emojis/dist $out/sharkey/tossface-emojis/ + + # create a wrapper script for running sharkey commands (ie. alias for pnpm run) + makeWrapper ${lib.getExe pnpm_9} $out/bin/sharkey \ + --chdir $out/sharkey \ + --add-flags run \ + --set-default NODE_ENV production \ + --prefix PATH : ${binPath} \ + --prefix LD_LIBRARY_PATH : ${libPath} + + runHook postInstall + ''; + + fixupPhase = '' + runHook preFixup + + # cleanup dangling symlinks in node_modules, referencing workspace packages we don't include in the final output + rm -r $out/sharkey/node_modules/.pnpm/node_modules/{sw,misskey-js-type-generator,frontend-shared} + + # remove packageManager line from package.json; tries to download a different pnpm version into $HOME otherwise + sed -i -e '9d' $out/sharkey/package.json + + runHook postFixup + ''; + + passthru.tests.sharkey = nixosTests.sharkey; + + meta = { + description = "Sharkish microblogging platform"; + homepage = "https://joinsharkey.org"; + changelog = "https://activitypub.software/TransFem-org/Sharkey/-/releases/${finalAttrs.version}"; + license = lib.licenses.agpl3Only; + platforms = with lib.platforms; linux ++ darwin; + mainProgram = "sharkey"; + maintainers = with lib.maintainers; [ srxl ]; + }; +})