From 520157cb2b5912eaec7e149eb4d0593d815f72e5 Mon Sep 17 00:00:00 2001 From: Nydragon Date: Mon, 14 Oct 2024 01:34:41 +0200 Subject: [PATCH] feat(rustypaste): create module instead of container and use newer version through flake --- flake.lock | 84 +++++++++++++++- flake.nix | 4 + home/scripts/default.nix | 3 +- hosts/raptus/configuration.nix | 52 ++++++++-- hosts/raptus/rustypaste/conf.nix | 90 ----------------- hosts/raptus/rustypaste/default.nix | 53 ---------- modules/nix/overlays.nix | 2 + options/server/default.nix | 1 + options/server/rustypaste.nix | 146 ++++++++++++++++++++++++++++ secrets/adguard-dns-list.age | Bin 668 -> 676 bytes secrets/couchdb.age | 20 ++-- secrets/navidrome.age | Bin 748 -> 748 bytes secrets/rustypaste.age | Bin 729 -> 717 bytes 13 files changed, 292 insertions(+), 163 deletions(-) delete mode 100644 hosts/raptus/rustypaste/conf.nix delete mode 100644 hosts/raptus/rustypaste/default.nix create mode 100644 options/server/rustypaste.nix diff --git a/flake.lock b/flake.lock index 2e974f1..a220a4a 100644 --- a/flake.lock +++ b/flake.lock @@ -296,6 +296,24 @@ "type": "github" } }, + "naersk": { + "inputs": { + "nixpkgs": "nixpkgs_4" + }, + "locked": { + "lastModified": 1721727458, + "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", + "owner": "nix-community", + "repo": "naersk", + "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, "nixos-hardware": { "locked": { "lastModified": 1728056216, @@ -360,6 +378,34 @@ "type": "github" } }, + "nixpkgs_4": { + "locked": { + "lastModified": 0, + "narHash": "sha256-HP89HZOT0ReIbI7IJZJQoJgxvB2Tn28V6XS3MNKnfLs=", + "path": "/nix/store/lryfc8mhk1czqsa421di2y5nzz5c3b8m-source", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_5": { + "locked": { + "lastModified": 1728492678, + "narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5633bcff0c6162b9e4b5f1264264611e950c8ec7", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "nysh": { "inputs": { "nixpkgs": [ @@ -435,7 +481,8 @@ "nixos-hardware": "nixos-hardware", "nixpkgs": "nixpkgs_2", "nysh": "nysh", - "rofi-obsidian": "rofi-obsidian" + "rofi-obsidian": "rofi-obsidian", + "rustypaste": "rustypaste" } }, "rust-overlay": { @@ -456,6 +503,41 @@ "type": "github" } }, + "rustypaste": { + "inputs": { + "naersk": "naersk", + "nixpkgs": "nixpkgs_5", + "rustypasteRepo": "rustypasteRepo" + }, + "locked": { + "dirtyRev": "561025fca47e63a4356cf8e41669ab823f033717-dirty", + "dirtyShortRev": "561025f-dirty", + "lastModified": 1728306155, + "narHash": "sha256-ewGx/X970ErBg6oKAaBKZd4pqnP2s0jeRHm9bIj8CzM=", + "type": "git", + "url": "file:///home/ny/Documents/opensource/rustypaste" + }, + "original": { + "type": "git", + "url": "file:///home/ny/Documents/opensource/rustypaste" + } + }, + "rustypasteRepo": { + "flake": false, + "locked": { + "lastModified": 1728306155, + "narHash": "sha256-mgB/arakSQu7mC/n7Oal/rsHdVxCD4NEgX7TAsCrCMI=", + "owner": "orhun", + "repo": "rustypaste", + "rev": "561025fca47e63a4356cf8e41669ab823f033717", + "type": "github" + }, + "original": { + "owner": "orhun", + "repo": "rustypaste", + "type": "github" + } + }, "systems": { "locked": { "lastModified": 1681028828, diff --git a/flake.nix b/flake.nix index 47965a0..1386342 100644 --- a/flake.nix +++ b/flake.nix @@ -42,6 +42,10 @@ url = "github:hyprwm/hyprlock"; inputs.nixpkgs.follows = "nixpkgs"; }; + + rustypaste = { + url = "/home/ny/Documents/opensource/rustypaste"; + }; }; outputs = diff --git a/home/scripts/default.nix b/home/scripts/default.nix index a9a1158..c609ffb 100644 --- a/home/scripts/default.nix +++ b/home/scripts/default.nix @@ -59,8 +59,7 @@ in getext = pkgs.writeScriptBin "ls | grep -E \"\.[a-zA-Z0-9]+$\" --only-matching | sort | uniq"; rpaste = writeFishBin "rpaste" '' - export $(cat ${config.age.secrets.rustypaste.path} | xargs) - ${pkgs.rustypaste-cli}/bin/rpaste -a "$AUTH_TOKEN" -s "https://rusty.ccnlc.eu/" $argv + ${pkgs.rustypaste-cli}/bin/rpaste -a "$(cat ${config.age.secrets.rustypaste.path})" -s "https://rusty.ccnlc.eu/" $argv ''; gentest = nixos-rebuild "gentest" "test"; diff --git a/hosts/raptus/configuration.nix b/hosts/raptus/configuration.nix index cc1619e..546f13f 100644 --- a/hosts/raptus/configuration.nix +++ b/hosts/raptus/configuration.nix @@ -3,7 +3,6 @@ lib, pkgs, config, - pubkeys, ... }: let @@ -14,14 +13,17 @@ in (modulesPath + "/profiles/qemu-guest.nix") ./disk-config.nix ./container-root.nix - ./rustypaste ./forgejo ./headscale.nix ]; age.secrets = { couchdb.file = ../../secrets/couchdb.age; - rustypaste.file = ../../secrets/rustypaste.age; + rustypaste = { + file = ../../secrets/rustypaste.age; + owner = "rustypaste"; + group = "rustypaste"; + }; }; boot.loader.grub = { @@ -29,10 +31,46 @@ in efiInstallAsRemovable = true; }; - modules.services.tailscale = { - enable = true; - tags = [ "server" ]; - extraFlags = [ "--accept-dns=false" ]; # Want to disable that since *server* can't access the private dns... for now + modules = { + server.rustypaste = { + enable = true; + authTokenFile = config.age.secrets.rustypaste.path; + settings = { + server = { + url = "https://rusty.ccnlc.eu"; + max_content_length = "50MB"; + timeout = "30s"; + expose_version = false; + expose_list = false; + handle_spaces = "replace"; + }; + paste = { + random_url = { + type = "petname"; + words = 3; + separator = "-"; + }; + default_extension = "txt"; + mime_blacklist = [ + "application/x-dosexec" + "application/java-archive" + "application/java-vm" + ]; + duplicate_files = false; + default_expiry = "1h"; + delete_expired_files = { + enabled = true; + interval = "1h"; + }; + }; + }; + }; + + services.tailscale = { + enable = true; + tags = [ "server" ]; + extraFlags = [ "--accept-dns=false" ]; # Want to disable that since *server* can't access the private dns... for now + }; }; services.headscale.enable = true; diff --git a/hosts/raptus/rustypaste/conf.nix b/hosts/raptus/rustypaste/conf.nix deleted file mode 100644 index 942e944..0000000 --- a/hosts/raptus/rustypaste/conf.nix +++ /dev/null @@ -1,90 +0,0 @@ -{ - config = { - refresh_rate = "1s"; - }; - server = { - address = "127.0.0.1:8000"; - url = "https://rusty.ccnlc.eu"; - workers = 4; - max_content_length = "50MB"; - upload_path = "./upload"; - timeout = "30s"; - expose_version = false; - expose_list = false; - handle_spaces = "replace"; - }; - landing_page = { - text = '' - ┬─┐┬ ┬┌─┐┌┬┐┬ ┬┌─┐┌─┐┌─┐┌┬┐┌─┐ - ├┬┘│ │└─┐ │ └┬┘├─┘├─┤└─┐ │ ├┤ - ┴└─└─┘└─┘ ┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘ - - Submit files via HTTP POST here: - curl -F 'file=@example.txt' - This will return the URL of the uploaded file. - - The server administrator might remove any pastes that they do not personally - want to host. - - If you are the server administrator and want to change this page, just go - into your config file and change it! If you change the expiry time, it is - recommended that you do. - - By default, pastes expire every hour. The server admin may or may not have - changed this. - - Check out the GitHub repository at https://github.com/orhun/rustypaste - Command line tool is available at https://github.com/orhun/rustypaste-cli - ''; - content_type = "text/plain; charset=utf-8"; - }; - paste = { - random_url = { - type = "petname"; - words = 2; - separator = "-"; - }; - default_extension = "txt"; - mime_override = [ - { - mime = "image/jpeg"; - regex = "^.*\.jpg$"; - } - { - mime = "image/png"; - regex = "^.*\.png$"; - } - { - mime = "image/svg+xml"; - regex = "^.*\.svg$"; - } - { - mime = "video/webm"; - regex = "^.*\.webm$"; - } - { - mime = "video/x-matroska"; - regex = "^.*\.mkv$"; - } - { - mime = "application/octet-stream"; - regex = "^.*\.bin$"; - } - { - mime = "text/plain"; - regex = "^.*\.(log|txt|diff|sh|rs|toml)$"; - } - ]; - mime_blacklist = [ - "application/x-dosexec" - "application/java-archive" - "application/java-vm" - ]; - duplicate_files = false; - default_expiry = "1h"; - delete_expired_files = { - enabled = true; - interval = "1h"; - }; - }; -} diff --git a/hosts/raptus/rustypaste/default.nix b/hosts/raptus/rustypaste/default.nix deleted file mode 100644 index 054d156..0000000 --- a/hosts/raptus/rustypaste/default.nix +++ /dev/null @@ -1,53 +0,0 @@ -{ - pkgs, - config, - lib, - ... -}: -{ - virtualisation.oci-containers.containers."rustypaste" = { - image = "orhunp/rustypaste:latest"; - environment = { - "RUST_LOG" = "debug"; - }; - environmentFiles = [ config.age.secrets.rustypaste.path ]; - volumes = [ - "${(pkgs.formats.toml { }).generate "conf" (import ./conf.nix)}:/app/config.toml:ro" - "test_rustypaste-data:/app/upload:rw" - ]; - ports = [ "8000:8000/tcp" ]; - log-driver = "journald"; - extraOptions = [ - "--network-alias=rustypaste" - "--network=test_default" - ]; - }; - systemd.services."podman-rustypaste" = { - serviceConfig = { - Restart = lib.mkOverride 500 "always"; - }; - after = [ - "podman-network-test_default.service" - "podman-volume-test_rustypaste-data.service" - ]; - requires = [ - "podman-network-test_default.service" - "podman-volume-test_rustypaste-data.service" - ]; - partOf = [ "podman-compose-test-root.target" ]; - wantedBy = [ "podman-compose-test-root.target" ]; - }; - - systemd.services."podman-volume-test_rustypaste-data" = { - path = [ pkgs.podman ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - podman volume inspect test_rustypaste-data || podman volume create test_rustypaste-data - ''; - partOf = [ "podman-compose-test-root.target" ]; - wantedBy = [ "podman-compose-test-root.target" ]; - }; -} diff --git a/modules/nix/overlays.nix b/modules/nix/overlays.nix index 09113a8..c0f4de9 100644 --- a/modules/nix/overlays.nix +++ b/modules/nix/overlays.nix @@ -1,4 +1,5 @@ { + inputs, inputs', config, lib, @@ -37,5 +38,6 @@ hyprlock = inputs'.hyprlock.packages.hyprlock; }) + inputs.rustypaste.overlays.default ]; } diff --git a/options/server/default.nix b/options/server/default.nix index c73374b..d5e34af 100644 --- a/options/server/default.nix +++ b/options/server/default.nix @@ -3,5 +3,6 @@ ./paperless-ngx ./navidrome.nix ./rsync-daemon + ./rustypaste.nix ]; } diff --git a/options/server/rustypaste.nix b/options/server/rustypaste.nix new file mode 100644 index 0000000..dc71d98 --- /dev/null +++ b/options/server/rustypaste.nix @@ -0,0 +1,146 @@ +{ + config, + lib, + pkgs, + options, + ... +}: +let + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; + cfg = config.modules.server.rustypaste; + opts = options.modules.server.rustypaste; + toml = pkgs.formats.toml { }; +in +{ + options.modules.server.rustypaste = { + enable = mkEnableOption "rustypaste, a pastebin alternative"; + + authTokenFile = mkOption { + type = types.str; + default = ""; + }; + + deleteTokenFile = mkOption { + type = types.str; + default = ""; + }; + + port = mkOption { + type = types.port; + default = 8000; + description = "The port rustypaste should listen on."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the specified port."; + }; + + address = mkOption { + type = types.nonEmptyStr; + default = "127.0.0.1"; + description = "The address rustypaste should listen on."; + }; + + package = mkOption { + type = types.package; + default = pkgs.rustypaste; + }; + + user = mkOption { + type = types.str; + default = "rustypaste"; + description = "User account under which rustypaste runs."; + }; + + group = mkOption { + type = types.str; + default = "rustypaste"; + description = "Group under which rustypaste runs."; + }; + + settings = lib.mkOption { + inherit (toml) type; + default = { + config.refresh_rate = "1y"; + server = { + address = "${cfg.address}:${toString cfg.port}"; + max_content_length = "10MB"; + upload_path = cfg.dataDir; + }; + paste = { + default_extension = "txt"; + }; + }; + defaultText = "Refer to https://github.com/orhun/rustypaste/blob/master/config.toml"; + apply = lib.recursiveUpdate opts.settings.default; + }; + + dataDir = lib.mkOption { + type = types.nonEmptyStr; + default = "/var/lib/rustypaste"; + description = "Where rustypaste stores uploaded files."; + }; + + settingsFile = lib.mkOption { + type = types.path; + default = toml.generate "rustypaste.toml" cfg.settings; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0700 ${cfg.user} ${cfg.group}" + ]; + + systemd.services.rustypaste = { + enable = true; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + "CONFIG" = cfg.settingsFile; + "AUTH_TOKENS_FILE" = mkIf (cfg.authTokenFile != "") cfg.authTokenFile; + "DELETE_TOKENS_FILE" = mkIf (cfg.deleteTokenFile != "") cfg.deleteTokenFile; + }; + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.dataDir; + ExecStart = "${cfg.package}/bin/rustypaste"; + Restart = "on-failure"; + PrivateDevices = true; + PrivateTmp = true; + ProtectSystem = "full"; + ReadWritePaths = cfg.dataDir; + ReadOnlyPaths = lib.concatStringsSep " " [ + cfg.settingsFile + cfg.authTokenFile + cfg.deleteTokenFile + ]; + IPAddressAllow = "any"; + }; + }; + + users.users = mkIf (cfg.user == "rustypaste") { + rustypaste = { + useDefaultShell = true; + group = cfg.group; + isSystemUser = true; + }; + }; + + users.groups = mkIf (cfg.group == "rustypaste") { + rustypaste = { }; + }; + }; +} diff --git a/secrets/adguard-dns-list.age b/secrets/adguard-dns-list.age index 6649fa46c58a54995484308a725530093b7432b9..59aacd74cfb1928b602c7222a26f6b4834f3453a 100644 GIT binary patch delta 605 zcmV~$OK8&o002;&OlAgxFc=78r{aXRBu&##5R|5Antsz{ZIcdVG^R;vk~VEVO^3Qo zK?Z{OLp_L6neI@=24i}h=t2D8VN5~U#uPt7E+p^s;Q)4vpf_OS$d?VMBHgk2U|*B7$48H zwR&C%CD1V7J?b(H*;^6dK){E(ct5G;BCaInkDsE^{HR%y7~-TU5JiJVZCeO81Zw4RK+gn zR;{hYph*PRW2zZ zZnglVB9#UPN7@D{H*$(o0yT#$2KXEf%2BFZYx@|qWa6}qWNE-%T#1+nmdRAqu?xV>75XzK#pj1FU0?sV9tpJ z`?ANHegn1McF+H6Z!B+LJiE33)#kvrgF9ZRtMfmuDo=W%9vyM^WB;ss+gX_};2(MCO#~!3QFq6cGd&9#jy-AqXD4xLp(k*+JY-_`Z9Cw+81<_KKKPaU2Tf z@Gj#+6Oygss+vtSS%-)>Jd~-Yy{4IS>xfwkL?>9`2hu*tVF(xiQX$(`0a=0&B$5n?Fs@IhLp~u&oqW-POBgImpqR|5&0M1zBbCfl zSSO_nf?^Y5+W_r!GyqwXnvp}$WC1GVXUmk<%$r`PTr?4Z%U}j0xei^MMh%Ogul)*g zG-kOozFw)=1=;clo@2XpVaCZ;^tvITNs8TOPLY%;(sUpkqaemnW-^_$8RumWp*@CI z{IVGqLAS(|>TOzAV3VMui5qA8gT0v2p~%ryYk2X$ieS)9we)jh2`W2zf?e~LAd&m0L=H!0|#=+c~ zzTZ3dj2iUa2aQ)x#)iz*@x@-ZcVR@cg%uL8UL0Ip;5K(5>*qI4+lcCp gpN-V%cRvy$*LMDlP_MfK!&~3q{9P7TzAmKx15yatGXMYp diff --git a/secrets/couchdb.age b/secrets/couchdb.age index 4168bc2..5bad6c1 100644 --- a/secrets/couchdb.age +++ b/secrets/couchdb.age @@ -1,11 +1,11 @@ age-encryption.org/v1 --> ssh-ed25519 biwZXw M19MPetxrj5viO9n3YQ80hEObhyJg5IZnNycR3Wzqk4 -YBOWqQzb+zU8tSwEcrsr/ocPj6kzSly2wbJq0WK+gDM --> ssh-ed25519 b3HlPA Z01OXca+e/XNFR0V9hPlCMZaQUdmbDfIqhQvLSfF5is -bjPwLeKSzatDDIjAaKh1q4ZdgEvHB82EyC4hSzS4qXE --> ssh-ed25519 cdUqUg 97W5cmHE/PS0MAlel2MDdzYJVinRVxBkigbV+c/xLRk -UyMUJYb+782FZEbuCcn4xj62bCLaYSBLD5714xpQN4c --> ssh-ed25519 a1hgwg +kQW6lvFa/sTuU91My1NepIasAFnscjluc3z3zyHWws -rx/jQxCiC6sjGeXYeZcW0+UxkQr8uHNJKCGPxvH9GqQ ---- VGD7NEIKcPMDhDKCGXKP+kXXf1YIPIK/y64k5e4YFTs -bwXm ws hF1ɭ+Zܠ}LY Zݍfgq]̃txۯ`w)Y%u^_v8QZfqu:g1 V!’A[e{Tn -f \ No newline at end of file +-> ssh-ed25519 biwZXw iMQ+wyhe0edDZpfFTcvg2zKzdGt6aHVE/tQBqbtCcUc +hAizLz8grjdYwjOnLSeEtp4kxA4MjRPywRs1BHrDSdo +-> ssh-ed25519 b3HlPA G1inkewMY0dv885snGeJqWRVVXsaCK35iSBXVA6OEAc +/VgBYT9W4ic6piJM5QTvmaAJtWDbzsXyP7M3GOilW8I +-> ssh-ed25519 cdUqUg UzHj6mpJ1qNWA02x37JFhNEq/VdOP6k3p4ZtBY68Hjo +4trOjzntsQ05C07SVSfO/gGoUTR2owyj6VnZPwLi4eE +-> ssh-ed25519 a1hgwg pMJOkvg4Lg5tbY+nb9eqGPMweLwzxPxtvkhaOY8s3xk +7rpmGdN84Lq/yU+FQCnaaHJOmvUhJgboFDmXgO4B7lc +--- kZ3ul3tAFvY769eSvcTei2KdrGyHKCDh+OdOAHk3NoU +הS*DWv=!mR 921Qk!h0ǃ]_ݞ]j6 1764HTؒJűljHoO@ ]9SUq'n \ No newline at end of file diff --git a/secrets/navidrome.age b/secrets/navidrome.age index 5e4c32b0f8e3559f747e69184bb0646882736471..f4e16872d27949f22ad728d94a8c62ed300f6810 100644 GIT binary patch delta 677 zcmV;W0$Tm-1?&ZoEPr@3F-1aWc}8e2cR@-*b!{+JcV<{@O+!skN@aFqY)4~mc2hEH zN^f>DZwhWNFIROzGDTK0K~yv_Sb9iIcUWgJGc!?8RZ>JjNl|BSMl(iFXfMpAleaZGq*L|0mL zPjoM5k?|K-WOYtzN=k4;Rx?>gQExPBZc<`fOp=5~&M#YpJs=h3A77 z4>p--gub(s1F78>)gKw%GVgSrrunHl9-0aI delta 677 zcmV;W0$Tm-1?&ZoEPpdrQ7<-DZA>sLO;lnsPEKxPT5CdAa!N>XGgL}iS3zt~I4e&> zN^C-RNeWSNQdvfDIWbjLZ!u^=WomClVn%sSNlk5LN{`-OHgG@c|kWsH&Hk>RZmMpbwyTCL{(HwD{nGNFiA9%UjY|?bTUa~ zcu;axFjGY{OmtC8S2$^LMRRK}PH|FtT3U8#dUQB(SwU55Nk>r%ZCO?;R!B5KT1R(N zS59ejLQhR(G;CK-NOMVcaCu{AD`_`xR##~>MS5=vEiEk|c~U`WM|fpcYcE7eQA00! zM@LskX-_yoD|tj!T6K6-HA`}TGfs0cMp`p-3SX78dQL>5Tq3mT7QE{BPfQpz+&sU< zyiLp)KOX`_q5U@+<*mwQ8|#|f$t@FA|tKVl@nN< zT_Xg;AXU)b860bbA7d(0gsjKYa)aX35E!pEl$G?UVm<}}tGQu^c_GF!RK5E%OGXA^ zjLGYFF?a8^tI%pI^5xp6kZJLiCJO#&!3m+?P8G^V1-54xyJ&P18gIc29`NBT%T-WBq*^=j60OxYeDD=RGPpFLlD}4 z5Edf?>#8ngQ!LJtMZg$^OYxDGW4d)gr1D8ZvV>U6#EI3AyQW9a6DMqj2O6(dF zLDnT6>(flq9uFH(spg$(yyp6=T1^CANcvfb8;hPmb=`38>`*w`^rHkrjMn+hcE-ur zgHcB5SXj`S&^A!@jn2fbuV!s!6VU9)Z#a||g+Aj>t)UD_QQ+_tNl%7u(Pr1Gx6mtZ z83Q$nW@szV6n@CB3ef5j^7+j|XWrU_aaTzEnoe{*vf;YwqC07aW(rI$j3sxF%c`E=s>55Im4 vE*(0*4<6jv+WIShxM>ya!OQpVy}QT39{G-;bOO`7J_JU|Z| zTn^!+f)1j%h>L@6PIBI$%i*Al;@~NVqq^Pb_Z_T!UD>_19fdq3IHWczCLmVED)1#* zbAxIz9tm2Ann_7WXI46LI&2n1UE0Dd&c!594nrO=97Z*eY1A}I?rhj?jZ1=Q7$NC7 z*}{#S?;C~D5G4qN%BdKzTIC!bp>Ur;NV$(w-4@l^s7a#D=*}Eey?_%s!dRK6Y(pt? z9Xu=X}V-DDs9KaDYBEyBVt;I*t)GR4>GIUVoFD*M6h|jP1HP6fce@ih+%eN z&4?=LI{6?(7f@&n^(Y&g;h>R6)i_~*?G8jO)G--TI>TNdL3y&Qn2OGDlf>8)9V{9Mv5c(MQb?}=xpbEoJTvUl@n@$2yL=$rKU@c!2PV*kv$|5)