{ inputs = { nixpkgs.url = "github:nixos/nixpkgs/release-22.05"; flake-utils.url = "github:numtide/flake-utils/master"; rust-overlay = { url = "github:oxalica/rust-overlay/master"; inputs = { flake-utils.follows = "flake-utils"; nixpkgs.follows = "nixpkgs"; }; }; naersk = { url = "github:nmattia/naersk/master"; inputs.nixpkgs.follows = "nixpkgs"; }; arion = { url = "github:hercules-ci/arion/master"; inputs.nixpkgs.follows = "nixpkgs"; }; }; description = "thulani discord bot"; outputs = { self, nixpkgs, flake-utils, ... } @ inputs: let mkBuildToolchain = pkgs: pkgs.rust-bin.nightly."2022-11-20".minimal; deps = pkgs: with pkgs; [ openssl pkgconfig libopus postgresql ]; mkPkg = pkgs: let buildToolchain = mkBuildToolchain pkgs; naersk = pkgs.callPackage inputs.naersk { cargo = buildToolchain; rustc = buildToolchain; }; inherit (pkgs) lib; in naersk.buildPackage { pname = "thulani"; version = self.rev or "dirty"; src = lib.cleanSourceWith { src = lib.cleanSource ./.; filter = path: ty: with builtins; with lib; let baseName = baseNameOf "${path}"; extMatch = match "((.*\\.?)*\\.)?(.*)" baseName; extension = elemAt extMatch 2; parent = baseNameOf (dirOf "${path}"); isCargoFile = baseName == "Cargo.toml" || baseName == "Cargo.lock"; isCargoConfig = parent == ".cargo" && baseName == "config.toml"; in ty == "directory" || isCargoFile || isCargoConfig || extension == "rs" || extension == "sql" || extension == "pest"; }; nativeBuildInputs = deps pkgs; remapPathPrefix = true; }; in (flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; overlays = [ (import inputs.rust-overlay) ]; }; dbInit = (pkgs.writeShellScript "thulani-dbinit" (builtins.readFile ./nix/dbinit.sh)).overrideAttrs (final: prev: { checkPhase = '' ${pkgs.shellcheck}/bin/shellcheck $out ''; }); devToolchain = (mkBuildToolchain pkgs).override { extensions = [ "rust-src" "rust-analyzer" "clippy" "rust-docs" "rustfmt" ]; }; pkg = mkPkg pkgs; in { devShells.default = pkgs.mkShell { buildInputs = (with pkgs; [ devToolchain shellcheck ]) ++ (deps pkgs); RUST_SRC_PATH = "${devToolchain}/lib/rustlib/src/rust"; }; packages = { default = pkg; inherit dbInit; }; apps.default = { type = "app"; program = "${pkg}/bin/thulani"; }; }) // { hydraJobs = { inherit (self.packages) x86_64-linux; }; overlays.default = final: prev: let withRust = ((import inputs.rust-overlay) final prev); in withRust // { thulani = mkPkg (final // withRust); }; nixosConfigurations.test = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ self.nixosModules.default "${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix" ({ config, lib, pkgs, ... }: { nixpkgs.overlays = [ self.overlays.default ]; environment.systemPackages = with pkgs; [ (writeShellScriptBin "jl" '' journalctl -eu thulani | cat '') (writeShellScriptBin "s" '' exec systemctl status thulani $@ '') ]; users = { mutableUsers = false; users.root.hashedPassword = lib.mkForce ""; users.test = { password = lib.mkForce "test"; group = "test"; isNormalUser = true; extraGroups = [ "wheel" "sudo" ]; }; groups.test = {}; }; security.sudo.wheelNeedsPassword = false; virtualisation = { cores = 8; graphics = false; diskSize = 32*1024; memorySize = 12*1024; writableStoreUseTmpfs = false; }; services.thulani = { enable = true; environment = { STEAM_API_KEY = ""; SHEETS_API_KEY = ""; SPREADSHEET_ID = ""; THULANI_CLIENT_ID = "1"; THULANI_TOKEN = ""; VOICE_CHANNEL = "1"; OWNER_ID = "1"; TARGET_GUILD = "1"; }; postgres = { enable = true; }; }; }) ]; }; nixosModules.default = { pkgs, lib, config, ... }: let cfg = config.services.thulani; in { options.services.thulani = with lib; with lib.types; { enable = mkEnableOption "thulani"; package = mkOption { description = "thulani derivation to use"; type = package; default = pkgs.thulani; }; environment = mkOption { description = "literal environment to include"; type = attrs; }; envFiles = mkOption { description = "environment files to include"; type = listOf path; default = []; }; user = mkOption { description = "user to run service as"; type = str; default = "thulani"; }; group = mkOption { description = "group to run service as"; type = str; default = "thulani"; }; userIdMappingFile = mkOption { description = "user id mapping file"; type = nullOr path; default = null; }; restrictFile = mkOption { description = "restrict file"; type = nullOr path; default = null; }; postgres = mkOption { description = "local postgres server with automatic setup"; type = submodule { options = { enable = mkEnableOption "postgres"; db = mkOption { description = "db name"; type = str; default = "memes"; }; }; }; }; }; config = lib.mkIf cfg.enable { services.thulani.environment = { RUST_BACKTRACE = lib.mkDefault "1"; MAX_HIST = lib.mkDefault "30"; DEFAULT_HIST = lib.mkDefault "5"; MAX_SHEET_COLUMN = lib.mkDefault "ZZZ"; YTDL = lib.mkDefault "${pkgs.yt-dlp}/bin/yt-dlp"; FFMPEG = lib.mkDefault "${pkgs.ffmpeg_4}/bin/ffmpeg"; RESTRICT = lib.mkIf (cfg.restrictFile != null) "${cfg.restrictFile}"; USER_ID_MAPPING = lib.mkIf (cfg.userIdMappingFile != null) "${cfg.userIdMappingFile}"; DATABASE_URL = lib.mkIf cfg.postgres.enable "postgres:///${cfg.postgres.db}?user=${cfg.user}&host=/var/run/postgresql"; }; systemd.services.thulani = { description = "thulani bot"; wantedBy = [ "multi-user.target" ]; bindsTo = [ "network-online.target" ] ++ lib.optional cfg.postgres.enable "postgresql.service"; after = [ "network-online.target" ] ++ lib.optional cfg.postgres.enable "postgresql.service"; inherit (cfg) environment; unitConfig = { StartLimitBurst = 3; StartLimitIntervalSec = "1m"; }; serviceConfig = { Type = "exec"; ExecStart = "${cfg.package}/bin/thulani"; ExecStartPre = let inherit (self.packages.${pkgs.system}) dbInit; in lib.mkIf cfg.postgres.enable "+${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} -- ${dbInit} ${config.services.postgresql.package}/bin/psql ${cfg.postgres.db} ${cfg.user}"; EnvironmentFile = cfg.envFiles; DynamicUser = true; User = cfg.user; Group = cfg.group; Restart = "always"; RestartSec = "10s"; TimeoutStopSec = "10s"; MemoryHigh = "200M"; MemoryMax = "300M"; ProtectSystem = "strict"; ProtectProc = "noaccess"; ProtectHome = true; ProtectHostname = true; ProtectClock = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectKernelLogs = true; ProtectControlGroups = true; PrivateDevices = true; PrivateUsers = true; PrivateMounts = true; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; MemoryDenyWriteExecute = true; LockPersonality = true; NoNewPrivileges = true; KeyringMode = "private"; SystemCallFilter = "@system-service"; SystemCallErrorNumber = "EPERM"; }; }; services.postgresql = lib.mkIf cfg.postgres.enable { enable = true; authentication = '' local ${cfg.postgres.db} ${cfg.user} ident ''; }; }; }; } ); }