diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index e62b828..0000000 --- a/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true -charset = utf-8 diff --git a/.envrc b/.envrc deleted file mode 100644 index 0f94eed..0000000 --- a/.envrc +++ /dev/null @@ -1,2 +0,0 @@ -# shellcheck shell=bash -use flake diff --git a/.forgejo/workflows/action.yml b/.forgejo/workflows/action.yml deleted file mode 100644 index 2c61087..0000000 --- a/.forgejo/workflows/action.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Test action - -on: - workflow_dispatch: - push: - branches: - - main - -jobs: - kaas: - runs-on: nix - steps: - - name: Echo - run: | - nix --version diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 6313b56..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto eol=lf diff --git a/.gitignore b/.gitignore index 3cb44c3..87a3018 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,2 @@ -# ---> Nix -# Ignore build outputs from performing a nix-build or `nix build` command result -result-* - -# Ignore automatically generated direnv output -.direnv - +*.qcow2 diff --git a/.jq/format.jq b/.jq/format.jq deleted file mode 100644 index 5c65495..0000000 --- a/.jq/format.jq +++ /dev/null @@ -1,34 +0,0 @@ -def RESET: "0"; -def BOLD: "1"; -def DIM: "2"; -def ITALIC: "3"; -def UNDERLINE: "4"; -def BLINKING: "5"; -def INVERSE: "7"; -def HIDDEN: "8"; -def STRIKETHROUGH: "9"; -def RESET_FONT: "22"; - -def BLACK: 0; -def RED: 1; -def GREEN: 2; -def YELLOW: 3; -def BLUE: 4; -def MAGENTA: 5; -def CYAN: 6; -def WHITE: 7; -def DEFAULT: 9; - -def foreground(color): 30 + color; -def background(color): 40 + color; -def bright(color): 60 + color; - -def escape(options): - (if ((options|type) == "array") then options else [options] end) as $o - | "\u001b[\($o | map(tostring) | join(";"))m"; - -def style(options): escape(options) + . + escape([RESET]); - -def to_title: - (.|ascii_upcase) as $str - | escape([BOLD, foreground(BLACK), background(WHITE)]) + " " + $str + " " + escape([RESET]); diff --git a/.jq/table.jq b/.jq/table.jq deleted file mode 100644 index 04f80c6..0000000 --- a/.jq/table.jq +++ /dev/null @@ -1,58 +0,0 @@ -import "format" as _ {search:"./"}; - -def n_max(limit): - if . > limit then limit else . end; - -def n_min(limit): - if . < limit then limit else . end; - -def pad_right(width): - (. | tostring) as $s - | ($s | length) as $l - | ((width - $l) | n_min(0)) as $w - | ($s + (" " * $w)); - -def to_cells(sizes; fn): - to_entries - | map( - (sizes[.key]) as $size - | (" " + .value) - | pad_right($size + 2) - | fn // . - ); - -def to_cells(sizes): to_cells(sizes; null); - -def to_line(left; joiner; right): - [left, .[0], (.[1:] | map([joiner, .]) ), right] | flatten | join(""); - -def create(data; header_callback; cell_callback): - (data[0] | keys_unsorted) as $keys - | (data | map(to_entries | map(.value))) as $rows - | ([$keys] + $rows) as $cells - | ( - $keys # Use keys so that we have an array of the correct size - | to_entries - | map( - (.key) as $i - | $cells - | map(.[$i] | length) - | max - ) - ) as $column_sizes - | ( - [ - ($column_sizes | map("═" * (. + 2)) | to_line("╔"; "╤"; "╗")), - ($keys | to_cells($column_sizes; header_callback) | to_line("║"; "│"; "║")), - ($rows | map([ - ($column_sizes | map("─" * (. + 2)) | to_line("╟"; "┼"; "╢")), - (. | to_cells($column_sizes; cell_callback) | to_line("║"; "│"; "║")) - ])), - ($column_sizes | map("═" * (. + 2)) | to_line("╚"; "╧"; "╝")) - ] - | flatten - | join("\n") - ); - -def create(data; header_callback): create(data; header_callback; null); -def create(data): create(data; _::style(_::BOLD); null); diff --git a/.just/machine.just b/.just/machine.just deleted file mode 100644 index 3cb4587..0000000 --- a/.just/machine.just +++ /dev/null @@ -1,20 +0,0 @@ -@_default: list - -[doc('List machines')] -@list: - ls -1 ../systems/x86_64-linux/ - -[doc('Update target machine')] -[no-exit-message] -@update machine: - echo "Checking vars" - cd .. && just vars _check {{ machine }} - echo "" - just assert '-d "../systems/x86_64-linux/{{ machine }}"' "Machine {{ machine }} does not exist, must be one of: $(ls ../systems/x86_64-linux/ | sed ':a;N;$!ba;s/\n/, /g')" - nixos-rebuild switch -L --sudo --target-host {{ machine }} --flake ..#{{ machine }} --log-format internal-json -v |& nom --json - -[doc('Check if target machine builds')] -[no-exit-message] -@check machine: - just assert '-d "../systems/x86_64-linux/{{ machine }}"' "Machine {{ machine }} does not exist, must be one of: $(ls ../systems/x86_64-linux/ | sed ':a;N;$!ba;s/\n/, /g')" - nix build ..#nixosConfigurations.{{ machine }}.config.system.build.toplevel diff --git a/.just/users.just b/.just/users.just deleted file mode 100644 index e798cc3..0000000 --- a/.just/users.just +++ /dev/null @@ -1,101 +0,0 @@ -set unstable := true -set quiet := true - -_default: - just --list users - -[doc('List available users')] -[script] -list: - cd .. && just vars get ulmo zitadel/users | jq -r -C ' - import ".jq/table" as table; - import ".jq/format" as f; - - fromjson - | to_entries - | sort_by(.key) - | map( - (.key|f::to_title) + ":\n" - + table::create( - .value - | to_entries - | sort_by(.key) - | map({username:.key} + .value) - ) - ) - | join("\n\n┄┄┄\n\n") - '; - -[doc('Add a new user')] -[script] -add: - exec 5>&1 - - pad () { [ "$#" -gt 1 ] && [ -n "$2" ] && printf "%$2.${2#-}s" "$1"; } - - input() { - local label=$1 - local value=$2 - - local res=$(gum input --header "$label" --value "$value") - echo -e "\e[2m$(pad "$label" -11)\e[0m$res" >&5 - echo $res - } - - data=`cd .. && just vars get ulmo zitadel/users | jq 'fromjson'` - - # Gather inputs - org=` - jq -r 'to_entries | map(.key)[]' <<< "$data" \ - | gum choose --header 'Which organisation to save to?' --select-if-one - ` - username=`input 'user name' ''` - email=`input 'email' ''` - first_name=`input 'first name' ''` - last_name=`input 'last name' ''` - - user_exists=`jq --arg 'org' "$org" --arg 'username' "$username" '.[$org][$username]? | . != null' <<< "$data"` - - if [ "$user_exists" == "true" ]; then - gum confirm 'User already exists, overwrite it?' --padding="1 1" || exit 0 - fi - - next=` - jq \ - --arg 'org' "$org" \ - --arg 'username' "$username" \ - --arg 'email' "$email" \ - --arg 'first_name' "$first_name" \ - --arg 'last_name' "$last_name" \ - --compact-output \ - '.[$org] += { $username: { email: $email, firstName: $first_name, lastName: $last_name } }' \ - <<< $data - ` - - gum spin --title "saving..." -- echo "$(cd .. && just vars set ulmo 'zitadel/users' "$next")" - -[doc('Remove a new user')] -[script] -remove: - data=`cd .. && just vars get ulmo zitadel/users | jq fromjson` - - # Gather inputs - org=` - jq -r 'to_entries | map(.key)[]' <<< "$data" \ - | gum choose --header 'Which organisation?' --select-if-one - ` - user=` - jq -r --arg org "$org" '.[$org] | to_entries | map(.key)[]' <<< "$data" \ - | gum choose --header 'Which user?' --select-if-one - ` - - next=` - jq \ - --arg 'org' "$org" \ - --arg 'user' "$user" \ - --compact-output \ - 'del(.[$org][$user])' \ - <<< $data - ` - - gum spin --title "saving..." -- echo "$(cd .. && just vars set ulmo 'zitadel/users' "$next")" diff --git a/.just/vars.just b/.just/vars.just deleted file mode 100644 index 2ae9a44..0000000 --- a/.just/vars.just +++ /dev/null @@ -1,108 +0,0 @@ -set unstable := true -set quiet := true - -base_path := justfile_directory() + "/systems/x86_64-linux" - -_default: - just --list vars - -[doc('List all vars of {machine}')] -list machine: - sops decrypt {{ base_path }}/{{ machine }}/secrets.yml - -[doc('Edit all vars of {machine} in your editor')] -edit machine: - sops edit {{ base_path }}/{{ machine }}/secrets.yml - -[doc('Set var {value} by {key} for {machine}')] -@set machine key value: - sops set {{ base_path }}/{{ machine }}/secrets.yml "$(printf '%s\n' '["{{ key }}"]' | sed -E 's#/#"]["#g; s/\["([0-9]+)"\]/[\1]/g')" "\"$(echo '{{ value }}' | sed 's/\"/\\\"/g')\"" - - git add {{ base_path }}/{{ machine }}/secrets.yml - git commit -m 'chore(secrets): set secret "{{ key }}" for machine "{{ machine }}"' -- {{ base_path }}/{{ machine }}/secrets.yml > /dev/null - - echo "Done" - -[doc('Get var by {key} from {machine}')] -get machine key: - sops decrypt {{ base_path }}/{{ machine }}/secrets.yml | yq ".$(echo "{{ key }}" | sed -E 's/\//./g') // \"\"" - -[doc('Remove var by {key} for {machine}')] -remove machine key: - sops unset {{ base_path }}/{{ machine }}/secrets.yml "$(printf '%s\n' '["{{ key }}"]' | sed -E 's#/#"]["#g; s/\["([0-9]+)"\]/[\1]/g')" - - git add {{ base_path }}/{{ machine }}/secrets.yml - git commit -m 'chore(secrets): removed secret "{{ key }}" from machine "{{ machine }}"' -- {{ base_path }}/{{ machine }}/secrets.yml > /dev/null - - echo "Done" - -[doc('Generate var values for {machine}')] -[script] -generate machine: - for key in $(nix eval --apply 'builtins.attrNames' --json ..#nixosConfigurations.{{ machine }}.config.sops.secrets | jq -r '.[]'); do - # Skip if we already have a value - [ $(just vars get "{{ machine }}" "$key" | jq -r) ] && continue - - just vars _rotate "{{ machine }}" "$key" - done - -[doc('Regenerate var values for {machine}')] -[script] -_rotate machine key: - # Exit if there's no script - [ -f "{{ justfile_directory() }}/script/{{ key }}" ] || exit 0 - - echo "Executing script for {{ key }}" - just vars set "{{ machine }}" "{{ key }}" "$(cd -- "$(dirname "{{ justfile_directory() }}/script/{{ key }}")" && source "./$(basename "{{ key }}")")" - -[script] -check: - cd .. - - for machine in $(ls {{ base_path }}); do - just vars _check "$machine" - done - -[no-exit-message] -[script] -_check machine: - # If the default nix file is missing, - # we can skip this folder as we are - # missing the files used to compare - # the defined vs the configured secrets - if [ ! -f "{{ base_path }}/{{ machine }}/default.nix" ]; then - printf "\r• %-8sskipped\n" "{{ machine }}" - exit 0 - fi - - exec 3< <(jq -nr \ - --rawfile defined <(nix eval --json ..#nixosConfigurations.{{ machine }}.config.sops.secrets 2>/dev/null) \ - --rawfile configured <([ -f "{{ base_path }}/{{ machine }}/secrets.yml" ] && sops decrypt {{ base_path }}/{{ machine }}/secrets.yml | yq '.' || echo "{}") \ - ' - [ $configured | fromjson | paths(scalars) | join("/") ] as $conf - | $defined - | fromjson - | map(.key | select(. | IN($conf[]) | not)) - | unique - | .[] - ') - - pid=$! # Process Id of the previous running command - spin='⠇⠋⠙⠸⢰⣠⣄⡆' - - i=0 - while kill -0 $pid 2>/dev/null - do - i=$(( (i+1) %${#spin} )) - printf "\r${spin:$i:1} %s" "{{ machine }}" - sleep .1 - done - - mapfile -t missing <&3 - - if (( ${#missing[@]} > 0 )); then - printf '\r✗ %-8smissing %d secret(s):\n%s\n' "{{ machine }}" "${#missing[@]}" "$(printf -- ' %s\n' "${missing[@]}")" - exit 1 - else - printf "\r✓ %-8sup to date\n" "{{ machine }}" - fi diff --git a/.justfile b/.justfile deleted file mode 100644 index cee0db9..0000000 --- a/.justfile +++ /dev/null @@ -1,36 +0,0 @@ -@_default: - just --list --list-submodules - -[doc('Manage vars')] -mod vars '.just/vars.just' - -[doc('Manage users')] -mod users '.just/users.just' - -[doc('Manage machines')] -mod machine '.just/machine.just' - -[doc('Show information about project')] -@show: - echo "show" - -[doc('update the flake dependencies')] -@update: - nix flake update - git commit -m 'chore: update dependencies' -- ./flake.lock > /dev/null - echo "Done" - -[doc('Introspection on flake output')] -@select key: - nix eval --show-trace --json .#{{ key }} | jq . - - - -#=============================================================================================== -# Utils -#=============================================================================================== -[no-exit-message] -[no-cd] -[private] -@assert condition message: - [ {{ condition }} ] || { echo -e 1>&2 "\n\x1b[1;41m Error \x1b[0m {{ message }}\n"; exit 1; } diff --git a/.sops.yaml b/.sops.yaml deleted file mode 100644 index 1faf874..0000000 --- a/.sops.yaml +++ /dev/null @@ -1,13 +0,0 @@ -keys: - - &ulmo_1 age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq - - &ulmo_2 age1ewes0f5snqx3sh5ul6fa6qtxzhd25829v6mf5rx2wnheat6fefps5rme2x - - &manwe_1 age1jmrmdw4kmjeu9d6z74r2unqt7wpgsx24vqejmdjretsnsn8g4drsl3m98w - -creation_rules: - # All Machine secrets - - path_regex: systems/[^/]+/[^/]+/[^/]+\.(yml|yaml)$ - key_groups: - - age: - - *ulmo_1 - - *ulmo_2 - - *manwe_1 diff --git a/.sops.yml b/.sops.yml new file mode 100644 index 0000000..2d6e291 --- /dev/null +++ b/.sops.yml @@ -0,0 +1,57 @@ +keys: + - home: + - &chris age1ewes0f5snqx3sh5ul6fa6qtxzhd25829v6mf5rx2wnheat6fefps5rme2x + - system: + - &aule age + - &mandos age + - &manwe age10c5hmykkduvy75yvqfnchm5lcesr5puarhkwp4l7xdwpykdm397q6xdxuy + - &melkor age + - &orome age + - &tulkas age + - &varda age + - &yavanna age1ewes0f5snqx3sh5ul6fa6qtxzhd25829v6mf5rx2wnheat6fefps5rme2x + +creation_rules: + #=================================================================== + # HOSTS + #=================================================================== + - path_regex: systems/x86_64-linux/aule/secrets.yaml$ + age: *aule + + - path_regex: systems/x86_64-linux/mandos/secrets.yaml$ + age: *mandos + + - path_regex: systems/x86_64-linux/manwe/secrets.yaml$ + key_groups: + - age: + - *manwe + - *yavanna + + - path_regex: systems/x86_64-linux/melkor/secrets.yaml$ + age: *melkor + + - path_regex: systems/x86_64-linux/orome/secrets.yaml$ + age: *orome + + - path_regex: systems/x86_64-linux/tulkas/secrets.yaml$ + age: *tulkas + + - path_regex: systems/x86_64-linux/varda/secrets.yaml$ + age: *varda + + - path_regex: systems/x86_64-linux/yavanna/secrets.yaml$ + age: *yavanna + + #=================================================================== + # USERS + #=================================================================== + - path_regex: homes/x86_64-linux/chris@\w+/secrets.yaml$ + age: *chris + + + + + + + + diff --git a/README.md b/README.md index 2eb75c9..db11887 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,5 @@ nix build .#install-isoConfigurations.minimal - [dafitt/dotfiles](https://github.com/dafitt/dotfiles/) - [khaneliman/khanelinix](https://github.com/khaneliman/khanelinix) +- [alex007sirois/nix-config](https://github.com/alex007sirois/nix-config) (justfile) - [hmajid2301/nixicle](https://gitlab.com/hmajid2301/nixicle) (the GOAT, he did what I am aiming for!) \ No newline at end of file diff --git a/flake.lock b/flake.lock index 757ab1e..ef769ed 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "fromYaml": "fromYaml" }, "locked": { - "lastModified": 1755819240, - "narHash": "sha256-qcMhnL7aGAuFuutH4rq9fvAhCpJWVHLcHVZLtPctPlo=", + "lastModified": 1746562888, + "narHash": "sha256-YgNJQyB5dQiwavdDFBMNKk1wyS77AtdgDk/VtU6wEaI=", "owner": "SenchoPens", "repo": "base16.nix", - "rev": "75ed5e5e3fce37df22e49125181fa37899c3ccd6", + "rev": "806a1777a5db2a1ef9d5d6f493ef2381047f2b89", "type": "github" }, "original": { @@ -21,28 +21,27 @@ "base16-fish": { "flake": false, "locked": { - "lastModified": 1765809053, - "narHash": "sha256-XCUQLoLfBJ8saWms2HCIj4NEN+xNsWBlU1NrEPcQG4s=", + "lastModified": 1622559957, + "narHash": "sha256-PebymhVYbL8trDVVXxCvZgc0S5VxI7I1Hv4RMSquTpA=", "owner": "tomyun", "repo": "base16-fish", - "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "rev": "2f6dd973a9075dabccd26f1cded09508180bf5fe", "type": "github" }, "original": { "owner": "tomyun", "repo": "base16-fish", - "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", "type": "github" } }, "base16-helix": { "flake": false, "locked": { - "lastModified": 1760703920, - "narHash": "sha256-m82fGUYns4uHd+ZTdoLX2vlHikzwzdu2s2rYM2bNwzw=", + "lastModified": 1752979451, + "narHash": "sha256-0CQM+FkYy0fOO/sMGhOoNL80ftsAzYCg9VhIrodqusM=", "owner": "tinted-theming", "repo": "base16-helix", - "rev": "d646af9b7d14bff08824538164af99d0c521b185", + "rev": "27cf1e66e50abc622fb76a3019012dc07c678fac", "type": "github" }, "original": { @@ -68,87 +67,18 @@ "type": "github" } }, - "clan-core": { - "inputs": { - "data-mesher": "data-mesher", - "disko": "disko", - "flake-parts": "flake-parts", - "nix-darwin": "nix-darwin", - "nix-select": "nix-select", - "nixpkgs": [ - "nixpkgs" - ], - "sops-nix": "sops-nix", - "systems": "systems", - "treefmt-nix": "treefmt-nix" - }, - "locked": { - "lastModified": 1775389026, - "narHash": "sha256-cHYF7eGiVqgEnIQKs105eV0P5/zOvxl443qO1f5/Bps=", - "rev": "d53f3c0b42400ff608dd468ac33359881baf969e", - "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/d53f3c0b42400ff608dd468ac33359881baf969e.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://git.clan.lol/clan/clan-core/archive/main.tar.gz" - } - }, - "crane": { - "locked": { - "lastModified": 1757183466, - "narHash": "sha256-kTdCCMuRE+/HNHES5JYsbRHmgtr+l9mOtf5dpcMppVc=", - "owner": "ipetkov", - "repo": "crane", - "rev": "d599ae4847e7f87603e7082d73ca673aa93c916d", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "data-mesher": { - "inputs": { - "flake-parts": [ - "clan-core", - "flake-parts" - ], - "nixpkgs": [ - "clan-core", - "nixpkgs" - ], - "treefmt-nix": [ - "clan-core", - "treefmt-nix" - ] - }, - "locked": { - "lastModified": 1774796937, - "narHash": "sha256-uDcgnNHK1D2oTHOQKsqQUPdDGMuG94dp3Nv8LsnqkEM=", - "rev": "04e10e10c7b4bbf2930f24d139326707a43cbb54", - "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/04e10e10c7b4bbf2930f24d139326707a43cbb54.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://git.clan.lol/clan/data-mesher/archive/main.tar.gz" - } - }, "disko": { "inputs": { "nixpkgs": [ - "clan-core", "nixpkgs" ] }, "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "lastModified": 1753140376, + "narHash": "sha256-7lrVrE0jSvZHrxEzvnfHFE/Wkk9DDqb+mYCodI5uuB8=", "owner": "nix-community", "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "rev": "545aba02960caa78a31bd9a8709a0ad4b6320a5c", "type": "github" }, "original": { @@ -163,11 +93,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1775241072, - "narHash": "sha256-YpXDFEkd+JjxZOgTnvt5GHvEhORxkAda9Lc1e8e8Ox8=", + "lastModified": 1753879613, + "narHash": "sha256-oYhCJSAIZiu3maM2q6JBzh0+MYd4KTaq5eNFIstUurE=", "owner": "emmanuelrosa", "repo": "erosanix", - "rev": "14ac50e5ddefdb1c5ed66c11d2c6fa68959d690a", + "rev": "0ad38bd182cd737f0f4b878ea04cb3676ecd4000", "type": "github" }, "original": { @@ -184,11 +114,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1775373929, - "narHash": "sha256-Elx3es3UvLova3YBdJTc9rju9ULl9+5XF4K5t5Ejsa8=", + "lastModified": 1754290399, + "narHash": "sha256-KwYm1/FeLqP9uE4Sbw+j2nI2/ErNbc9Mn+LPcrEOpX0=", "owner": "nix-community", "repo": "fenix", - "rev": "221468471f762f355db24ce728012544561650f5", + "rev": "f53ddf7518d85d59b58df6e9955b25b0ac25f569", "type": "github" }, "original": { @@ -204,11 +134,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1775388520, - "narHash": "sha256-WUnKn7L/yBo7a5xH2UmPvBfYUr3d4Q8EPCz5r09C8Eo=", + "lastModified": 1754311269, + "narHash": "sha256-y84Q8qS5acSxl3QsLLGs4DboPhM/AYUMiTsJJZwmQxY=", "owner": "nix-community", "repo": "flake-firefox-nightly", - "rev": "00070174d7a635f5238aee06e4feb481ccc7d9f9", + "rev": "5a6856f353975206aec02373c18e8cea3fa6bb75", "type": "github" }, "original": { @@ -220,11 +150,11 @@ "firefox-gnome-theme": { "flake": false, "locked": { - "lastModified": 1775176642, - "narHash": "sha256-2veEED0Fg7Fsh81tvVDNYR6SzjqQxa7hbi18Jv4LWpM=", + "lastModified": 1748383148, + "narHash": "sha256-pGvD/RGuuPf/4oogsfeRaeMm6ipUIznI2QSILKjKzeA=", "owner": "rafaelmardojai", "repo": "firefox-gnome-theme", - "rev": "179704030c5286c729b5b0522037d1d51341022c", + "rev": "4eb2714fbed2b80e234312611a947d6cb7d70caf", "type": "github" }, "original": { @@ -251,11 +181,11 @@ }, "flake-compat_2": { "locked": { - "lastModified": 1761640442, - "narHash": "sha256-AtrEP6Jmdvrqiv4x2xa5mrtaIp3OEe8uBYCDZDS+hu8=", + "lastModified": 1746162366, + "narHash": "sha256-5SSSZ/oQkwfcAz/o/6TlejlVGqeK08wyREBQ5qFFPhM=", "owner": "nix-community", "repo": "flake-compat", - "rev": "4a56054d8ffc173222d09dad23adf4ba946c8884", + "rev": "0f158086a2ecdbb138cd0429410e44994f1b7e4b", "type": "github" }, "original": { @@ -315,16 +245,16 @@ "flake-parts": { "inputs": { "nixpkgs-lib": [ - "clan-core", + "nvf", "nixpkgs" ] }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1753121425, + "narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "rev": "644e0fc48951a860279da645ba77fe4a6e814c5e", "type": "github" }, "original": { @@ -334,48 +264,6 @@ } }, "flake-parts_2": { - "inputs": { - "nixpkgs-lib": [ - "mydia", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1763759067, - "narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-parts_3": { - "inputs": { - "nixpkgs-lib": [ - "nvf", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1769996383, - "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-parts_4": { "inputs": { "nixpkgs-lib": [ "stylix", @@ -383,32 +271,11 @@ ] }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1751413152, + "narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-parts_5": { - "inputs": { - "nixpkgs-lib": [ - "terranix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1736143030, - "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de", + "rev": "77826244401ea9de6e3bac47c2db46005e1f30b5", "type": "github" }, "original": { @@ -419,7 +286,7 @@ }, "flake-utils": { "inputs": { - "systems": "systems_2" + "systems": "systems" }, "locked": { "lastModified": 1731533236, @@ -437,7 +304,7 @@ }, "flake-utils-plus": { "inputs": { - "flake-utils": "flake-utils_2" + "flake-utils": "flake-utils_4" }, "locked": { "lastModified": 1715533576, @@ -455,6 +322,42 @@ } }, "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_4": { "inputs": { "systems": "systems_5" }, @@ -474,15 +377,14 @@ }, "flux": { "inputs": { - "mcman": "mcman", - "nixpkgs": "nixpkgs_4" + "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1767316901, - "narHash": "sha256-tllrks9CW9WWWa0w9Brc25uieYixR8pAMUJODith/yU=", + "lastModified": 1729388191, + "narHash": "sha256-Ga5JJPgwfpqMFvk8g4un7ySwfTefGCsbUrCDsSnQyiU=", "owner": "IogaMaster", "repo": "flux", - "rev": "b12173e0b75c10c8f590a9d3a3ad23681f14d038", + "rev": "400896b5c977e0569ea0f8bacb9b42509e0bbd00", "type": "github" }, "original": { @@ -510,30 +412,30 @@ "gnome-shell": { "flake": false, "locked": { - "lastModified": 1767737596, - "narHash": "sha256-eFujfIUQDgWnSJBablOuG+32hCai192yRdrNHTv0a+s=", + "lastModified": 1748186689, + "narHash": "sha256-UaD7Y9f8iuLBMGHXeJlRu6U1Ggw5B9JnkFs3enZlap0=", "owner": "GNOME", "repo": "gnome-shell", - "rev": "ef02db02bf0ff342734d525b5767814770d85b49", + "rev": "8c88f917db0f1f0d80fa55206c863d3746fa18d0", "type": "github" }, "original": { "owner": "GNOME", + "ref": "48.2", "repo": "gnome-shell", - "rev": "ef02db02bf0ff342734d525b5767814770d85b49", "type": "github" } }, "grub2-themes": { "inputs": { - "nixpkgs": "nixpkgs_5" + "nixpkgs": "nixpkgs_4" }, "locked": { - "lastModified": 1757136219, - "narHash": "sha256-tKU+vq34KHu/A2wD7WdgP5A4/RCmSD8hB0TyQAUlixA=", + "lastModified": 1753279958, + "narHash": "sha256-EJ1udnwKYgWeAJzncAccbLPtbSWiuIANryXTGI9nY6w=", "owner": "vinceliuice", "repo": "grub2-themes", - "rev": "80dd04ddf3ba7b284a7b1a5df2b1e95ee2aad606", + "rev": "6c26f99622cb1c705b3fe2dbe1eb88521096b25a", "type": "github" }, "original": { @@ -544,16 +446,17 @@ }, "himmelblau": { "inputs": { + "flake-utils": "flake-utils_2", "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1775230022, - "narHash": "sha256-FBhkbsqDTULYB1nS92y1CT7qSAM9rUMZR9hS8AvIw24=", + "lastModified": 1754075821, + "narHash": "sha256-ihlkNqYsNgJPCDOE2LPpUl/ww3LBKfsxeWs2sivhb10=", "owner": "himmelblau-idm", "repo": "himmelblau", - "rev": "d700f39281354c0b08cfb9640011a381bed29136", + "rev": "f77821437959ecd67f2fb2b1266e5a644a46d149", "type": "github" }, "original": { @@ -569,32 +472,11 @@ ] }, "locked": { - "lastModified": 1775360939, - "narHash": "sha256-XUBlSgUFdvTh6+K5LcI5mJu5F5L8scmJDMRiZM484TM=", + "lastModified": 1754263839, + "narHash": "sha256-ck7lILfCNuunsLvExPI4Pw9OOCJksxXwozum24W8b+8=", "owner": "nix-community", "repo": "home-manager", - "rev": "2097a5c82bdc099c6135eae4b111b78124604554", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "home-manager", - "type": "github" - } - }, - "home-manager_2": { - "inputs": { - "nixpkgs": [ - "zen-browser", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1774991950, - "narHash": "sha256-kScKj3qJDIWuN9/6PMmgy5esrTUkYinrO5VvILik/zw=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "f2d3e04e278422c7379e067e323734f3e8c585a7", + "rev": "1d7abbd5454db97e0af51416f4960b3fb64a4773", "type": "github" }, "original": { @@ -611,11 +493,11 @@ ] }, "locked": { - "lastModified": 1775287496, - "narHash": "sha256-tCBlt+RP85MLrMYntro/YvG7NWktbmFiyItGBo85Tf8=", + "lastModified": 1754110197, + "narHash": "sha256-N7GWK2084EsNdwzwg6FCIgMrSau1WwzxGSNdPHx5Tak=", "owner": "Jovian-Experiments", "repo": "Jovian-NixOS", - "rev": "0a7a3feb77606db451aa10287ad4c4c8f85922f8", + "rev": "04ce5c103eb621220d69102bc0ee27c3abd89204", "type": "github" }, "original": { @@ -630,11 +512,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1774789463, - "narHash": "sha256-MFraiT8o6manIcEloazGYafji1ua3HJ7Re/A/uauqYA=", + "lastModified": 1754223384, + "narHash": "sha256-pewBF80b4slivTMSeONyOPceyzUUlBLpVOxlGf0hFEY=", "owner": "nix-community", "repo": "lib-aggregate", - "rev": "dc3bd444a2ea0834374b7d759c532f232e144128", + "rev": "2d6fee65844e851060a6817984248bcf8358c6b0", "type": "github" }, "original": { @@ -643,32 +525,13 @@ "type": "github" } }, - "mcman": { - "inputs": { - "crane": "crane", - "nixpkgs": "nixpkgs_3" - }, - "locked": { - "lastModified": 1766962671, - "narHash": "sha256-n1+76Xuk30JKy4raeU5okeaYZjzDQkS+bBLjX8NGZIg=", - "owner": "deniz-blue", - "repo": "mcman", - "rev": "0e3bef55234406eb0634b80f9cac9a284254768a", - "type": "github" - }, - "original": { - "owner": "deniz-blue", - "repo": "mcman", - "type": "github" - } - }, "mnw": { "locked": { - "lastModified": 1770419553, - "narHash": "sha256-b1XqsH7AtVf2dXmq2iyRr2NC1yG7skY7Z6N2MpWHlK4=", + "lastModified": 1748710831, + "narHash": "sha256-eZu2yH3Y2eA9DD3naKWy/sTxYS5rPK2hO7vj8tvUCSU=", "owner": "Gerg-L", "repo": "mnw", - "rev": "2aaffa8030d0b262176146adbb6b0e6374ce2957", + "rev": "cff958a4e050f8d917a6ff3a5624bc4681c6187d", "type": "github" }, "original": { @@ -677,68 +540,6 @@ "type": "github" } }, - "mydia": { - "inputs": { - "flake-parts": "flake-parts_2", - "nixpkgs": "nixpkgs_6" - }, - "locked": { - "lastModified": 1764866402, - "narHash": "sha256-0NOWsPks+/vV5ZM9ti71hUPMLy3FzbEIlFI6vxARvuY=", - "owner": "chris-kruining", - "repo": "mydia", - "rev": "458fc9a21c6987d994bc7932efb6c49df25ba806", - "type": "github" - }, - "original": { - "owner": "chris-kruining", - "repo": "mydia", - "type": "github" - } - }, - "ndg": { - "inputs": { - "nixpkgs": [ - "nvf", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768214250, - "narHash": "sha256-hnBZDQWUxJV3KbtvyGW5BKLO/fAwydrxm5WHCWMQTbw=", - "owner": "feel-co", - "repo": "ndg", - "rev": "a6bd3c1ce2668d096e4fdaaa03ad7f03ba1fbca8", - "type": "github" - }, - "original": { - "owner": "feel-co", - "ref": "refs/tags/v2.6.0", - "repo": "ndg", - "type": "github" - } - }, - "nix-darwin": { - "inputs": { - "nixpkgs": [ - "clan-core", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1775037210, - "narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=", - "owner": "nix-darwin", - "repo": "nix-darwin", - "rev": "06648f4902343228ce2de79f291dd5a58ee12146", - "type": "github" - }, - "original": { - "owner": "nix-darwin", - "repo": "nix-darwin", - "type": "github" - } - }, "nix-github-actions": { "inputs": { "nixpkgs": [ @@ -764,15 +565,15 @@ "nix-minecraft": { "inputs": { "flake-compat": "flake-compat_3", - "nixpkgs": "nixpkgs_7", - "systems": "systems_3" + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs_5" }, "locked": { - "lastModified": 1775359538, - "narHash": "sha256-PbX+bT49p9c7cmT03ufao8tDDEn0Qi7R82R1yXDyk5k=", + "lastModified": 1754274768, + "narHash": "sha256-bI+Z15bpec7VEnxkrqOG+JX0bFa9CnVeg/uiaf8iiS0=", "owner": "Infinidoge", "repo": "nix-minecraft", - "rev": "bdf703935b0aa47d9de1c6a7536fc76756b044ef", + "rev": "b54894d44fbe4d29c081ade695ffdb07bb21b322", "type": "github" }, "original": { @@ -781,19 +582,6 @@ "type": "github" } }, - "nix-select": { - "locked": { - "lastModified": 1763303120, - "narHash": "sha256-yxcNOha7Cfv2nhVpz9ZXSNKk0R7wt4AiBklJ8D24rVg=", - "rev": "3d1e3860bef36857a01a2ddecba7cdb0a14c35a9", - "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/3d1e3860bef36857a01a2ddecba7cdb0a14c35a9.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz" - } - }, "nixlib": { "locked": { "lastModified": 1736643958, @@ -832,11 +620,11 @@ ] }, "locked": { - "lastModified": 1769813415, - "narHash": "sha256-nnVmNNKBi1YiBNPhKclNYDORoHkuKipoz7EtVnXO50A=", + "lastModified": 1751903740, + "narHash": "sha256-PeSkNMvkpEvts+9DjFiop1iT2JuBpyknmBUs0Un0a4I=", "owner": "nix-community", "repo": "nixos-generators", - "rev": "8946737ff703382fda7623b9fab071d037e897d5", + "rev": "032decf9db65efed428afd2fa39d80f7089085eb", "type": "github" }, "original": { @@ -853,11 +641,11 @@ ] }, "locked": { - "lastModified": 1774972752, - "narHash": "sha256-DnLIpFxznohpLkIFs390uZ0gxwkVyhtknhKNu+lQJK8=", + "lastModified": 1754260137, + "narHash": "sha256-IViMH6Fwj8nwO1nuYCqOTpjm9OK9rQ0w8nmoOwPlo98=", "owner": "nix-community", "repo": "nixos-wsl", - "rev": "d97e078f4788cddb8d11c3c99f72a4bb9ddec221", + "rev": "57ba096649fa4e12dc564e8e3c529255baf89b35", "type": "github" }, "original": { @@ -868,11 +656,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775054576, - "narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=", + "lastModified": 1751186460, + "narHash": "sha256-tSnI50oYaXOi/SFUmJC+gZ2xE9pAhTnV0D2/3JoKL7g=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912", + "rev": "dd5540905b1a13176efa13fa2f8dac776bcb275a", "type": "github" }, "original": { @@ -884,11 +672,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1774748309, - "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", + "lastModified": 1754184128, + "narHash": "sha256-AjhoyBL4eSyXf01Bmc6DiuaMrJRNdWopmdnMY0Pa/M0=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "333c4e0545a6da976206c74db8773a1645b5870a", + "rev": "02e72200e6d56494f4a7c0da8118760736e41b60", "type": "github" }, "original": { @@ -899,31 +687,15 @@ }, "nixpkgs_10": { "locked": { - "lastModified": 1775126147, - "narHash": "sha256-J0dZU4atgcfo4QvM9D92uQ0Oe1eLTxBVXjJzdEMQpD0=", - "owner": "NixOS", + "lastModified": 1727348695, + "narHash": "sha256-J+PeFKSDV+pHL7ukkfpVzCOO7mBSrrpJ3svwBFABbhI=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "8d8c1fa5b412c223ffa47410867813290cdedfef", + "rev": "1925c603f17fc89f4c8f6bf6f631a802ad85d784", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_11": { - "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", - "type": "github" - }, - "original": { - "owner": "NixOS", + "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" @@ -931,11 +703,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1775371993, - "narHash": "sha256-shlcgEOzW6rl7zmZeYBMP9EpF3O/cTL7/HpWlyqearw=", + "lastModified": 1754284898, + "narHash": "sha256-wzM6HN0xxyooekXfl7p5P4Bn0LieOKOfsLg4DqY7XLk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ff2af6f7ebc6c123603d5689aeea6461290f46b5", + "rev": "114484ca7213ac06fa7907e58dd8ef9d801d39f0", "type": "github" }, "original": { @@ -947,42 +719,27 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1757347588, - "narHash": "sha256-tLdkkC6XnsY9EOZW9TlpesTclELy8W7lL2ClL+nma8o=", - "owner": "NixOS", + "lastModified": 1712608508, + "narHash": "sha256-vMZ5603yU0wxgyQeHJryOI+O61yrX2AHwY6LOFyV1gM=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "b599843bad24621dcaa5ab60dac98f9b0eb1cabe", + "rev": "4cba8b53da471aea2ab2b0c1f30a81e7c451f4b6", "type": "github" }, "original": { - "id": "nixpkgs", + "owner": "nixos", "ref": "nixos-unstable", - "type": "indirect" + "repo": "nixpkgs", + "type": "github" } }, "nixpkgs_4": { "locked": { - "lastModified": 1766902085, - "narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_5": { - "locked": { - "lastModified": 1775391773, - "narHash": "sha256-8h0YBzKR6kf+68qnZtZnC6GhTf2XAilTQ9F/tm5JDWs=", + "lastModified": 1754315431, + "narHash": "sha256-fnVgd+mIJeR/fsaJB11KcTFjoJzLZNglLjVRtAzwcUI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "728629d3d4797ab52406df91b319c07a7d2ce479", + "rev": "66023e4de2495a69792a2b72bd131358b824d2e3", "type": "github" }, "original": { @@ -992,17 +749,33 @@ "type": "github" } }, - "nixpkgs_6": { + "nixpkgs_5": { "locked": { - "lastModified": 1764242076, - "narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=", - "owner": "NixOS", + "lastModified": 1748929857, + "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4", + "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_6": { + "locked": { + "lastModified": 1754214453, + "narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376", + "type": "github" + }, + "original": { + "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" @@ -1010,47 +783,47 @@ }, "nixpkgs_7": { "locked": { - "lastModified": 1769461804, - "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", + "lastModified": 1753432016, + "narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", + "rev": "6027c30c8e9810896b92429f0092f624f7b1aace", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-unstable", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_8": { "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", - "owner": "nixos", + "lastModified": 1744868846, + "narHash": "sha256-5RJTdUHDmj12Qsv7XOhuospjAjATNiTMElplWnJE9Hs=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "rev": "ebe4301cbd8f81c4f8d3244b3632338bbeb6d49c", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-unstable", + "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_9": { "locked": { - "lastModified": 1774386573, - "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", - "owner": "nixos", + "lastModified": 1751792365, + "narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb", "type": "github" }, "original": { - "owner": "nixos", + "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" @@ -1068,11 +841,11 @@ ] }, "locked": { - "lastModified": 1775228139, - "narHash": "sha256-ebbeHmg+V7w8050bwQOuhmQHoLOEOfqKzM1KgCTexK4=", + "lastModified": 1751906969, + "narHash": "sha256-BSQAOdPnzdpOuCdAGSJmefSDlqmStFNScEnrWzSqKPw=", "owner": "nix-community", "repo": "NUR", - "rev": "601971b9c89e0304561977f2c28fa25e73aa7132", + "rev": "ddb679f4131e819efe3bbc6457ba19d7ad116f25", "type": "github" }, "original": { @@ -1084,18 +857,17 @@ "nvf": { "inputs": { "flake-compat": "flake-compat_4", - "flake-parts": "flake-parts_3", + "flake-parts": "flake-parts", "mnw": "mnw", - "ndg": "ndg", - "nixpkgs": "nixpkgs_9", + "nixpkgs": "nixpkgs_7", "systems": "systems_4" }, "locked": { - "lastModified": 1775122065, - "narHash": "sha256-ZlowJNkQOhpsXDuWbHgB1xY6W8kyzYn9coK9nJsqqNg=", + "lastModified": 1754137146, + "narHash": "sha256-V2AE32tLNvtYVBuc8ZRbkGjAZGsJchFbNVd6v5JXvg8=", "owner": "notashelf", "repo": "nvf", - "rev": "d3304af3d5771e8d5bac6ee9bbdbce56086d54f7", + "rev": "16d396f039ffefabf93b7b3261e2a17e2f84439b", "type": "github" }, "original": { @@ -1114,11 +886,11 @@ ] }, "locked": { - "lastModified": 1774915545, - "narHash": "sha256-COT4l/+ZddGBvrDVfPf7MEOJxV8EDKame6/aRnNIKcY=", + "lastModified": 1754241118, + "narHash": "sha256-nsBBqbAFB7lUYIh6S6l7fQ/ALDhCckp7+rqbY2767uE=", "owner": "nix-community", "repo": "plasma-manager", - "rev": "f3177b3c69fb3f03201098d7fe8ab6422cce7fc1", + "rev": "968109159b4bbe4386ac281272ddcebeef09ebfc", "type": "github" }, "original": { @@ -1129,7 +901,7 @@ }, "root": { "inputs": { - "clan-core": "clan-core", + "disko": "disko", "erosanix": "erosanix", "fenix": "fenix", "firefox": "firefox", @@ -1138,29 +910,27 @@ "himmelblau": "himmelblau", "home-manager": "home-manager", "jovian": "jovian", - "mydia": "mydia", "nix-minecraft": "nix-minecraft", "nixos-boot": "nixos-boot", "nixos-generators": "nixos-generators", "nixos-wsl": "nixos-wsl", - "nixpkgs": "nixpkgs_8", + "nixpkgs": "nixpkgs_6", "nvf": "nvf", "plasma-manager": "plasma-manager", "snowfall-lib": "snowfall-lib", - "sops-nix": "sops-nix_2", + "sops-nix": "sops-nix", "stylix": "stylix", - "terranix": "terranix", "zen-browser": "zen-browser" } }, "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1775228522, - "narHash": "sha256-+6eTD6EAabjow5gdjWRP6aI2UUwOZJEjzzsvvbVu8f8=", + "lastModified": 1754218780, + "narHash": "sha256-M+bLCsYRYA7iudlZkeOf+Azm/1TUvihIq51OKia6KJ8=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "f4b77dc99d9925667246e2887783b79bdc46a50d", + "rev": "8d75311400a108d7ffe17dc9c38182c566952e6e", "type": "github" }, "original": { @@ -1179,11 +949,11 @@ ] }, "locked": { - "lastModified": 1765361626, - "narHash": "sha256-kX0Dp/kYSRbQ+yd9e3lmmUWdNbipufvKfL2IzbrSpnY=", + "lastModified": 1736130495, + "narHash": "sha256-4i9nAJEZFv7vZMmrE0YG55I3Ggrtfo5/T07JEpEZ/RM=", "owner": "snowfallorg", "repo": "lib", - "rev": "c566ad8b7352c30ec3763435de7c8f1c46ebb357", + "rev": "02d941739f98a09e81f3d2d9b3ab08918958beac", "type": "github" }, "original": { @@ -1194,35 +964,14 @@ }, "sops-nix": { "inputs": { - "nixpkgs": [ - "clan-core", - "nixpkgs" - ] + "nixpkgs": "nixpkgs_8" }, "locked": { - "lastModified": 1775365543, - "narHash": "sha256-f50qrK0WwZ9z5EdaMGWOTtALgSF7yb7XwuE7LjCuDmw=", + "lastModified": 1752544651, + "narHash": "sha256-GllP7cmQu7zLZTs9z0J2gIL42IZHa9CBEXwBY9szT0U=", "owner": "Mic92", "repo": "sops-nix", - "rev": "a4ee2de76efb759fe8d4868c33dec9937897916f", - "type": "github" - }, - "original": { - "owner": "Mic92", - "repo": "sops-nix", - "type": "github" - } - }, - "sops-nix_2": { - "inputs": { - "nixpkgs": "nixpkgs_10" - }, - "locked": { - "lastModified": 1775365543, - "narHash": "sha256-f50qrK0WwZ9z5EdaMGWOTtALgSF7yb7XwuE7LjCuDmw=", - "owner": "Mic92", - "repo": "sops-nix", - "rev": "a4ee2de76efb759fe8d4868c33dec9937897916f", + "rev": "2c8def626f54708a9c38a5861866660395bb3461", "type": "github" }, "original": { @@ -1238,22 +987,23 @@ "base16-helix": "base16-helix", "base16-vim": "base16-vim", "firefox-gnome-theme": "firefox-gnome-theme", - "flake-parts": "flake-parts_4", + "flake-parts": "flake-parts_2", "gnome-shell": "gnome-shell", - "nixpkgs": "nixpkgs_11", + "nixpkgs": "nixpkgs_9", "nur": "nur", "systems": "systems_6", + "tinted-foot": "tinted-foot", "tinted-kitty": "tinted-kitty", "tinted-schemes": "tinted-schemes", "tinted-tmux": "tinted-tmux", "tinted-zed": "tinted-zed" }, "locked": { - "lastModified": 1775247334, - "narHash": "sha256-eVKt8wpQqg6Hq/UdHQkV1izXGloGQxdlE4SSk9/X27s=", + "lastModified": 1754264048, + "narHash": "sha256-Yg1W0sFhBpnglfhWGlFmxzSmte1F157luHAADp5Hguk=", "owner": "nix-community", "repo": "stylix", - "rev": "6d0502ef7447090abf8b00362b5cda8ac64595b4", + "rev": "1b5e1c5642cf96e07daf14ae4c5ddd23d7ed5623", "type": "github" }, "original": { @@ -1352,40 +1102,20 @@ "type": "github" } }, - "systems_7": { + "tinted-foot": { + "flake": false, "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "lastModified": 1726913040, + "narHash": "sha256-+eDZPkw7efMNUf3/Pv0EmsidqdwNJ1TaOum6k7lngDQ=", + "owner": "tinted-theming", + "repo": "tinted-foot", + "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "terranix": { - "inputs": { - "flake-parts": "flake-parts_5", - "nixpkgs": [ - "nixpkgs" - ], - "systems": "systems_7" - }, - "locked": { - "lastModified": 1773700838, - "narHash": "sha256-6KFxpxyXjcqhOexc7ZeaXVWdDtGb6zO8HtjBEci9DfU=", - "owner": "terranix", - "repo": "terranix", - "rev": "306ce146bf0324dc3b3c45c095036b6f0e26bf35", - "type": "github" - }, - "original": { - "owner": "terranix", - "repo": "terranix", + "owner": "tinted-theming", + "repo": "tinted-foot", + "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", "type": "github" } }, @@ -1408,11 +1138,11 @@ "tinted-schemes": { "flake": false, "locked": { - "lastModified": 1772661346, - "narHash": "sha256-4eu3LqB9tPqe0Vaqxd4wkZiBbthLbpb7llcoE/p5HT0=", + "lastModified": 1750770351, + "narHash": "sha256-LI+BnRoFNRa2ffbe3dcuIRYAUcGklBx0+EcFxlHj0SY=", "owner": "tinted-theming", "repo": "schemes", - "rev": "13b5b0c299982bb361039601e2d72587d6846294", + "rev": "5a775c6ffd6e6125947b393872cde95867d85a2a", "type": "github" }, "original": { @@ -1424,11 +1154,11 @@ "tinted-tmux": { "flake": false, "locked": { - "lastModified": 1772934010, - "narHash": "sha256-x+6+4UvaG+RBRQ6UaX+o6DjEg28u4eqhVRM9kpgJGjQ=", + "lastModified": 1751159871, + "narHash": "sha256-UOHBN1fgHIEzvPmdNMHaDvdRMgLmEJh2hNmDrp3d3LE=", "owner": "tinted-theming", "repo": "tinted-tmux", - "rev": "c3529673a5ab6e1b6830f618c45d9ce1bcdd829d", + "rev": "bded5e24407cec9d01bd47a317d15b9223a1546c", "type": "github" }, "original": { @@ -1440,11 +1170,11 @@ "tinted-zed": { "flake": false, "locked": { - "lastModified": 1772909925, - "narHash": "sha256-jx/5+pgYR0noHa3hk2esin18VMbnPSvWPL5bBjfTIAU=", + "lastModified": 1751158968, + "narHash": "sha256-ksOyv7D3SRRtebpXxgpG4TK8gZSKFc4TIZpR+C98jX8=", "owner": "tinted-theming", "repo": "base16-zed", - "rev": "b4d3a1b3bcbd090937ef609a0a3b37237af974df", + "rev": "86a470d94204f7652b906ab0d378e4231a5b3384", "type": "github" }, "original": { @@ -1453,44 +1183,20 @@ "type": "github" } }, - "treefmt-nix": { - "inputs": { - "nixpkgs": [ - "clan-core", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1775125835, - "narHash": "sha256-2qYcPgzFhnQWchHo0SlqLHrXpux5i6ay6UHA+v2iH4U=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "75925962939880974e3ab417879daffcba36c4a3", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" - } - }, "zen-browser": { "inputs": { - "home-manager": "home-manager_2", - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs_10" }, "locked": { - "lastModified": 1775367672, - "narHash": "sha256-nGC6qrRsWysfR7/8wsSooq0X71rfJjhq1b+dFI6oQtY=", - "owner": "0xc000022070", + "lastModified": 1727721329, + "narHash": "sha256-QYlWZwUSwrM7BuO+dXclZIwoPvBIuJr6GpFKv9XKFPI=", + "owner": "MarceColl", "repo": "zen-browser-flake", - "rev": "33cd729244914f1e121477c5de148639c5e73c4a", + "rev": "e6ab73f405e9a2896cce5956c549a9cc359e5fcc", "type": "github" }, "original": { - "owner": "0xc000022070", + "owner": "MarceColl", "repo": "zen-browser-flake", "type": "github" } diff --git a/flake.nix b/flake.nix index 692afe1..fa4895c 100644 --- a/flake.nix +++ b/flake.nix @@ -1,10 +1,6 @@ { description = "Nixos config flake"; - nixConfig = { - warn-dirty = false; - }; - inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; @@ -13,6 +9,11 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + home-manager = { url = "github:nix-community/home-manager"; inputs.nixpkgs.follows = "nixpkgs"; @@ -28,14 +29,14 @@ url = "github:nix-community/nixos-generators"; inputs.nixpkgs.follows = "nixpkgs"; }; - - # neovim - nvf.url = "github:notashelf/nvf"; - - # plymouth theme - nixos-boot.url = "github:Melkor333/nixos-boot"; - - firefox.url = "github:nix-community/flake-firefox-nightly"; + + nixos-wsl = { + url = "github:nix-community/nixos-wsl"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-compat.follows = ""; + }; + }; stylix.url = "github:nix-community/stylix"; @@ -45,10 +46,13 @@ inputs.nixpkgs.follows = "nixpkgs"; }; - zen-browser = { - url = "github:0xc000022070/zen-browser-flake"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + # neovim + nvf.url = "github:notashelf/nvf"; + + # plymouth theme + nixos-boot.url = "github:Melkor333/nixos-boot"; + + zen-browser.url = "github:MarceColl/zen-browser-flake"; nix-minecraft.url = "github:Infinidoge/nix-minecraft"; @@ -70,77 +74,42 @@ url = "github:Jovian-Experiments/Jovian-NixOS"; inputs.nixpkgs.follows = "nixpkgs"; }; - + grub2-themes = { url = "github:vinceliuice/grub2-themes"; }; - - nixos-wsl = { - url = "github:nix-community/nixos-wsl"; - inputs = { - nixpkgs.follows = "nixpkgs"; - flake-compat.follows = ""; - }; - }; - - terranix = { - url = "github:terranix/terranix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - - clan-core = { - url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - - mydia = { - url = "github:chris-kruining/mydia"; - # url = "github:getmydia/mydia"; - }; }; - outputs = inputs: - inputs.snowfall-lib.mkFlake { - inherit inputs; - src = ./.; + outputs = inputs: inputs.snowfall-lib.mkFlake { + inherit inputs; + src = ./.; - snowfall = { - namespace = "sneeuwvlok"; + snowfall = { + namespace = "sneeuwvlok"; - meta = { - name = "sneeuwvlok"; - title = "Sneeuwvlok"; - }; + meta = { + name = "sneeuwvlok"; + title = "Sneeuwvlok"; }; + }; - channels-config = { - allowUnfree = true; - permittedInsecurePackages = [ - # Due to *arr stack - "dotnet-sdk-6.0.428" - "aspnetcore-runtime-6.0.36" - - # I think this is because of zen - "qtwebengine-5.15.19" - - # For Nheko, the matrix client - "olm-3.2.16" - ]; - }; - - overlays = with inputs; [ - fenix.overlays.default - nix-minecraft.overlay - flux.overlays.default - ]; - - systems.modules = with inputs; [ - clan-core.nixosModules.default - ]; - - homes.modules = with inputs; [ - stylix.homeModules.stylix - plasma-manager.homeModules.plasma-manager + channels-config = { + allowUnfree = true; + permittedInsecurePackages = [ + "dotnet-sdk-6.0.428" + "aspnetcore-runtime-6.0.36" ]; }; + + overlays = with inputs; [ + fenix.overlays.default + nix-minecraft.overlay + flux.overlays.default + ]; + + homes.modules = with inputs; [ + stylix.homeModules.stylix + plasma-manager.homeManagerModules.plasma-manager + ]; + }; } diff --git a/homes/x86_64-linux/chris@mandos/default.nix b/homes/x86_64-linux/chris@mandos/default.nix index ba87e73..6989314 100644 --- a/homes/x86_64-linux/chris@mandos/default.nix +++ b/homes/x86_64-linux/chris@mandos/default.nix @@ -1,11 +1,10 @@ -{osConfig, ...}: { +{ osConfig, ... }: +{ home.stateVersion = osConfig.system.stateVersion; programs.git = { - settings.user = { - name = "Chris Kruining"; - email = "chris@kruining.eu"; - }; + userName = "Chris Kruining"; + userEmail = "chris@kruining.eu"; }; sneeuwvlok = { diff --git a/homes/x86_64-linux/chris@manwe/default.nix b/homes/x86_64-linux/chris@manwe/default.nix index 0aced9b..cd6fa1a 100644 --- a/homes/x86_64-linux/chris@manwe/default.nix +++ b/homes/x86_64-linux/chris@manwe/default.nix @@ -1,11 +1,10 @@ -{osConfig, ...}: { +{ osConfig, ... }: +{ home.stateVersion = osConfig.system.stateVersion; programs.git = { - settings.user = { - name = "Chris Kruining"; - email = "chris@kruining.eu"; - }; + userName = "Chris Kruining"; + userEmail = "chris@kruining.eu"; }; sneeuwvlok = { @@ -36,7 +35,6 @@ bitwarden.enable = true; discord.enable = true; ladybird.enable = true; - matrix.enable = true; obs.enable = true; onlyoffice.enable = true; signal.enable = true; diff --git a/homes/x86_64-linux/chris@manwe/secrets.yaml b/homes/x86_64-linux/chris@manwe/secrets.yaml new file mode 100644 index 0000000..0af2506 --- /dev/null +++ b/homes/x86_64-linux/chris@manwe/secrets.yaml @@ -0,0 +1,21 @@ +user_level_secrets: ENC[AES256_GCM,data:TNT+via+r4bpgROz,iv:cVO6/r4Aovr5uJFhU87mE5XwRJ518y4OJdHo4m92ahM=,tag:jYInD+euh7k1zSnMRppI5Q==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1ewes0f5snqx3sh5ul6fa6qtxzhd25829v6mf5rx2wnheat6fefps5rme2x + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTYVRQTEVSMWM3WXY3eTdW + ZkUwSnNidlJwWGVETURpNUJRRUllYXo4WjNvCmxmN21qVzNFV3N4UVR6WEV1am1W + eW1KTk9HVDluek1BUnBmSGI3Y2ZqaDQKLS0tIHlMYldYMTVORVNWbEgrWlBSanRM + bUZiMHlOU3pxYUhQSTREb0l4TmFlOEkKiasV2H481aJzAvEAvyeWqGYDOW+WKRFX + yyocZDo0o1lHz/gNXoC0/ujU+O3rSXdsy6Qdz6Rm+xeFUfe4KoD4bg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2025-08-11T13:21:38Z" + mac: ENC[AES256_GCM,data:kfMcZuYuQqxxfqtyfH7DltSkq8YNz+vroB+ZQKTIpCNC/W6vJP1o23/xLRzdnEgnnH5GfgZQFAK8Am00/bUD2BgEPyXxXNf1lG70ocFbRM9htii92BFfHgfi25zlEqCO7yrudm1HEJyYrFbZnT63H6u1OgWSC38CzEZTBsCE0kU=,iv:feWGBau48s2GSvZjnKPfP2z46SBuHbh//4zzcLv+MTY=,tag:D86akwawLxobhEu2AvBFKg==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.9.4 diff --git a/homes/x86_64-linux/chris@orome/default.nix b/homes/x86_64-linux/chris@orome/default.nix index 7a1dc43..dece506 100644 --- a/homes/x86_64-linux/chris@orome/default.nix +++ b/homes/x86_64-linux/chris@orome/default.nix @@ -1,11 +1,10 @@ -{osConfig, ...}: { +{ osConfig, ... }: +{ home.stateVersion = osConfig.system.stateVersion; programs.git = { - settings.user = { - name = "Chris Kruining"; - email = "chris@kruining.eu"; - }; + userName = "Chris Kruining"; + userEmail = "chris@kruining.eu"; }; sneeuwvlok = { diff --git a/homes/x86_64-linux/chris@tulkas/default.nix b/homes/x86_64-linux/chris@tulkas/default.nix index ba87e73..6989314 100644 --- a/homes/x86_64-linux/chris@tulkas/default.nix +++ b/homes/x86_64-linux/chris@tulkas/default.nix @@ -1,11 +1,10 @@ -{osConfig, ...}: { +{ osConfig, ... }: +{ home.stateVersion = osConfig.system.stateVersion; programs.git = { - settings.user = { - name = "Chris Kruining"; - email = "chris@kruining.eu"; - }; + userName = "Chris Kruining"; + userEmail = "chris@kruining.eu"; }; sneeuwvlok = { diff --git a/justfile b/justfile new file mode 100644 index 0000000..70450dd --- /dev/null +++ b/justfile @@ -0,0 +1,24 @@ +[private] +default: + @just -l + +[doc('Update flake dependencies')] +update: + nix flake update + +[doc('install nixos on a system (uses nix-anywhere) +> profile: Which profile to use +> host: How to reach the target system in the standard format of `user@host` +')] +install profile host: + nix run nixpkgs#nixos-anywhere -- \ + --flake .#{{profile}} \ + --generate-hardware-config nixos-generate-config ./hardware-configuration.nix \ + {{host}} + +[doc('builds the configuration for the host')] +build host: + nh os build . -H {{host}} + +edit-secrets target: + sops --config "{{justfile_directory()}}/.sops.yml" edit "{{justfile_directory()}}/{{ if target =~ ".+@.+" { "homes" } else { "systems" } }}/x86_64-linux/{{target}}/secrets.yaml" \ No newline at end of file diff --git a/lib/options/default.nix b/lib/options/default.nix deleted file mode 100644 index 72e8621..0000000 --- a/lib/options/default.nix +++ /dev/null @@ -1,38 +0,0 @@ -{ lib, ...}: -let - inherit (builtins) isString typeOf; - inherit (lib) mkOption types throwIfNot concatStringsSep splitStringBy toLower map; -in -{ - options = { - mkUrlOptions = - defaults: - { - host = mkOption { - type = types.str; - example = "host.tld"; - description = '' - Hostname - ''; - } // (defaults.host or {}); - - port = mkOption { - type = types.port; - default = 1234; - example = "1234"; - description = '' - Port - ''; - } // (defaults.port or {}); - - protocol = mkOption { - type = types.str; - default = "https"; - example = "https"; - description = '' - Which protocol to use when creating a url string - ''; - } // (defaults.protocol or {}); - }; - }; -} \ No newline at end of file diff --git a/lib/strings/default.nix b/lib/strings/default.nix deleted file mode 100644 index 0c15699..0000000 --- a/lib/strings/default.nix +++ /dev/null @@ -1,39 +0,0 @@ -{ lib, ...}: -let - inherit (builtins) isString typeOf match toString head; - inherit (lib) throwIfNot concatStringsSep splitStringBy toLower map concatMapAttrsStringSep; -in -{ - strings = { - #======================================================================================== - # Converts a string to snake case - # - # simply replaces any uppercase letter to its lowercase variant preceeded by an underscore - #======================================================================================== - toSnakeCase = - str: - throwIfNot (isString str) "toSnakeCase only accepts string values, but got ${typeOf str}" ( - str - |> splitStringBy (prev: curr: builtins.match "[a-z]" prev != null && builtins.match "[A-Z]" curr != null) true - |> map (p: toLower p) - |> concatStringsSep "_" - ); - - #======================================================================================== - # Converts a set of url parts to a string - #======================================================================================== - toUrl = - { protocol ? null, host, port ? null, path ? null, query ? null, hash ? null }: - let - trim_slashes = str: str |> match "^\/*(.+?)\/*$" |> head; - encode_to_str = set: concatMapAttrsStringSep "&" (n: v: "${n}=${v}") set; - - _protocol = if protocol != null then "${protocol}://" else ""; - _port = if port != null then ":${toString port}" else ""; - _path = if path != null then "/${path |> trim_slashes}" else ""; - _query = if query != null then "?${query |> encode_to_str}" else ""; - _hash = if hash != null then "#${hash |> encode_to_str}" else ""; - in - "${_protocol}${host}${_port}${_path}${_query}${_hash}"; - }; -} \ No newline at end of file diff --git a/logs/bridge-2026-04-15T09-11-43.612.log b/logs/bridge-2026-04-15T09-11-43.612.log deleted file mode 100644 index df81d78..0000000 --- a/logs/bridge-2026-04-15T09-11-43.612.log +++ /dev/null @@ -1,2 +0,0 @@ -{"level":"fatal","error":"homeserver.address not configured","time":"2026-04-15T09:10:06.949460064Z","message":"Configuration error"} -{"level":"info","time":"2026-04-15T09:10:06.949840013Z","message":"See https://docs.mau.fi/faq/field-unconfigured for more info"} diff --git a/logs/bridge.log b/logs/bridge.log deleted file mode 100644 index 63567e0..0000000 --- a/logs/bridge.log +++ /dev/null @@ -1,2 +0,0 @@ -{"level":"fatal","error":"appservice.as_token not configured. Did you forget to generate the registration? ","time":"2026-04-15T09:11:43.617908298Z","message":"Configuration error"} -{"level":"info","time":"2026-04-15T09:11:43.618232253Z","message":"See https://docs.mau.fi/faq/field-unconfigured for more info"} diff --git a/modules/home/application/matrix/default.nix b/modules/home/application/matrix/default.nix deleted file mode 100644 index 867a94f..0000000 --- a/modules/home/application/matrix/default.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ config, lib, pkgs, namespace, osConfig ? {}, ... }: -let - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.application.matrix; -in -{ - options.${namespace}.application.matrix = { - enable = mkEnableOption "enable Matrix client (Fractal)"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [ fractal element-desktop ]; - - programs.element-desktop = { - enable = true; - }; - }; -} diff --git a/modules/home/application/onlyoffice/default.nix b/modules/home/application/onlyoffice/default.nix index 0479539..8153b68 100644 --- a/modules/home/application/onlyoffice/default.nix +++ b/modules/home/application/onlyoffice/default.nix @@ -1,20 +1,16 @@ -{ - inputs, - config, - lib, - pkgs, - namespace, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; cfg = config.${namespace}.application.onlyoffice; -in { +in +{ options.${namespace}.application.onlyoffice = { enable = mkEnableOption "enable onlyoffice"; }; config = mkIf cfg.enable { - home.packages = with pkgs; [onlyoffice-desktopeditors]; + home.packages = with pkgs; [ onlyoffice-bin ]; + # fonts.packages = with pkgs; [ corefonts ]; }; } diff --git a/modules/home/application/steam/default.nix b/modules/home/application/steam/default.nix index 8c87b40..4e62c41 100644 --- a/modules/home/application/steam/default.nix +++ b/modules/home/application/steam/default.nix @@ -10,7 +10,7 @@ in }; config = mkIf cfg.enable { - home.packages = with pkgs; [ protonup-ng ]; + home.packages = with pkgs; [ protonup ]; home.sessionVariables = { STEAM_EXTRA_COMPAT_TOOLS_PATHS = "\${HOME}/.steam/root/compatibilitytools.d"; diff --git a/modules/home/application/teamspeak/default.nix b/modules/home/application/teamspeak/default.nix index 3e5e530..e15bd96 100644 --- a/modules/home/application/teamspeak/default.nix +++ b/modules/home/application/teamspeak/default.nix @@ -10,9 +10,6 @@ in }; config = mkIf cfg.enable { - home.packages = with pkgs; [ - # teamspeak3 - teamspeak6-client - ]; + home.packages = with pkgs; [ teamspeak_client ]; }; } diff --git a/modules/home/application/zen/default.nix b/modules/home/application/zen/default.nix index b7cec03..ad4cb92 100644 --- a/modules/home/application/zen/default.nix +++ b/modules/home/application/zen/default.nix @@ -5,61 +5,35 @@ let cfg = config.${namespace}.application.zen; in { - imports = [ - inputs.zen-browser.homeModules.default - ]; - options.${namespace}.application.zen = { enable = mkEnableOption "enable zen"; }; config = mkIf cfg.enable { + home.packages = [ inputs.zen-browser.packages.${pkgs.system}.specific ]; + home.sessionVariables = { MOZ_ENABLE_WAYLAND = "1"; }; programs.zen-browser = { - enable = true; - policies = { AutofillAddressEnabled = true; AutofillCreditCardEnabled = false; - - AppAutoUpdate = false; DisableAppUpdate = true; - ManualAppUpdateOnly = true; - DisableFeedbackCommands = true; DisableFirefoxStudies = true; DisablePocket = true; DisableTelemetry = true; - - DontCheckDefaultBrowser = false; + # DontCheckDefaultBrowser = false; NoDefaultBookmarks = true; - OfferToSaveLogins = false; + # OfferToSaveLogins = false; EnableTrackingProtection = { Value = true; Locked = true; Cryptomining = true; Fingerprinting = true; }; - - HttpAllowlist = [ - "http://ulmo" - ]; - }; - - policies.ExtensionSettings = let - mkExtension = id: { - install_url = "https://addons.mozilla.org/firefox/downloads/latest/${builtins.toString id}/latest.xpi"; - installation_mode = "force_installed"; - }; - in - { - ublock_origin = 4531307; - ghostry = 4562168; - bitwarden = 4562769; - sponsorblock = 4541835; }; }; }; diff --git a/modules/home/desktop/plasma/default.nix b/modules/home/desktop/plasma/default.nix index 0b679a0..13476fb 100644 --- a/modules/home/desktop/plasma/default.nix +++ b/modules/home/desktop/plasma/default.nix @@ -64,7 +64,7 @@ in }; kwalletrc = { - Wallet.Enabled = true; + Wallet.Enabled = false; }; plasmarc = { diff --git a/modules/home/desktop/plasma/panels.nix b/modules/home/desktop/plasma/panels.nix index 6e20938..52212b7 100644 --- a/modules/home/desktop/plasma/panels.nix +++ b/modules/home/desktop/plasma/panels.nix @@ -95,7 +95,7 @@ digitalClock = { date = { enable = true; - format.custom = "dd-MM-yyyy"; + format = "shortDate"; position = "belowTime"; }; time = { @@ -106,4 +106,4 @@ } ]; } -] +] \ No newline at end of file diff --git a/modules/home/editor/zed/default.nix b/modules/home/editor/zed/default.nix index f0fe7fa..b35acba 100644 --- a/modules/home/editor/zed/default.nix +++ b/modules/home/editor/zed/default.nix @@ -15,7 +15,7 @@ in { programs.zed-editor = { enable = true; - extensions = [ "nix" "toml" "html" "just-ls" ]; + extensions = [ "nix" "toml" "html" ]; userSettings = { assistant.enabled = false; diff --git a/modules/home/home-manager/default.nix b/modules/home/home-manager/default.nix index 5f3be03..93bae2e 100644 --- a/modules/home/home-manager/default.nix +++ b/modules/home/home-manager/default.nix @@ -4,9 +4,7 @@ let in { systemd.user.startServices = "sd-switch"; - programs.home-manager = { - enable = true; - }; + programs.home-manager.enable = true; home.stateVersion = mkDefault (osConfig.system.stateVersion or "25.05"); -} +} \ No newline at end of file diff --git a/modules/home/shell/default.nix b/modules/home/shell/default.nix index 9968e54..d1df4cb 100644 --- a/modules/home/shell/default.nix +++ b/modules/home/shell/default.nix @@ -17,7 +17,6 @@ in eza.enable = true; fzf.enable = true; git.enable = true; - just.enable = true; starship.enable = true; tmux.enable = true; yazi.enable = true; diff --git a/modules/home/shell/toolset/git/default.nix b/modules/home/shell/toolset/git/default.nix index dd138c8..3edfb60 100644 --- a/modules/home/shell/toolset/git/default.nix +++ b/modules/home/shell/toolset/git/default.nix @@ -1,14 +1,10 @@ -{ - config, - lib, - pkgs, - namespace, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkEnableOption mkIf; cfg = config.${namespace}.shell.toolset.git; -in { +in +{ options.${namespace}.shell.toolset.git = { enable = mkEnableOption "version-control system"; }; @@ -16,7 +12,7 @@ in { config = mkIf cfg.enable { home.sessionVariables.GITHUB_TOKEN = "$(cat /run/agenix/tokenGH)"; - home.packages = with pkgs; [lazygit lazyjj jujutsu]; + home.packages = with pkgs; [ lazygit lazyjj jujutsu ]; programs = { zsh.initContent = '' @@ -33,6 +29,12 @@ in { git = { enable = true; package = pkgs.gitFull; + difftastic = { + enable = true; + background = "dark"; + color = "always"; + display = "inline"; + }; ignores = [ # General: @@ -65,7 +67,7 @@ in { "*.elc" ]; - settings = { + extraConfig = { init.defaultBranch = "main"; core = { editor = "nvim"; @@ -102,16 +104,6 @@ in { }; }; }; - - difftastic = { - enable = true; - git.enable = true; - options = { - background = "dark"; - color = "always"; - display = "inline"; - }; - }; }; }; } diff --git a/modules/home/shell/toolset/just/default.nix b/modules/home/shell/toolset/just/default.nix deleted file mode 100644 index e956b2a..0000000 --- a/modules/home/shell/toolset/just/default.nix +++ /dev/null @@ -1,15 +0,0 @@ -{ config, lib, pkgs, namespace, ... }: -let - inherit (lib) mkEnableOption mkIf; - - cfg = config.${namespace}.shell.toolset.just; -in -{ - options.${namespace}.shell.toolset.just = { - enable = mkEnableOption "version-control system"; - }; - - config = mkIf cfg.enable { - home.packages = with pkgs; [ just gum ]; - }; -} diff --git a/modules/home/themes/default.nix b/modules/home/themes/default.nix index d338b88..276e850 100644 --- a/modules/home/themes/default.nix +++ b/modules/home/themes/default.nix @@ -26,14 +26,12 @@ in { config = mkIf (cfg.enable) { stylix = { - enable = true; + enable = true; base16Scheme = "${pkgs.base16-schemes}/share/themes/${cfg.theme}.yaml"; image = ./${cfg.theme}.jpg; polarity = cfg.polarity; - - targets.qt.platform = mkDefault "kde"; - targets.zen-browser.profileNames = [ "Chris" ]; + targets.qt.platform = mkDefault "kde6"; fonts = { serif = { @@ -52,7 +50,7 @@ in { }; emoji = { - package = pkgs.noto-fonts-color-emoji; + package = pkgs.noto-fonts-emoji; name = "Noto Color Emoji"; }; }; diff --git a/modules/nixos/application/steam/default.nix b/modules/nixos/application/steam/default.nix index 061765e..6170e8a 100644 --- a/modules/nixos/application/steam/default.nix +++ b/modules/nixos/application/steam/default.nix @@ -1,52 +1,37 @@ -{ - inputs, - config, - lib, - pkgs, - namespace, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; cfg = config.${namespace}.application.steam; -in { +in +{ options.${namespace}.application.steam = { enable = mkEnableOption "enable steam"; }; config = mkIf cfg.enable { - # environment.systemPackages = with pkgs; [ steam ]; - programs = { steam = { enable = true; - remotePlay.openFirewall = true; - dedicatedServer.openFirewall = true; - localNetworkGameTransfers.openFirewall = true; + package = pkgs.steam-small.override { + extraEnv = { + DXVK_HUD = "compiler"; + MANGOHUD = true; + }; + }; - extraCompatPackages = with pkgs; [ - proton-ge-bin - ]; - - # package = pkgs.steam.override { - # extraEnv = { - # DXVK_HUD = "compiler"; - # MANGOHUD = true; - # }; - # }; - - # gamescopeSession = { - # enable = true; - # args = ["--immediate-flips"]; - # }; + gamescopeSession = { + enable = true; + args = ["--immediate-flips"]; + }; }; # https://github.com/FeralInteractive/gamemode - # gamemode = { - # enable = true; - # enableRenice = true; - # settings = {}; - # }; + gamemode = { + enable = true; + enableRenice = true; + settings = {}; + }; # gamescope = { # enable = true; diff --git a/modules/nixos/desktop/cosmic/default.nix b/modules/nixos/desktop/cosmic/default.nix deleted file mode 100644 index cba6955..0000000 --- a/modules/nixos/desktop/cosmic/default.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ - lib, - config, - namespace, - inputs, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.desktop.cosmic; -in { - options.${namespace}.desktop.cosmic = { - enable = - mkEnableOption "Enable Cosmic desktop" - // { - default = config.${namespace}.desktop.use == "cosmic"; - }; - }; - - config = mkIf cfg.enable { - services = { - displayManager.cosmic-greeter.enable = true; - desktopManager.cosmic.enable = true; - }; - }; -} diff --git a/modules/nixos/desktop/default.nix b/modules/nixos/desktop/default.nix index 13ef881..9fd9192 100644 --- a/modules/nixos/desktop/default.nix +++ b/modules/nixos/desktop/default.nix @@ -1,22 +1,18 @@ -{ - lib, - config, - namespace, - inputs, - ... -}: let +{ lib, config, namespace, inputs, ... }: +let inherit (lib) mkIf mkOption mkEnableOption mkMerge; inherit (lib.types) nullOr enum; cfg = config.${namespace}.desktop; -in { +in +{ imports = [ inputs.grub2-themes.nixosModules.default ]; options.${namespace}.desktop = { use = mkOption { - type = nullOr (enum ["plasma" "gamescope" "gnome" "cosmic"]); + type = nullOr (enum [ "plasma" "gamescope" "gnome" ]); default = null; example = "plasma"; description = "Which desktop to enable"; @@ -24,11 +20,11 @@ in { }; config = mkMerge [ - { + ({ services.displayManager = { enable = true; }; - } + }) # (mkIf (cfg.use != null) { # ${namespace}.desktop.${cfg.use}.enable = true; diff --git a/modules/nixos/desktop/plasma/default.nix b/modules/nixos/desktop/plasma/default.nix index aa1e497..11c0cd9 100644 --- a/modules/nixos/desktop/plasma/default.nix +++ b/modules/nixos/desktop/plasma/default.nix @@ -12,18 +12,7 @@ in }; config = mkIf cfg.enable { - environment.plasma6.excludePackages = with pkgs.kdePackages; [ - elisa - kmahjongg - kmines - konversation - kpat - ksudoku - konsole - kate - ghostwriter - # oxygen - ]; + environment.plasma6.excludePackages = with pkgs.kdePackages; [ konsole kate ghostwriter oxygen ]; environment.sessionVariables.NIXOS_OZONE_WL = "1"; services = { diff --git a/modules/nixos/hardware/gpu/amd/default.nix b/modules/nixos/hardware/gpu/amd/default.nix index cdc9d1e..68db574 100644 --- a/modules/nixos/hardware/gpu/amd/default.nix +++ b/modules/nixos/hardware/gpu/amd/default.nix @@ -17,6 +17,11 @@ in }; amdgpu = { + amdvlk = { + enable = true; + support32Bit.enable = true; + }; + initrd.enable = true; }; }; diff --git a/modules/nixos/home-manager/default.nix b/modules/nixos/home-manager/default.nix deleted file mode 100644 index d147d46..0000000 --- a/modules/nixos/home-manager/default.nix +++ /dev/null @@ -1,6 +0,0 @@ -{ ... }: -{ - config = { - home-manager.backupFileExtension = "homeManagerBackup"; - }; -} diff --git a/modules/nixos/nix/default.nix b/modules/nixos/nix/default.nix index bf96f59..3104ecd 100644 --- a/modules/nixos/nix/default.nix +++ b/modules/nixos/nix/default.nix @@ -1,11 +1,15 @@ { pkgs, lib, namespace, config, ... }: let + inherit (lib) mkIf mkEnableOption; + cfg = config.${namespace}.nix; in { - options.${namespace}.nix = {}; + options.${namespace}.nix = { + enable = mkEnableOption "Enable nix command"; + }; - config = { + config = mkIf cfg.enable { programs.git.enable = true; nix = { diff --git a/modules/nixos/services/authentication/authelia/default.nix b/modules/nixos/services/authentication/authelia.nix similarity index 84% rename from modules/nixos/services/authentication/authelia/default.nix rename to modules/nixos/services/authentication/authelia.nix index 7aea103..e706439 100644 --- a/modules/nixos/services/authentication/authelia/default.nix +++ b/modules/nixos/services/authentication/authelia.nix @@ -1,36 +1,16 @@ -{ - config, - lib, - pkgs, - namespace, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption; user = "authelia-testing"; cfg = config.${namespace}.services.authentication.authelia; -in { +in +{ options.${namespace}.services.authentication.authelia = { enable = mkEnableOption "Authelia"; }; config = mkIf cfg.enable { - ${namespace}.services.networking.caddy = { - hosts = { - "auth.kruining.eu".extraConfig = '' - reverse_proxy http://127.0.0.1:9091 - ''; - }; - extraConfig = '' - (auth) { - forward_auth http://127.0.0.1:9091 { - uri /api/authz/forward-auth - copy_headers Remote-User Remote-Groups Remote-Email Remote-Name - } - } - ''; - }; - environment.systemPackages = with pkgs; [ authelia ]; @@ -132,8 +112,8 @@ in { authorization_policy = "one_factor"; userinfo_signed_response_alg = "none"; consent_mode = "implicit"; - scopes = ["openid" "profile" "groups"]; - redirect_uris = ["https://jellyfin.kruining.eu/sso/OID/redirect/authelia"]; + scopes = [ "openid" "profile" "groups" ]; + redirect_uris = [ "https://jellyfin.kruining.eu/sso/OID/redirect/authelia" ]; } { client_id = "streamarr"; @@ -147,25 +127,8 @@ in { authorization_policy = "one_factor"; userinfo_signed_response_alg = "none"; consent_mode = "implicit"; - scopes = ["offline_access" "openid" "email" "picture" "profile" "groups"]; - redirect_uris = ["http://localhost:3000/api/auth/oauth2/callback/authelia"]; - } - { - client_id = "forgejo"; - client_name = "forgejo"; - # ZPuiW2gpVV6MGXIJFk5P3EeSW8V_ICgqduF.hJVCKkrnVmRqIQXRk0o~HSA8ZdCf8joA4m_F - client_secret = "$pbkdf2-sha512$310000$CzZjvJT75bz5z7MjwxsEtg$JtOiIgaY5/HcLLxJgyX4zvsQV9jIoow0e4JdlFsk/LWRDOJ0kc.PzstlYfw7QERTXtJILoWsDqPzmvpneK5Leg"; - public = false; - require_pkce = true; - pkce_challenge_method = "S256"; - token_endpoint_auth_method = "client_secret_post"; - authorization_policy = "one_factor"; - userinfo_signed_response_alg = "none"; - consent_mode = "implicit"; - scopes = ["offline_access" "openid" "email" "picture" "profile" "groups"]; - response_types = ["code"]; - grant_types = ["authorization_code"]; - redirect_uris = ["http://localhost:5002/user/oauth2/authelia/callback"]; + scopes = [ "offline_access" "openid" "email" "picture" "profile" "groups" ]; + redirect_uris = [ "http://localhost:3000/api/auth/oauth2/callback/authelia" ]; } ]; }; @@ -215,8 +178,48 @@ in { - jellyfin-users - admin - dev + + jacqueline: + disabled: false + displayname: Jacqueline Bevers + password: $argon2id$v=19$m=65536,t=3,p=4$XgN8yEJV+syAE5yeos3HsA$SlN+j/lJfxJ5VxLu2CdrwowlCiWQNNGhIrSyDpohq18 + groups: + - jellyfin-users + + martijn: + disabled: false + displayname: Martijn Kruining + password: $argon2id$v=19$m=65536,t=3,p=4$XgN8yEJV+syAE5yeos3HsA$SlN+j/lJfxJ5VxLu2CdrwowlCiWQNNGhIrSyDpohq18 + groups: + - jellyfin-users + + andrea: + disabled: false + displayname: Andrea Kruining + password: $argon2id$v=19$m=65536,t=3,p=4$XgN8yEJV+syAE5yeos3HsA$SlN+j/lJfxJ5VxLu2CdrwowlCiWQNNGhIrSyDpohq18 + groups: + - jellyfin-users ''; }; }; + + services.caddy = { + enable = true; + virtualHosts = { + "auth.kruining.eu".extraConfig = '' + reverse_proxy http://127.0.0.1:9091 + ''; + }; + extraConfig = '' + (auth) { + forward_auth http://127.0.0.1:9091 { + uri /api/authz/forward-auth + copy_headers Remote-User Remote-Groups Remote-Email Remote-Name + } + } + ''; + }; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; }; } diff --git a/modules/nixos/services/authentication/default.nix b/modules/nixos/services/authentication/default.nix new file mode 100644 index 0000000..c157af7 --- /dev/null +++ b/modules/nixos/services/authentication/default.nix @@ -0,0 +1 @@ +{ ... }: {} diff --git a/modules/nixos/services/authentication/himmelblau/default.nix b/modules/nixos/services/authentication/himmelblau.nix similarity index 75% rename from modules/nixos/services/authentication/himmelblau/default.nix rename to modules/nixos/services/authentication/himmelblau.nix index d39d4cf..1228759 100644 --- a/modules/nixos/services/authentication/himmelblau/default.nix +++ b/modules/nixos/services/authentication/himmelblau.nix @@ -1,15 +1,10 @@ -{ - inputs, - lib, - config, - namespace, - ... -}: let +{ inputs, lib, config, namespace, ... }: let inherit (lib) mkEnableOption mkIf; cfg = config.${namespace}.services.authentication.himmelblau; -in { - imports = [inputs.himmelblau.nixosModules.himmelblau]; +in +{ + imports = [ inputs.himmelblau.nixosModules.himmelblau ]; options.${namespace}.services.authentication.himmelblau = { enable = mkEnableOption "enable azure entra ID authentication"; @@ -19,7 +14,7 @@ in { services.himmelblau = { enable = true; settings = { - domain = ""; + domains = []; pam_allow_groups = []; local_groups = []; }; diff --git a/modules/nixos/services/authentication/zitadel.nix b/modules/nixos/services/authentication/zitadel.nix new file mode 100644 index 0000000..6142857 --- /dev/null +++ b/modules/nixos/services/authentication/zitadel.nix @@ -0,0 +1,86 @@ +{ config, lib, pkgs, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.services.authentication.zitadel; + + db_name = "zitadel"; + db_user = "zitadel"; +in +{ + options.${namespace}.services.authentication.zitadel = { + enable = mkEnableOption "Zitadel"; + }; + + config = mkIf cfg.enable { + environment.systemPackages = with pkgs; [ + zitadel + ]; + + services = { + zitadel = { + enable = true; + openFirewall = true; + masterKeyFile = config.sops.secrets."zitadel/masterKey".path; + tlsMode = "external"; + settings = { + Port = 9092; + Database = { + Host = "/run/postgresql"; + # Zitadel will report error if port is not set + Port = 5432; + Database = db_name; + User.Username = db_user; + }; + }; + steps = { + TestInstance = { + InstanceName = "Zitadel test"; + Org = { + Name = "Kruining.eu"; + Human = { + UserName = "admin"; + Password = "kaas"; + }; + }; + }; + }; + }; + + postgresql = { + enable = true; + ensureDatabases = [ db_name ]; + ensureUsers = [ + { + name = db_user; + ensureDBOwnership = true; + } + ]; + }; + + caddy = { + enable = true; + virtualHosts = { + "auth-z.kruining.eu".extraConfig = '' + reverse_proxy h2c://127.0.0.1:9092 + ''; + }; + # extraConfig = '' + # (auth) { + # forward_auth h2c://127.0.0.1:9092 { + # uri /api/authz/forward-auth + # copy_headers Remote-User Remote-Groups Remote-Email Remote-Name + # } + # } + # ''; + }; + }; + + # Secrets + sops.secrets."zitadel/masterKey" = { + owner = "zitadel"; + group = "zitadel"; + restartUnits = [ "zitadel.service" ]; + }; + }; +} diff --git a/modules/nixos/services/authentication/zitadel/default.nix b/modules/nixos/services/authentication/zitadel/default.nix deleted file mode 100644 index 6e42eeb..0000000 --- a/modules/nixos/services/authentication/zitadel/default.nix +++ /dev/null @@ -1,732 +0,0 @@ -{ config, lib, pkgs, namespace, system, inputs, ... }: -let - inherit (lib) mkIf mkEnableOption mkOption toString types toUpper toSentenceCase nameValuePair mapAttrs mapAttrs' concatMapAttrs concatMapStringsSep filterAttrsRecursive listToAttrs imap0 head drop length literalExpression attrNames; - inherit (lib.${namespace}.strings) toSnakeCase; - - cfg = config.${namespace}.services.authentication.zitadel; - port = 3010; - - database = "zitadel"; -in -{ - options.${namespace}.services.authentication.zitadel = { - enable = mkEnableOption "Zitadel"; - - organization = mkOption { - type = types.attrsOf (types.submodule ({ name, ... }: { - options = - let - org = name; - in - { - isDefault = mkOption { - type = types.bool; - default = false; - example = "true"; - description = '' - True sets the '${org}' org as default org for the instance. Only one org can be default org. - Nothing happens if you set it to false until you set another org as default org. - ''; - }; - - project = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - hasProjectCheck = mkOption { - type = types.bool; - default = false; - example = "true"; - description = '' - ZITADEL checks if the org of the user has permission to this project. - ''; - }; - - privateLabelingSetting = mkOption { - type = types.nullOr (types.enum [ "unspecified" "enforceProjectResourceOwnerPolicy" "allowLoginUserResourceOwnerPolicy" ]); - default = null; - example = "enforceProjectResourceOwnerPolicy"; - description = '' - Defines from where the private labeling should be triggered, - - supported values: - - unspecified - - enforceProjectResourceOwnerPolicy - - allowLoginUserResourceOwnerPolicy - ''; - }; - - projectRoleAssertion = mkOption { - type = types.bool; - default = false; - example = "true"; - description = '' - Describes if roles of user should be added in token. - ''; - }; - - projectRoleCheck = mkOption { - type = types.bool; - default = false; - example = "true"; - description = '' - ZITADEL checks if the user has at least one on this project. - ''; - }; - - role = mkOption { - default = {}; - type = types.attrsOf (types.submodule ({ name, ... }: { - options = - let - roleName = name; - in - { - displayName = mkOption { - type = types.str; - default = toSentenceCase name; - example = "RoleName"; - description = '' - Name used for project role. - ''; - }; - - group = mkOption { - type = types.nullOr types.str; - default = null; - example = "some_group"; - description = '' - Group used for project role. - ''; - }; - }; - })); - }; - - assign = mkOption { - default = {}; - type = types.attrsOf (types.listOf types.str); - }; - - application = mkOption { - default = {}; - type = types.attrsOf (types.submodule { - options = { - redirectUris = mkOption { - type = types.nonEmptyListOf types.str; - example = '' - [ "https://example.com/redirect/url" ] - ''; - description = '' - . - ''; - }; - - grantTypes = mkOption { - type = types.nonEmptyListOf (types.enum [ "authorizationCode" "implicit" "refreshToken" "deviceCode" "tokenExchange" ]); - example = '' - [ "authorizationCode" ] - ''; - description = '' - . - ''; - }; - - responseTypes = mkOption { - type = types.nonEmptyListOf (types.enum [ "code" "idToken" "idTokenToken" ]); - example = '' - [ "code" ] - ''; - description = '' - . - ''; - }; - - exportMap = - let - strOpt = mkOption { type = types.nullOr types.str; default = null; }; - in - mkOption { - type = types.submodule { options = { client_id = strOpt; client_secret = strOpt; }; }; - default = {}; - example = literalExpression '' - { - client_id = "SSO_CLIENT_ID"; - client_secret = "SSO_CLIENT_SECRET"; - } - ''; - description = '' - Remap the outputted variables to another key. - ''; - }; - }; - }); - }; - }; - }); - }; - - user = mkOption { - default = {}; - type = types.attrsOf (types.submodule ({ name, ... }: { - options = - let - username = name; - in - { - email = mkOption { - type = types.str; - example = "someone@some.domain"; - description = '' - Username. - ''; - }; - - userName = mkOption { - type = types.nullOr types.str; - default = username; - example = "some_user_name"; - description = '' - Username. Default value is the key of the config object you created, you can overwrite that by setting this option - ''; - }; - - firstName = mkOption { - type = types.str; - example = "John"; - description = '' - First name of the user. - ''; - }; - - lastName = mkOption { - type = types.str; - example = "Doe"; - description = '' - Last name of the user. - ''; - }; - - roles = mkOption { - type = types.listOf types.str; - default = []; - example = "[ \"ORG_OWNER\" ]"; - description = '' - List of roles granted to organisation. - ''; - }; - - instanceRoles = mkOption { - type = types.listOf types.str; - default = []; - example = "[ \"IAM_OWNER\" ]"; - description = '' - List of roles granted to instance. - ''; - }; - }; - })); - }; - - action = mkOption { - default = {}; - type = types.attrsOf (types.submodule ({ name, ... }: { - options = { - script = mkOption { - type = types.str; - example = '' - (ctx, api) => { - api.v1.claims.setClaim('some_claim', 'some_value'); - }; - ''; - description = '' - The script to run. This must be a function that receives 2 parameters, and returns void. During the creation of the action's script this module simly does `const {{name}} = {{script}}`. - ''; - }; - - timeout = mkOption { - type = (types.ints.between 0 20); - default = 10; - example = "10"; - description = '' - After which time the action will be terminated if not finished. - ''; - }; - - allowedToFail = mkOption { - type = types.bool; - default = true; - example = "true"; - description = '' - Allowed to fail. - ''; - }; - }; - })); - }; - - triggers = mkOption { - default = []; - type = types.listOf (types.submodule { - options = { - flowType = mkOption { - type = types.enum [ "authentication" "customiseToken" "internalAuthentication" "samlResponse" ]; - example = "customiseToken"; - description = '' - Type of the flow to which the action triggers belong. - ''; - }; - - triggerType = mkOption { - type = types.enum [ "postAuthentication" "preCreation" "postCreation" "preUserinfoCreation" "preAccessTokenCreation" "preSamlResponse" ]; - example = "postAuthentication"; - description = '' - Trigger type on when the actions get triggered. - ''; - }; - - actions = mkOption { - type = types.nonEmptyListOf types.str; - example = ''[ "action_name" ]''; - description = '' - Names of actions to trigger - ''; - }; - }; - }); - }; - }; - })); - }; - }; - - config = let - _refTypeMap = { - org = { type = "org"; }; - project = { type = "project"; }; - user = { type = "user"; tfType = "human_user"; }; - }; - - mapRef' = { type, tfType ? type }: name: { "${type}Id" = "\${ resource.zitadel_${tfType}.${toSnakeCase name}.id }"; }; - mapRef = type: name: mapRef' (_refTypeMap.${type}) name; - mapEnum = prefix: value: "${prefix}_${value |> toSnakeCase |> toUpper}"; - - mapValue = type: value: ({ - appType = mapEnum "OIDC_APP_TYPE" value; - grantTypes = map (t: mapEnum "OIDC_GRANT_TYPE" t) value; - responseTypes = map (t: mapEnum "OIDC_RESPONSE_TYPE" t) value; - authMethodType = mapEnum "OIDC_AUTH_METHOD_TYPE" value; - - flowType = mapEnum "FLOW_TYPE" value; - triggerType = mapEnum "TRIGGER_TYPE" value; - accessTokenType = mapEnum "OIDC_TOKEN_TYPE" value; - }."${type}" or value); - - toResource = name: value: nameValuePair - (toSnakeCase name) - (lib.mapAttrs' (k: v: nameValuePair (toSnakeCase k) (mapValue k v)) value); - - withRef = type: name: attrs: attrs // (mapRef type name); - - select = keys: callback: set: - if (length keys) == 0 then - mapAttrs' callback set - else let key = head keys; in - concatMapAttrs (k: v: select (drop 1 keys) (callback k) (v.${key} or {})) set - ; - - append = attrList: set: set // (listToAttrs attrList); - - config' = config; - - # this is a nix package, the generated json file to be exact - terraformConfiguration = inputs.terranix.lib.terranixConfiguration { - inherit system; - - modules = [ - ({ config, lib, ... }: { - config = - let - forEach = src: key: set: - let - _key = concatMapStringsSep "_" (k: "\${item.${k}}") key; - in - { - forEach = lib.tfRef ''{ - for item in ${src} : - "''${item.org}_''${item.name}" => item - }''; - } - // set; - in - { - terraform.required_providers.zitadel = { - source = "zitadel/zitadel"; - version = "2.2.0"; - }; - - provider.zitadel = { - domain = "auth.kruining.eu"; - insecure = "false"; - jwt_profile_file = "/var/lib/zitadel/machine-key.json"; - }; - - locals = { - extra_users = lib.tfRef " - flatten([ for org, users in jsondecode(file(\"${config'.sops.secrets."zitadel/users".path}\")): [ - for name, details in users: { - org = org - name = name - email = details.email - firstName = details.firstName - lastName = details.lastName - } - ] ]) - "; - orgs = cfg.organization |> mapAttrs (org: _: lib.tfRef "resource.zitadel_org.${org}.id"); - }; - - resource = { - # Organizations - zitadel_org = cfg.organization |> select [] (name: { isDefault, ... }: - { inherit name isDefault; } - |> toResource name - ); - - # Projects per organization - zitadel_project = cfg.organization |> select [ "project" ] (org: name: { hasProjectCheck, privateLabelingSetting, projectRoleAssertion, projectRoleCheck, ... }: - { - inherit name hasProjectCheck privateLabelingSetting projectRoleAssertion projectRoleCheck; - } - |> withRef "org" org - |> toResource "${org}_${name}" - ); - - # Each OIDC app per project - zitadel_application_oidc = cfg.organization |> select [ "project" "application" ] (org: project: name: { redirectUris, grantTypes, responseTypes, ...}: - { - inherit name redirectUris grantTypes responseTypes; - - accessTokenRoleAssertion = true; - idTokenRoleAssertion = true; - accessTokenType = "JWT"; - } - |> withRef "org" org - |> withRef "project" "${org}_${project}" - |> toResource "${org}_${project}_${name}" - ); - - # Each project role - zitadel_project_role = cfg.organization |> select [ "project" "role" ] (org: project: name: value: - { inherit (value) displayName group; roleKey = name; } - |> withRef "org" org - |> withRef "project" "${org}_${project}" - |> toResource "${org}_${project}_${name}" - ); - - # Each project role assignment - zitadel_user_grant = cfg.organization |> select [ "project" "assign" ] (org: project: user: roles: - { roleKeys = roles; } - |> withRef "org" org - |> withRef "project" "${org}_${project}" - |> withRef "user" "${org}_${user}" - |> toResource "${org}_${project}_${user}" - ); - - # Users - zitadel_human_user = - cfg.organization - |> select [ "user" ] (org: name: { email, userName, firstName, lastName, ... }: - { - inherit email userName firstName lastName; - - isEmailVerified = true; - } - |> withRef "org" org - |> toResource "${org}_${name}" - ) - |> append [ - (forEach "local.extra_users" [ "org" "name" ] { - orgId = lib.tfRef "local.orgs[each.value.org]"; - userName = lib.tfRef "each.value.name"; - email = lib.tfRef "each.value.email"; - firstName = lib.tfRef "each.value.firstName"; - lastName = lib.tfRef "each.value.lastName"; - - isEmailVerified = true; - } - |> toResource "extraUsers") - ] - ; - - # Global user roles - zitadel_instance_member = - cfg.organization - |> filterAttrsRecursive (n: v: !(v ? "instanceRoles" && (length v.instanceRoles) == 0)) - |> select [ "user" ] (org: name: { instanceRoles, ... }: - { roles = instanceRoles; } - |> withRef "user" "${org}_${name}" - |> toResource "${org}_${name}" - ); - - # Organazation specific roles - zitadel_org_member = - cfg.organization - |> filterAttrsRecursive (n: v: !(v ? "roles" && (length v.roles) == 0)) - |> select [ "user" ] (org: name: { roles, ... }: - { inherit roles; } - |> withRef "org" org - |> withRef "user" "${org}_${name}" - |> toResource "${org}_${name}" - ); - - # Organazation's actions - zitadel_action = cfg.organization |> select [ "action" ] (org: name: { timeout, allowedToFail, script, ...}: - { - inherit allowedToFail name; - timeout = "${toString timeout}s"; - script = "const ${name} = ${script}"; - } - |> withRef "org" org - |> toResource "${org}_${name}" - ); - - # Organazation's action assignments - zitadel_trigger_actions = - cfg.organization - |> concatMapAttrs (org: { triggers, ... }: - triggers - |> imap0 (i: { flowType, triggerType, actions, ... }: (let name = "trigger_${toString i}"; in - { - inherit flowType triggerType; - - actionIds = - actions - |> map (action: (lib.tfRef "zitadel_action.${org}_${toSnakeCase action}.id")); - } - |> withRef "org" org - |> toResource "${org}_${name}" - )) - |> listToAttrs - ); - - # SMTP config - zitadel_smtp_config.default = { - sender_address = "chris@kruining.eu"; - sender_name = "no-reply (Zitadel)"; - tls = true; - host = "black-mail.nl:587"; - user = "chris@kruining.eu"; - password = lib.tfRef "file(\"${config'.sops.secrets."zitadel/email".path}\")"; - set_active = true; - }; - - # Client credentials per app - local_sensitive_file = cfg.organization |> select [ "project" "application" ] (org: project: name: { exportMap, ... }: - nameValuePair "${org}_${project}_${name}" { - content = '' - ${if exportMap.client_id != null then exportMap.client_id else "CLIENT_ID"}=${lib.tfRef "resource.zitadel_application_oidc.${org}_${project}_${name}.client_id"} - ${if exportMap.client_secret != null then exportMap.client_secret else "CLIENT_SECRET"}=${lib.tfRef "resource.zitadel_application_oidc.${org}_${project}_${name}.client_secret"} - ''; - filename = "/var/lib/zitadel/clients/${org}_${project}_${name}"; - } - ); - }; - }; - }) - ]; - }; - in - mkIf cfg.enable { - ${namespace}.services = { - persistance.postgresql.enable = true; - - networking.caddy = { - hosts = { - "auth.kruining.eu" = '' - reverse_proxy h2c://[::1]:${toString port} - ''; - }; - extraConfig = '' - (auth) { - forward_auth h2c://[::1]:${toString port} { - uri /api/authz/forward-auth - copy_headers Remote-User Remote-Groups Remote-Email Remote-Name - } - } - ''; - }; - }; - - environment.systemPackages = with pkgs; [ - zitadel - ]; - - systemd.tmpfiles.rules = [ - "d /tmp/zitadelApplyTerraform 0755 zitadel zitadel -" - "d /var/lib/zitadel/clients 0755 zitadel zitadel -" - ]; - - systemd.services.zitadelApplyTerraform = { - description = "Zitadel terraform apply"; - - wantedBy = [ "multi-user.target" ]; - wants = [ "zitadel.service" ]; - - script = - let - tofu = lib.getExe pkgs.opentofu; - in - '' - if [ "$(systemctl is-active zitadel)" != "active" ]; then - echo "Zitadel is not running" - exit 1 - fi - - # Print the path to the source for easier debugging - echo "config location: ${terraformConfiguration}" - - # Copy infra code into workspace - cp -f ${terraformConfiguration} config.tf.json - - # Initialize OpenTofu - ${tofu} init - - # Run the infrastructure code - ${tofu} plan -refresh=false -out=tfplan - ${tofu} apply -auto-approve tfplan - ''; - - serviceConfig = { - Type = "oneshot"; - User = "zitadel"; - Group = "zitadel"; - - WorkingDirectory = "/tmp/zitadelApplyTerraform"; - }; - }; - - services = { - zitadel = { - enable = true; - openFirewall = true; - masterKeyFile = config.sops.secrets."zitadel/masterKey".path; - tlsMode = "external"; - settings = { - Port = port; - - ExternalDomain = "auth.kruining.eu"; - ExternalPort = 443; - ExternalSecure = true; - - Metrics.Type = "otel"; - Tracing.Type = "otel"; - Telemetry.Enabled = true; - - SystemDefaults = { - PasswordHasher.Hasher.Algorithm = "argon2id"; - SecretHasher.Hasher.Algorithm = "argon2id"; - }; - - Database.postgres = { - Host = "localhost"; - # Zitadel will report error if port is not set - Port = 5432; - Database = database; - User = { - Username = database; - SSL.Mode = "disable"; - }; - Admin = { - Username = "postgres"; - SSL.Mode = "disable"; - }; - }; - }; - steps = { - FirstInstance = { - # Not sure, this option seems to be mostly irrelevant - InstanceName = "eu"; - - MachineKeyPath = "/var/lib/zitadel/machine-key.json"; - # PatPath = "/var/lib/zitadel/machine-key.pat"; - # LoginClientPatPath = "/var/lib/zitadel/machine-key.json"; - - Org = { - Name = "kruining"; - - Human = { - UserName = "chris"; - FirstName = "Chris"; - LastName = "Kruining"; - Email = { - Address = "chris@kruining.eu"; - Verified = true; - }; - Password = "KaasIsAwesome1!"; - }; - - Machine = { - Machine = { - Username = "terraform-service-user"; - Name = "Terraform"; - }; - MachineKey = { ExpirationDate = "2026-01-01T00:00:00Z"; Type = 1; }; - # Pat = { ExpirationDate = "2026-01-01T00:00:00Z"; }; - }; - - # LoginClient.Machine = { - # Username = "terraform-service-user"; - # Name = "Terraform"; - # }; - }; - }; - }; - # extraStepsPaths = [ - # config.sops.templates."secrets.yaml".path - # ]; - }; - - postgresql = { - enable = true; - ensureDatabases = [ database ]; - ensureUsers = [ - { - name = database; - ensureDBOwnership = true; - } - ]; - }; - }; - - # Secrets - sops = { - secrets = { - "zitadel/masterKey" = { - owner = "zitadel"; - group = "zitadel"; - restartUnits = [ "zitadel.service" ]; #EMGDB#6O$8qpGoLI1XjhUhnng1san@0 - }; - - "zitadel/email" = { - owner = "zitadel"; - group = "zitadel"; - key = "email/chris_kruining_eu"; - restartUnits = [ "zitadel.service" ]; - }; - - "zitadel/users" = { - owner = "zitadel"; - group = "zitadel"; - restartUnits = [ "zitadelApplyTerraform.service" ]; - }; - }; - - templates = { - "users.yml" = { - - }; - }; - }; - }; -} diff --git a/modules/nixos/services/backup/borg/default.nix b/modules/nixos/services/backup/borg/default.nix deleted file mode 100644 index 9cbbea0..0000000 --- a/modules/nixos/services/backup/borg/default.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ config, lib, pkgs, namespace, ... }: -let - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.services.backup.borg; -in -{ - options.${namespace}.services.backup.borg = { - enable = mkEnableOption "Borg Backup"; - }; - - config = mkIf cfg.enable { - programs.ssh.extraConfig = '' - Host beheer.hazelhof.nl - Port 222 - User chris - AddressFamily inet - IdentityFile /home/chris/.ssh/id_ed25519 - ''; - - services = { - borgbackup.jobs = { - media = { - paths = "/var/media/test"; - encryption.mode = "none"; - # environment.BORG_SSH = "ssh -4 -i /home/chris/.ssh/id_ed25519"; - environment.BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK = "yes"; - repo = "ssh://beheer.hazelhof.nl//media"; - compression = "auto,zstd"; - startAt = "daily"; - }; - }; - }; - }; -} diff --git a/modules/nixos/services/communication/matrix/default.nix b/modules/nixos/services/communication/matrix/default.nix deleted file mode 100644 index 9a7d53c..0000000 --- a/modules/nixos/services/communication/matrix/default.nix +++ /dev/null @@ -1,399 +0,0 @@ -{ - config, - lib, - pkgs, - namespace, - ... -}: let - inherit (builtins) toString toJSON; - inherit (lib) mkIf mkEnableOption mkMerge; - - cfg = config.${namespace}.services.communication.matrix; - - domain = "kruining.eu"; - fqn = "matrix.${domain}"; - port = 4001; - - database = "synapse"; - keyFile = "/var/lib/element-call/key"; - - mkMautrix = bridge: i: conf: { - ${bridge} = mkMerge [ - { - enable = true; - registerToSynapse = true; - - settings = { - appservice = { - # hostname = "[::]"; - # port = 40010 + i; - # address = "http://${config.services.${bridge}.settings.appservice.hostname}:${toString config.services.${bridge}.settings.appservice.port}"; - provisioning.enabled = false; - }; - - homeserver = { - inherit domain; - address = "http://[::1]:${toString port}"; - }; - - bridge = { - permissions = { - "@chris:${domain}" = "admin"; - }; - }; - }; - } - conf - ]; - }; -in { - options.${namespace}.services.communication.matrix = { - enable = mkEnableOption "Matrix server (Synapse)"; - }; - - config = mkIf cfg.enable { - ${namespace}.services = { - persistance.postgresql.enable = true; - - networking.caddy = { - hosts = let - server = { - "m.server" = "${fqn}:443"; - }; - client = { - "m.homeserver".base_url = "https://${fqn}"; - "m.identity_server".base_url = "https://auth.${domain}"; - "org.matrix.msc3575.proxy".url = "https://${domain}"; - "org.matrix.msc4143.rtc_foci" = [ - { - type = "livekit"; - livekit_service_url = "https://${domain}/livekit/jwt"; - } - ]; - }; - in { - "${domain}, darkch.at" = '' - # Route for lk-jwt-service - handle /livekit/jwt* { - uri strip_prefix /livekit/jwt - reverse_proxy http://[::1]:${toString config.services.lk-jwt-service.port} { - header_up Host {host} - header_up X-Forwarded-Server {host} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - } - } - - handle_path /livekit/sfu* { - reverse_proxy http://[::1]:${toString config.services.livekit.settings.port} { - header_up Host {host} - header_up X-Forwarded-Server {host} - header_up X-Real-IP {remote_host} - header_up X-Forwarded-For {remote_host} - } - } - - header /.well-known/matrix/* Content-Type application/json - header /.well-known/matrix/* Access-Control-Allow-Origin * - respond /.well-known/matrix/server `${toJSON server}` - respond /.well-known/matrix/client `${toJSON client}` - ''; - - "${fqn}" = '' - reverse_proxy /_matrix/* http://[::1]:${toString port} - reverse_proxy /_synapse/client/* http://[::1]:${toString port} - ''; - }; - }; - }; - - services = mkMerge [ - (mkMautrix "mautrix-signal" 1 {}) - (mkMautrix "mautrix-telegram" 2 {}) - (mkMautrix "mautrix-whatsapp" 3 {}) - (mkMautrix "arrtrix" 4 { - environmentFile = config.sops.templates."arrtrix/secrets".path; - - settings = { - observability = { - otlp_grpc_endpoint = "http://[::1]:9071"; - service_name = "arrtrix"; - }; - - network.content = { - movies = { - url = "http://[::1]:${toString config.services.radarr.settings.server.port}"; - api_key = "$RADARR_APIKEY"; - root_folder_path = "/var/media/movies"; - quality_profile_id = 5; - }; - series = { - url = "http://[::1]:${toString config.services.sonarr.settings.server.port}"; - api_key = "$SONARR_APIKEY"; - root_folder_path = "/var/media/series"; - quality_profile_id = 5; - language_profile_id = 1; - }; - }; - }; - }) - { - matrix-synapse = { - enable = true; - - extras = ["oidc"]; - - extraConfigFiles = [ - config.sops.templates."synapse.yaml".path - config.sops.templates."synapse-oidc.yaml".path - ]; - - settings = { - server_name = domain; - public_baseurl = "https://${fqn}"; - - enable_metrics = true; - - url_preview_enabled = true; - precence.enabled = true; - - # Since we'll be using OIDC for auth disable all local options - enable_registration = false; - enable_registration_without_verification = false; - password_config.enabled = true; - backchannel_logout_enabled = true; - - # Element Call options - max_event_delay_duration = "24h"; - rc_message = { - per_second = 0.5; - burst_count = 30; - }; - rc_delayed_event_mgmt = { - per_second = 1; - burst_count = 20; - }; - turn_uris = ["turn:turn.${domain}:4004?transport=udp" "turn:turn.${domain}:4004?transport=tcp"]; - - experimental_features = { - # MSC2965: OAuth 2.0 Authorization Server Metadata discovery - msc2965_enabled = true; - - # MSC3266: Room summary API. Used for knocking over federation - msc3266_enabled = true; - # MSC4222 needed for syncv2 state_after. This allow clients to - # correctly track the state of the room. - msc4222_enabled = true; - }; - - sso = { - client_whitelist = ["http://[::1]:${toString config.services.zitadel.settings.Port}/" "https://auth.kruining.eu/"]; - update_profile_information = true; - }; - - database = { - # this is postgresql (also the default, but I prefer to be explicit) - name = "psycopg2"; - args = { - database = database; - user = database; - }; - }; - - listeners = [ - { - bind_addresses = ["::"]; - port = port; - type = "http"; - tls = false; - x_forwarded = true; - - resources = [ - { - names = ["client" "federation" "openid" "metrics" "media" "health"]; - compress = true; - } - ]; - } - ]; - }; - }; - - postgresql = { - ensureDatabases = [database]; - ensureUsers = [ - { - name = database; - ensureDBOwnership = true; - } - ]; - }; - - livekit = { - enable = true; - openFirewall = true; - inherit keyFile; - - settings = { - port = 4002; - room.auto_create = false; - }; - }; - - lk-jwt-service = { - enable = true; - port = 4003; - # can be on the same virtualHost as synapse - livekitUrl = "wss://${domain}/livekit/sfu"; - inherit keyFile; - }; - - coturn = rec { - enable = true; - listening-port = 4004; - tls-listening-port = 40004; - no-cli = true; - no-tcp-relay = true; - min-port = 50000; - max-port = 50100; - use-auth-secret = true; - static-auth-secret-file = config.sops.secrets."coturn/secret".path; - realm = "turn.${domain}"; - # cert = "${config.security.acme.certs.${realm}.directory}/full.pem"; - # pkey = "${config.security.acme.certs.${realm}.directory}/key.pem"; - extraConfig = '' - # for debugging - verbose - # ban private IP ranges - no-multicast-peers - denied-peer-ip=0.0.0.0-0.255.255.255 - denied-peer-ip=10.0.0.0-10.255.255.255 - denied-peer-ip=100.64.0.0-100.127.255.255 - denied-peer-ip=127.0.0.0-127.255.255.255 - denied-peer-ip=169.254.0.0-169.254.255.255 - denied-peer-ip=172.16.0.0-172.31.255.255 - denied-peer-ip=192.0.0.0-192.0.0.255 - denied-peer-ip=192.0.2.0-192.0.2.255 - denied-peer-ip=192.88.99.0-192.88.99.255 - denied-peer-ip=192.168.0.0-192.168.255.255 - denied-peer-ip=198.18.0.0-198.19.255.255 - denied-peer-ip=198.51.100.0-198.51.100.255 - denied-peer-ip=203.0.113.0-203.0.113.255 - denied-peer-ip=240.0.0.0-255.255.255.255 - denied-peer-ip=::1 - denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff - denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 - denied-peer-ip=100::-100::ffff:ffff:ffff:ffff - denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff - ''; - }; - } - ]; - - networking.firewall = { - allowedTCPPortRanges = []; - allowedTCPPorts = [ - # Synapse - port - - # coTURN ports - config.services.coturn.listening-port - config.services.coturn.alt-listening-port - config.services.coturn.tls-listening-port - config.services.coturn.alt-tls-listening-port - ]; - allowedUDPPortRanges = with config.services.coturn; - lib.singleton { - from = min-port; - to = max-port; - }; - allowedUDPPorts = [ - # coTURN ports - config.services.coturn.listening-port - config.services.coturn.alt-listening-port - ]; - }; - - systemd = { - services.livekit-key = { - before = ["lk-jwt-service.service" "livekit.service"]; - wantedBy = ["multi-user.target"]; - path = with pkgs; [livekit coreutils gawk]; - script = '' - echo "Key missing, generating key" - echo "lk-jwt-service: $(livekit-server generate-keys | tail -1 | awk '{print $3}')" > "${keyFile}" - ''; - serviceConfig.Type = "oneshot"; - unitConfig.ConditionPathExists = "!${keyFile}"; - }; - services.lk-jwt-service.environment.LIVEKIT_FULL_ACCESS_HOMESERVERS = "${domain}"; - }; - - sops = { - secrets = { - "synapse/oidc_id" = { - restartUnits = ["synapse-matrix.service"]; - }; - "synapse/oidc_secret" = { - restartUnits = ["synapse-matrix.service"]; - }; - "synapse/shared_secret" = { - restartUnits = ["synapse-matrix.service"]; - }; - "coturn/secret" = { - owner = config.systemd.services.coturn.serviceConfig.User; - group = config.systemd.services.coturn.serviceConfig.Group; - restartUnits = ["coturn.service"]; - }; - }; - - templates = { - "synapse.yaml" = { - owner = "matrix-synapse"; - content = '' - registration_shared_secret: ${config.sops.placeholder."synapse/shared_secret"} - ''; - restartUnits = ["matrix-synapse.service"]; - }; - "synapse-oidc.yaml" = { - owner = "matrix-synapse"; - content = '' - oidc_providers: - - discover: true - idp_id: zitadel - idp_name: Zitadel - issuer: "https://auth.kruining.eu" - scopes: - - openid - - profile - - email - - offline_access - client_id: '${config.sops.placeholder."synapse/oidc_id"}' - client_secret: '${config.sops.placeholder."synapse/oidc_secret"}' - backchannel_logout_enabled: true - user_profile_method: userinfo_endpoint - allow_existing_users: true - enable_registration: true - user_mapping_provider: - config: - localpart_template: "{{ user.preferred_username }}" - display_name_template: "{{ user.name }}" - email_template: "{{ user.email }}" - ''; - restartUnits = ["matrix-synapse.service"]; - }; - "arrtrix/secrets" = { - owner = "arrtrix"; - content = '' - RADARR_APIKEY=${config.sops.placeholder."radarr/apikey"} - SONARR_APIKEY=${config.sops.placeholder."sonarr/apikey"} - ''; - restartUnits = ["arrtrix.service"]; - }; - }; - }; - }; -} diff --git a/modules/nixos/services/development/forgejo/default.nix b/modules/nixos/services/development/forgejo/default.nix deleted file mode 100644 index f190b0c..0000000 --- a/modules/nixos/services/development/forgejo/default.nix +++ /dev/null @@ -1,210 +0,0 @@ -{ - config, - lib, - pkgs, - namespace, - ... -}: let - inherit (builtins) toString; - inherit (lib) mkIf mkEnableOption mkOption; - - cfg = config.${namespace}.services.development.forgejo; - domain = "git.amarth.cloud"; -in { - options.${namespace}.services.development.forgejo = { - enable = mkEnableOption "Forgejo"; - - port = mkOption { - type = lib.types.port; - default = 5002; - example = "1234"; - description = '' - Which port to bind forgejo to - ''; - }; - }; - - config = mkIf cfg.enable { - ${namespace}.services = { - persistance.postgresql.enable = true; - virtualisation.podman.enable = true; - - networking.caddy = { - hosts = { - "${domain}" = '' - # import auth - - # stupid dumb way to prevent the login page and go to zitadel instead - # be aware that this does not disable local login at all! - # rewrite /user/login /user/oauth2/Zitadel - - reverse_proxy http://127.0.0.1:${toString cfg.port} - ''; - }; - }; - }; - - environment.systemPackages = with pkgs; [forgejo]; - - services = { - forgejo = { - enable = true; - lfs.enable = true; - useWizard = false; - database.type = "postgres"; - - settings = { - DEFAULT = { - APP_NAME = "Tamin Amarth"; - APP_SLOGAN = "Where code is forged"; - }; - - server = { - DOMAIN = domain; - ROOT_URL = "https://${domain}/"; - HTTP_PORT = cfg.port; - LANDING_PAGE = "explore"; - }; - - cors = { - ENABLED = true; - ALLOW_DOMAIN = "https://*.amarth.cloud"; - }; - - security = { - INSTALL_LOCK = true; - PASSWORD_HASH_ALGO = "argon2"; - DISABLE_WEBHOOKS = true; - }; - - ui = { - EXPLORE_PAGING_NUM = 50; - ISSUE_PAGING_NUM = 50; - MEMBERS_PAGING_NUM = 50; - }; - - "ui.meta" = { - AUTHOR = "Where code is forged!"; - DESCRIPTION = "Self-hosted solution for git, because FOSS is the anvil of the future"; - }; - - admin = { - USER_DISABLED_FEATURES = "manage_gpg_keys"; - EXTERNAL_USER_DISABLE_FEATURES = "manage_gpg_keys"; - }; - - service = { - # Auth - ENABLE_BASIC_AUTHENTICATION = false; - DISABLE_REGISTRATION = false; - ALLOW_ONLY_EXTERNAL_REGISTRATION = true; - SHOW_REGISTRATION_BUTTON = false; - - # Privacy - DEFAULT_KEEP_EMAIL_PRIVATE = true; - DEFAULT_USER_VISIBILITY = "private"; - DEFAULT_ORG_VISIBILITY = "private"; - - # Common sense - VALID_SITE_URL_SCHEMES = "https"; - }; - - openid = { - ENABLE_OPENID_SIGNIN = true; - ENABLE_OPENID_SIGNUP = true; - WHITELISTED_URIS = "https://auth.kruining.eu"; - }; - - oauth2_client = { - ENABLE_AUTO_REGISTRATION = true; - UPDATE_AVATAR = true; - ACCOUNT_LINKING = "auto"; - }; - - actions = { - ENABLED = true; - # DEFAULT_ACTIONS_URL = "https://data.forgejo.org"; - }; - - other = { - SHOW_FOOTER_VERSION = false; - SHOW_FOOTER_TEMPLATE_LOAD_TIME = false; - }; - - metrics = { - ENABLED = true; - }; - - api = { - ENABLE_SWAGGER = false; - }; - - mirror = { - ENABLED = true; - }; - - session = { - PROVIDER = "db"; - COOKIE_SECURE = true; - }; - - mailer = { - ENABLED = true; - PROTOCOL = "smtp+starttls"; - SMTP_ADDR = "black-mail.nl"; - SMTP_PORT = 587; - FROM = "chris@kruining.eu"; - USER = "chris@kruining.eu"; - PASSWD_URI = "file:${config.sops.secrets."forgejo/email".path}"; - }; - }; - }; - - openssh.settings.AllowUsers = ["forgejo"]; - - gitea-actions-runner = { - package = pkgs.forgejo-runner; - instances.default = { - enable = true; - name = "default"; - url = "https://git.amarth.cloud"; - # Obtaining the path to the runner token file may differ - # tokenFile should be in format TOKEN=, since it's EnvironmentFile for systemd - tokenFile = config.sops.secrets."forgejo/action_runner_token".path; - # token = "ZBetud1F0IQ9VjVFpZ9bu0FXgx9zcsy1x25yvjhw"; - labels = [ - "default:docker://nixos/nix:latest" - "ubuntu:docker://ubuntu:24-bookworm" - "nix:docker://git.amarth.cloud/amarth/runners/default:latest" - ]; - settings = { - log.level = "info"; - }; - }; - }; - }; - - users = { - users."gitea-runner" = { - isSystemUser = true; - group = "gitea-runner"; - }; - groups."gitea-runner" = {}; - }; - - sops.secrets = { - "forgejo/action_runner_token" = { - owner = "gitea-runner"; - group = "gitea-runner"; - restartUnits = ["gitea-runner-default.service"]; - }; - - "forgejo/email" = { - owner = "forgejo"; - group = "forgejo"; - key = "email/chris_kruining_eu"; - restartUnits = ["forgejo.service"]; - }; - }; - }; -} diff --git a/modules/nixos/services/games/minecraft/default.nix b/modules/nixos/services/games/minecraft.nix similarity index 77% rename from modules/nixos/services/games/minecraft/default.nix rename to modules/nixos/services/games/minecraft.nix index 84567b3..7f408ae 100644 --- a/modules/nixos/services/games/minecraft/default.nix +++ b/modules/nixos/services/games/minecraft.nix @@ -1,16 +1,11 @@ -{ - inputs, - config, - lib, - pkgs, - namespace, - ... -}: let +{ inputs, config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption mkOption; inherit (lib.types) str; cfg = config.${namespace}.services.games.minecraft; -in { +in +{ imports = [ inputs.nix-minecraft.nixosModules.minecraft-servers ]; @@ -30,7 +25,7 @@ in { }; config = mkIf cfg.enable { - users.users.${cfg.user} = { + user.users.${cfg.user} = { isSystemUser = true; group = cfg.group; }; @@ -82,7 +77,7 @@ in { inherit whitelist; inherit jvmOpts; - package = pkgs.fabricServers.fabric-1_21_4.override {loaderVersion = "0.16.10";}; + package = pkgs.fabricServers.fabric-1_21_4.override { loaderVersion = "0.16.10"; }; serverProperties = { gamemode = "survival"; @@ -108,14 +103,8 @@ in { inherit (pkgs) linkFarmFromDrvs fetchurl; in { mods = linkFarmFromDrvs "mods" (attrValues { - FabricApi = fetchurl { - url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/ZNwYCTsk/fabric-api-0.118.0%2B1.21.4.jar"; - sha512 = "1e0d31b6663dc2c7be648f3a5a9cf7b698b9a0fd0f7ae16d1d3f32d943d7c5205ff63a4f81b0c4e94a8997482cce026b7ca486e99d9ce35ac069aeb29b02a30d"; - }; - Terralith = fetchurl { - url = "https://cdn.modrinth.com/data/8oi3bsk5/versions/MuJMtPGQ/Terralith_1.21.x_v2.5.8.jar"; - sha512 = "f862ed5435ce4c11a97d2ea5c40eee9f817c908f3223b5fd3e3fff0562a55111d7429dc73a2f1ca0b1af7b1ff6fa0470ed6efebb5de13336c40bb70fb357dd60"; - }; + FabricApi = fetchurl { url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/ZNwYCTsk/fabric-api-0.118.0%2B1.21.4.jar"; sha512 = "1e0d31b6663dc2c7be648f3a5a9cf7b698b9a0fd0f7ae16d1d3f32d943d7c5205ff63a4f81b0c4e94a8997482cce026b7ca486e99d9ce35ac069aeb29b02a30d"; }; + Terralith = fetchurl { url = "https://cdn.modrinth.com/data/8oi3bsk5/versions/MuJMtPGQ/Terralith_1.21.x_v2.5.8.jar"; sha512 = "f862ed5435ce4c11a97d2ea5c40eee9f817c908f3223b5fd3e3fff0562a55111d7429dc73a2f1ca0b1af7b1ff6fa0470ed6efebb5de13336c40bb70fb357dd60"; }; # DistantHorizons = fetchurl { url = "https://cdn.modrinth.com/data/uCdwusMi/versions/jptcCdp2/DistantHorizons-2.2.1-a-1.20.4-forge-fabric.jar"; sha512 = "47368d91099d0b5f364339a69f4e425f8fb1e3a7c3250a8b649da76135e68a22f1a76b191c87e15a5cdc0a1d36bc57f2fa825490d96711d09d96807be97d575d"; }; }); }; @@ -136,7 +125,7 @@ in { inherit whitelist; inherit jvmOpts; - package = pkgs.fabricServers.fabric-1_19_2.override {loaderVersion = "0.16.9";}; + package = pkgs.fabricServers.fabric-1_19_2.override { loaderVersion = "0.16.9"; }; serverProperties = { gamemode = "survival"; @@ -158,31 +147,24 @@ in { inherit (lib) concatMapAttrs; readDirRec = src: dir: fn: - concatMapAttrs ( - name: type: - if type == "directory" - then (readDirRec src "${dir}/${name}" fn) - else {"${dir}/${name}" = fn "${dir}/${name}";} + concatMapAttrs (name: type: if type == "directory" + then (readDirRec src "${dir}/${name}" fn) + else { "${dir}/${name}" = (fn "${dir}/${name}"); } ) (readDir "${src}/${dir}"); copyDir = dir: readDirRec src dir (x: "${src}/${x}"); - in - { - "ops.json" = { - value = ops; - }; - } - // (copyDir "config"); + in { + "ops.json" = { + value = ops; + }; + } + // (copyDir "config"); symlinks = let inherit (builtins) attrNames readDir map; inherit (pkgs) linkFarm; - linkFarmFromDir = name: dir: - linkFarm name (map (x: { - name = x; - path = "${src}/${dir}/${x}"; - }) (attrNames (readDir "${src}/${dir}"))); + linkFarmFromDir = name: dir: linkFarm name (map (x: { name = x; path = "${src}/${dir}/${x}"; }) (attrNames (readDir "${src}/${dir}"))); in { Deftu = linkFarmFromDir "tekxit-deftu" "Deftu"; TKXAddons = linkFarmFromDir "tekxit-TKXAddons" "TKXAddons"; diff --git a/modules/nixos/services/games/openrct.nix b/modules/nixos/services/games/openrct.nix deleted file mode 100644 index a36f0fb..0000000 --- a/modules/nixos/services/games/openrct.nix +++ /dev/null @@ -1,27 +0,0 @@ -{ config, lib, pkgs, namespace, ... }: -let - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.services.games.openrct; -in -{ - options.${namespace}.services.games.openrct = { - enable = mkEnableOption "OpenRCT2"; - }; - - config = mkIf cfg.enable { - environment.systemPackages = with pkgs; [ - openrct2 - ]; - - systemd.services.openrct = { - enable = true; - after = [ "network.target"]; - description = "OpenRCT2 Server"; - serviceConfig = { - Type = ""; - ExecStart = lib.getExe pkgs.openrct2; - }; - }; - }; -} diff --git a/modules/nixos/services/games/palworld.nix b/modules/nixos/services/games/palworld.nix new file mode 100644 index 0000000..dea16b3 --- /dev/null +++ b/modules/nixos/services/games/palworld.nix @@ -0,0 +1,25 @@ +{ config, lib, namespace, ... }: +let + inherit (lib) mkIf mkEnableOption; + + cfg = config.${namespace}.services.games.palworld; +in +{ + options.${namespace}.services.games.palworld = { + enable = mkEnableOption "Palworld"; + }; + + config = mkIf cfg.enable { +# kaas = (pkgs.mkSteamServer rec { +# name = "Palworld"; +# src = pkgs.fetchSteam { +# inherit name; +# appId = "2394010"; +# hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; +# }; +# +# sartCmd = "PalServer.sh"; +# hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; +# }); + }; +} diff --git a/modules/nixos/services/games/palworld/default.nix b/modules/nixos/services/games/palworld/default.nix deleted file mode 100644 index 152891d..0000000 --- a/modules/nixos/services/games/palworld/default.nix +++ /dev/null @@ -1,30 +0,0 @@ -{ - config, - lib, - namespace, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.services.games.palworld; -in { - options.${namespace}.services.games.palworld = { - enable = mkEnableOption "Palworld"; - }; - - config = mkIf cfg.enable { - # kaas = (pkgs.mkSteamServer rec { - # name = "Palworld"; - # src = pkgs.fetchSteam { - # inherit name; - # appId = "2394010"; - # hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; - # }; - # - # sartCmd = "PalServer.sh"; - # hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; - # }); - - sops.secrets."palworld/password" = {}; - }; -} diff --git a/modules/nixos/services/media/default.nix b/modules/nixos/services/media/default.nix index 900eee4..7d76794 100644 --- a/modules/nixos/services/media/default.nix +++ b/modules/nixos/services/media/default.nix @@ -1,15 +1,11 @@ -{ - pkgs, - lib, - namespace, - config, - ... -}: let +{ pkgs, lib, namespace, config, ... }: +let inherit (lib) mkIf mkEnableOption mkOption; inherit (lib.types) str; cfg = config.${namespace}.services.media; -in { +in +{ options.${namespace}.services.media = { enable = mkEnableOption "Enable media services"; @@ -35,6 +31,13 @@ in { #========================================================================= environment.systemPackages = with pkgs; [ podman-tui + jellyfin + jellyfin-web + jellyfin-ffmpeg + jellyseerr + mediainfo + id3v2 + yt-dlp ]; #========================================================================= @@ -49,27 +52,105 @@ in { }; systemd.tmpfiles.rules = [ - "d '${cfg.path}/qbittorrent' 0770 ${cfg.user} ${cfg.group} - -" - "d '${cfg.path}/sabnzbd' 0770 ${cfg.user} ${cfg.group} - -" - "d '${cfg.path}/downloads/incomplete' 0770 ${cfg.user} ${cfg.group} - -" - "d '${cfg.path}/downloads/done' 0770 ${cfg.user} ${cfg.group} - -" + "d '${cfg.path}/series' 0700 ${cfg.user} ${cfg.group} - -" + "d '${cfg.path}/movies' 0700 ${cfg.user} ${cfg.group} - -" + "d '${cfg.path}/music' 0700 ${cfg.user} ${cfg.group} - -" + "d '${cfg.path}/qbittorrent' 0700 ${cfg.user} ${cfg.group} - -" + "d '${cfg.path}/sabnzbd' 0700 ${cfg.user} ${cfg.group} - -" + "d '${cfg.path}/reiverr/config' 0700 ${cfg.user} ${cfg.group} - -" + "d '${cfg.path}/downloads/incomplete' 0700 ${cfg.user} ${cfg.group} - -" + "d '${cfg.path}/downloads/done' 0700 ${cfg.user} ${cfg.group} - -" ]; #========================================================================= # Services #========================================================================= - services = { - bazarr = { + services = let + serviceConf = { enable = true; openFirewall = true; user = cfg.user; group = cfg.group; - listenPort = 2050; + }; + in { + jellyfin = serviceConf; + radarr = serviceConf; + sonarr = serviceConf; + bazarr = serviceConf; + lidarr = serviceConf; + + jellyseerr = { + enable = true; + openFirewall = true; }; - postgresql = { + prowlarr = { enable = true; + openFirewall = true; + }; + + qbittorrent = { + enable = true; + openFirewall = true; + webuiPort = 5000; + + serverConfig = { + LegalNotice.Accepted = true; + }; + + user = cfg.user; + group = cfg.group; + }; + + sabnzbd = { + enable = true; + openFirewall = true; + configFile = "${cfg.path}/sabnzbd/config.ini"; + + user = cfg.user; + group = cfg.group; + }; + + caddy = { + enable = true; + virtualHosts = { + "media.kruining.eu".extraConfig = '' + import auth + + reverse_proxy http://127.0.0.1:9494 + ''; + "jellyfin.kruining.eu".extraConfig = '' + reverse_proxy http://127.0.0.1:8096 + ''; + }; }; }; + + systemd.services.jellyfin.serviceConfig.killSignal = lib.mkForce "SIGKILL"; + + ${namespace}.services.virtualisation.podman.enable = true; + + virtualisation = { + oci-containers = { + backend = "podman"; + + containers = { + flaresolverr = { + image = "flaresolverr/flaresolverr"; + autoStart = true; + ports = [ "127.0.0.1:8191:8191" ]; + }; + + reiverr = { + image = "ghcr.io/aleksilassila/reiverr:v2.2.0"; + autoStart = true; + ports = [ "127.0.0.1:9494:9494" ]; + volumes = [ "${cfg.path}/reiverr/config:/config" ]; + }; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ 80 443 6969 ]; }; } diff --git a/modules/nixos/services/media/glance/default.nix b/modules/nixos/services/media/glance/default.nix deleted file mode 100644 index bdd4c87..0000000 --- a/modules/nixos/services/media/glance/default.nix +++ /dev/null @@ -1,179 +0,0 @@ -{ - config, - lib, - namespace, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.services.media.glance; -in { - options.${namespace}.services.media.glance = { - enable = mkEnableOption "Enable Glance"; - }; - - config = mkIf cfg.enable { - # ${namespace}.services.networking.caddy.hosts = { - # "https://${config.networking.hostName}.arda:443" = '' - # reverse_proxy http://[::1]:2000 - # ''; - # }; - - services.glance = { - enable = true; - openFirewall = true; - - environmentFile = config.sops.templates."glance/secrets.env".path; - - settings = { - server = { - host = "0.0.0.0"; - port = 2000; - }; - - theme = { - # Teal city predefined theme (https://github.com/glanceapp/glance/blob/main/docs/themes.md#teal-city) - background-color = "225 14 15"; - primary-color = "157 47 65"; - contrast-multiplier = 1.1; - }; - - pages = [ - { - name = "Home"; - columns = [ - { - size = "small"; - widgets = [ - { - type = "calendar"; - first-day-of-the-week = "monday"; - } - ]; - } - - { - size = "full"; - widgets = [ - { - type = "monitor"; - cache = "1m"; - title = "Services"; - sites = [ - { - title = "Zitadel"; - url = "https://auth.kruining.eu"; - icon = "sh:zitadel"; - } - { - title = "Forgejo"; - url = "https://git.amarth.cloud/chris"; - icon = "sh:forgejo"; - } - { - title = "Vaultwarden"; - url = "https://vault.kruining.eu"; - icon = "sh:vaultwarden"; - } - ]; - } - { - type = "monitor"; - cache = "1m"; - title = "Observability"; - sites = [ - { - title = "Grafana"; - url = "http://${config.networking.hostName}:${builtins.toString config.services.grafana.settings.server.http_port}"; - icon = "sh:grafana"; - } - { - title = "Prometheus"; - url = "http://${config.networking.hostName}:${builtins.toString config.services.prometheus.port}"; - icon = "sh:prometheus"; - } - ]; - } - { - type = "monitor"; - cache = "1m"; - title = "Media"; - sites = [ - { - title = "Jellyfin"; - url = "http://${config.networking.hostName}:8096"; - icon = "sh:jellyfin"; - } - { - title = "Radarr"; - url = "http://${config.networking.hostName}:${builtins.toString config.services.radarr.settings.server.port}"; - icon = "sh:radarr"; - } - { - title = "Sonarr"; - url = "http://${config.networking.hostName}:${builtins.toString config.services.sonarr.settings.server.port}"; - icon = "sh:sonarr"; - } - { - title = "Lidarr"; - url = "http://${config.networking.hostName}:${builtins.toString config.services.lidarr.settings.server.port}"; - icon = "sh:lidarr"; - } - { - title = "Prowlarr"; - url = "http://${config.networking.hostName}:${builtins.toString config.services.prowlarr.settings.server.port}"; - icon = "sh:prowlarr"; - } - { - title = "qBittorrent"; - url = "http://${config.networking.hostName}:${builtins.toString config.services.qbittorrent.webuiPort}"; - icon = "sh:qbittorrent"; - } - { - title = "SABnzbd"; - url = "http://${config.networking.hostName}:${builtins.toString config.services.sabnzbd.settings.misc.port}"; - icon = "sh:sabnzbd"; - } - ]; - } - ]; - } - - { - size = "small"; - widgets = [ - { - type = "weather"; - location = "Amsterdam, The Netherlands"; - units = "metric"; - hour-format = "24h"; - } - - { - type = "server-stats"; - servers = [ - { - type = "local"; - name = "Ulmo"; - } - ]; - } - ]; - } - ]; - } - ]; - }; - }; - - sops.templates."glance/secrets.env" = { - # owner = config.services.glance.user; - # group = config.services.glance.group; - content = '' - RADARR_KEY="${config.sops.placeholder."radarr/apikey"}" - SONARR_KEY="${config.sops.placeholder."sonarr/apikey"}" - LIDARR_KEY="${config.sops.placeholder."lidarr/apikey"}" - ''; - }; - }; -} diff --git a/modules/nixos/services/media/jellyfin/default.nix b/modules/nixos/services/media/jellyfin/default.nix deleted file mode 100644 index de19896..0000000 --- a/modules/nixos/services/media/jellyfin/default.nix +++ /dev/null @@ -1,49 +0,0 @@ -{ - pkgs, - config, - lib, - namespace, - inputs, - system, - ... -}: let - inherit (builtins) toString; - inherit (lib) mkIf mkEnableOption mkOption types; - - cfg = config.${namespace}.services.media.jellyfin; -in { - options.${namespace}.services.media.jellyfin = { - enable = mkEnableOption "Enable jellyfin server"; - }; - - config = mkIf cfg.enable { - ${namespace}.services.networking.caddy = { - hosts = { - "jellyfin.kruining.eu" = '' - reverse_proxy http://[::1]:8096 - ''; - }; - }; - - environment.systemPackages = with pkgs; [ - jellyfin - jellyfin-web - jellyfin-ffmpeg - mediainfo - id3v2 - yt-dlp - ]; - - services = { - # port is harcoded in nixpkgs module - jellyfin = { - enable = true; - openFirewall = true; - user = "media"; - group = "media"; - }; - }; - - systemd.services.jellyfin.serviceConfig.killSignal = lib.mkForce "SIGKILL"; - }; -} diff --git a/modules/nixos/services/media/mydia/default.nix b/modules/nixos/services/media/mydia/default.nix deleted file mode 100644 index 9044c2e..0000000 --- a/modules/nixos/services/media/mydia/default.nix +++ /dev/null @@ -1,95 +0,0 @@ -{ - config, - lib, - namespace, - inputs, - system, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.services.media.mydia; -in { - imports = [ - inputs.mydia.nixosModules.default - ]; - - options.${namespace}.services.media.mydia = { - enable = mkEnableOption "Enable Mydia"; - }; - - config = mkIf cfg.enable { - services.mydia = { - enable = true; - - port = 2100; - listenAddress = "0.0.0.0"; - openFirewall = true; - - mediaLibraries = [ - "/var/mydia/movies" - "/var/mydia/series" - ]; - - database = { - # type = "sqlite"; - # uri = "file:///var/lib/mydia/mydia.db"; - type = "postgres"; - uri = "postgres://mydia@localhost:5432/mydia?sslmode=disable"; - passwordFile = config.sops.templates."mydia/database_password".path; - }; - - secretKeyBaseFile = config.sops.secrets."mydia/secret_key_base".path; - guardianSecretKeyFile = config.sops.secrets."mydia/guardian_secret".path; - - oidc = { - enable = true; - issuer = "https://auth.kruining.eu"; - clientIdFile = config.sops.secrets."mydia/oidc_id".path; - clientSecretFile = config.sops.secrets."mydia/oidc_secret".path; - scopes = ["openid" "profile" "email"]; - }; - - downloadClients = { - qbittorrent = { - type = "qbittorrent"; - host = "localhost"; - port = 2080; - username = "admin"; - passwordFile = config.sops.secrets."mydia/qbittorrent_password".path; - useSsl = false; - }; - }; - }; - - sops.secrets = let - base = - ["secret_key_base" "guardian_secret" "oidc_id" "oidc_secret"] - |> lib.map (name: - lib.nameValuePair "mydia/${name}" { - owner = config.services.mydia.user; - group = config.services.mydia.group; - restartUnits = ["mydia.service"]; - }) - |> lib.listToAttrs; - in - base - // { - "mydia/qbittorrent_password" = { - owner = config.services.mydia.user; - group = config.services.mydia.group; - restartUnits = ["mydia.service"]; - key = "qbittorrent/password"; - }; - }; - - sops.templates."mydia/database_password" = { - owner = config.services.mydia.user; - group = config.services.mydia.group; - restartUnits = ["mydia.service"]; - content = '' - DATABASE_PASSWORD="" - ''; - }; - }; -} diff --git a/modules/nixos/services/media/nextcloud/default.nix b/modules/nixos/services/media/nextcloud.nix similarity index 89% rename from modules/nixos/services/media/nextcloud/default.nix rename to modules/nixos/services/media/nextcloud.nix index 06904c6..658a5b4 100644 --- a/modules/nixos/services/media/nextcloud/default.nix +++ b/modules/nixos/services/media/nextcloud.nix @@ -1,16 +1,12 @@ -{ - config, - lib, - pkgs, - namespace, - ... -}: let +{ config, lib, pkgs, namespace, ... }: +let inherit (lib) mkIf mkEnableOption mkOption; inherit (lib.types) str; cfg = config.${namespace}.services.media.nextcloud; -in { - options.${namespace}.services.media.nextcloud = { +in +{ + options.modules.services.nextcloud = { enable = mkEnableOption "Nextcloud"; user = mkOption { @@ -25,14 +21,6 @@ in { }; config = mkIf cfg.enable { - ${namespace}.services.networking.caddy = { - hosts."cloud.kruining.eu" = '' - php_fastcgi unix//run/phpfpm/nextcloud.sock { - env front_controller_active true - } - ''; - }; - users = { users.${cfg.user} = { isSystemUser = true; @@ -52,7 +40,7 @@ in { services.nextcloud = { enable = true; - # webserver = "caddy"; + webserver = "caddy"; package = pkgs.nextcloud31; hostName = "localhost"; @@ -87,5 +75,14 @@ in { # startServices = true; # }; + + services.caddy = { + enable = true; + virtualHosts."cloud.kruining.eu".extraConfig = '' + php_fastcgi unix//run/phpfpm/nextcloud.sock { + env front_controller_active true + } + ''; + }; }; } diff --git a/modules/nixos/services/media/nfs/default.nix b/modules/nixos/services/media/nfs.nix similarity index 79% rename from modules/nixos/services/media/nfs/default.nix rename to modules/nixos/services/media/nfs.nix index 54b58e7..7674e69 100644 --- a/modules/nixos/services/media/nfs/default.nix +++ b/modules/nixos/services/media/nfs.nix @@ -2,10 +2,10 @@ let inherit (lib) mkIf mkEnableOption; - cfg = config.${namespace}.services.media.nfs; + cfg = config.${namespace}.media.nfs; in { - options.${namespace}.services.media.nfs = { + options.${namespace}.media.nfs = { enable = mkEnableOption "Enable NFS"; }; diff --git a/modules/nixos/services/media/servarr/default.nix b/modules/nixos/services/media/servarr/default.nix deleted file mode 100644 index ed9b94a..0000000 --- a/modules/nixos/services/media/servarr/default.nix +++ /dev/null @@ -1,533 +0,0 @@ -{ - pkgs, - config, - lib, - namespace, - inputs, - system, - ... -}: let - inherit (builtins) toString; - inherit (lib) mkIf mkEnableOption mkOption types; - - cfg = config.${namespace}.services.media.servarr; - servarr = import ./lib.nix {inherit lib;}; - anyEnabled = cfg |> lib.attrNames |> lib.length |> (l: l > 0); -in { - options.${namespace}.services.media = { - servarr = mkOption { - type = types.attrsOf (types.submodule ({name, ...}: { - options = { - enable = mkEnableOption "Enable ${name}"; - debug = mkEnableOption "Use tofu plan instead of tofu apply for ${name} "; - - port = mkOption { - type = types.port; - }; - - rootFolders = mkOption { - type = types.listOf types.str; - default = []; - }; - }; - })); - default = {}; - }; - }; - - config = mkIf anyEnabled { - services = - cfg - |> lib.mapAttrsToList (service: { - enable, - port, - ... - }: (mkIf enable { - "${service}" = - { - enable = true; - openFirewall = true; - - environmentFiles = [ - config.sops.templates."${service}/config.env".path - ]; - - settings = { - auth.authenticationMethod = "External"; - - server = { - # bindaddress = "0.0.0.0"; - bindaddress = "[::]"; - port = port; - }; - - postgres = { - host = "localhost"; - port = "5432"; - user = service; - maindb = service; - logdb = service; - }; - }; - } - // (lib.optionalAttrs (lib.elem service ["radarr" "sonarr" "lidarr" "whisparr"]) { - user = service; - group = "media"; - }); - })) - |> lib.concat [ - { - qbittorrent = { - enable = true; - openFirewall = true; - webuiPort = 2080; - serverConfig = lib.mkForce {}; - - user = "qbittorrent"; - group = "media"; - }; - - sabnzbd = { - enable = true; - openFirewall = true; - - allowConfigWrite = false; - configFile = lib.mkForce null; - - secretFiles = [ - config.sops.templates."sabnzbd/config.ini".path - ]; - - settings = { - misc = { - host = "0.0.0.0"; - port = 2090; - host_whitelist = "${config.networking.hostName}"; - - permissions = "770"; - download_dir = "/var/media/downloads/incomplete"; - complete_dir = "/var/media/downloads/done"; - }; - - servers = { - "news.sunnyusenet.com" = { - name = "news.sunnyusenet.com"; - displayname = "news.sunnyusenet.com"; - host = "news.sunnyusenet.com"; - port = 563; - timeout = 60; - }; - }; - }; - - user = "sabnzbd"; - group = "media"; - }; - - flaresolverr = { - enable = true; - openFirewall = true; - port = 2070; - }; - - postgresql = let - databases = [] ++ (cfg |> lib.attrNames); - in { - ensureDatabases = databases; - ensureUsers = - databases - |> lib.map (service: { - name = service; - ensureDBOwnership = true; - }); - }; - } - ] - |> lib.mkMerge; - - systemd.services = - cfg - |> lib.mapAttrsToList (service: { - enable, - debug, - port, - rootFolders, - ... - }: (mkIf enable { - "${service}ApplyTerraform" = let - config' = config; - lib' = lib; - - terraformConfiguration = inputs.terranix.lib.terranixConfiguration { - inherit system; - - modules = [ - ({ - config, - lib, - ... - }: { - config = { - variable = - cfg - |> lib'.mapAttrsToList (s: _: { - "${s}_api_key" = { - type = "string"; - description = "${s} API key"; - }; - }) - |> lib'.concat [ - { - qbittorrent_api_key = { - type = "string"; - description = "qbittorrent api key"; - }; - - sabnzbd_api_key = { - type = "string"; - description = "sabnzbd api key"; - }; - } - ] - |> lib'.mkMerge; - - terraform.required_providers.${service} = { - source = "devopsarr/${service}"; - version = - { - radarr = "2.3.5"; - sonarr = "3.4.0"; - prowlarr = "3.1.0"; - lidarr = "1.13.0"; - readarr = "2.1.0"; - whisparr = "1.2.0"; - }.${ - service - }; - }; - - provider.${service} = { - url = "http://127.0.0.1:${toString port}"; - api_key = lib.tfRef "var.${service}_api_key"; - }; - - resource = - { - "${service}_notification_webhook" = mkIf (lib.elem service ["radarr" "sonarr" "whisparr" "lidarr" "readarr"]) { - "arrtrix" = - { - method = 1; # HTTP METHOD 1=POST, 2=PUT - name = "Arrtrix"; - url = "http://localhost:${toString config'.services.arrtrix.settings.appservice.port}/_arrtrix/webhook"; - - on_grab = true; - on_download = true; - on_rename = true; - on_upgrade = true; - } - // (lib.optionalAttrs (lib.elem service ["radarr" "whisparr"]) { - on_movie_delete = true; - }); - }; - - "${service}_root_folder" = mkIf (lib.elem service ["radarr" "sonarr" "whisparr"]) ( - rootFolders - |> lib.imap (i: f: lib.nameValuePair "local${toString i}" {path = f;}) - |> lib.listToAttrs - ); - - "${service}_download_client_qbittorrent" = mkIf (lib.elem service ["radarr" "sonarr" "lidarr" "whisparr"]) { - "main" = { - name = "qBittorrent"; - enable = true; - priority = 1; - host = "localhost"; - username = "admin"; - password = lib.tfRef "var.qbittorrent_api_key"; - url_base = "/"; - port = 2080; - }; - }; - - "${service}_download_client_sabnzbd" = mkIf (lib.elem service ["radarr" "sonarr" "lidarr" "whisparr"]) { - "main" = - { - name = "SABnzbd"; - enable = true; - priority = 1; - host = "localhost"; - api_key = lib.tfRef "var.sabnzbd_api_key"; - url_base = "/"; - port = 2090; - } - // ({ - radarr = {movie_category = "movies";}; - sonarr = {tv_category = "tv";}; - lidarr = {music_category = "audio";}; - whisparr = {movie_category = "movies";}; - readarr = {book_category = "Default";}; - }.${ - service - }); - }; - } - // (lib.optionalAttrs (service == "prowlarr") ( - cfg - |> lib'.filterAttrs (s: _: lib'.elem s ["radarr" "sonarr" "lidarr" "whisparr"]) - |> lib'.mapAttrsToList (s: {port, ...}: { - "prowlarr_application_${s}"."main" = let - p = cfg.prowlarr.port or config'.services.prowlarr.settings.server.port or 9696; - in { - name = s; - sync_level = "addOnly"; - base_url = "http://localhost:${toString port}"; - prowlarr_url = "http://localhost:${toString p}"; - api_key = lib.tfRef "var.${s}_api_key"; - # sync_categories = [3000 3010 3030]; - }; - }) - |> lib'.concat [ - { - "prowlarr_indexer" = { - "nyaa" = { - enable = true; - - app_profile_id = 1; - priority = 1; - - name = "Nyaa"; - implementation = "Cardigann"; - config_contract = "CardigannSettings"; - protocol = "torrent"; - - fields = [ - { - name = "definitionFile"; - text_value = "nyaasi"; - } - { - name = "baseSettings.limitsUnit"; - number_value = 0; - } - { - name = "torrentBaseSettings.preferMagnetUrl"; - bool_value = false; - } - { - name = "prefer_magnet_links"; - bool_value = true; - } - { - name = "sonarr_compatibility"; - bool_value = false; - } - { - name = "strip_s01"; - bool_value = false; - } - { - name = "radarr_compatibility"; - bool_value = false; - } - { - name = "filter-id"; - number_value = 0; - } - { - name = "cat-id"; - number_value = 0; - } - { - name = "sort"; - number_value = 0; - } - { - name = "type"; - number_value = 1; - } - ]; - }; - }; - } - ] - |> lib'.mkMerge - )); - }; - }) - ]; - }; - in { - description = "${service} terraform apply"; - - wantedBy = ["multi-user.target"]; - wants = ["${service}.service"]; - - preStart = '' - install -d -m 0770 -o ${service} -g media /var/lib/${service}ApplyTerraform - ${ - rootFolders - |> lib.map (folder: "install -d -m 0770 -o media -g media ${folder}") - |> lib.join "\n" - } - ''; - - script = '' - # Sleep for a bit to give the service a chance to start up - sleep 5s - - if [ "$(systemctl is-active "${service}")" != "active" ]; then - echo "${service} is not running" - exit 1 - fi - - # Print the path to the source for easier debugging - echo "config location: ${terraformConfiguration}" - - # Copy infra code into workspace - cp -f ${terraformConfiguration} config.tf.json - - # Initialize OpenTofu - ${lib.getExe pkgs.opentofu} init - - # Run the infrastructure code - ${lib.getExe pkgs.opentofu} \ - ${ - if debug - then "plan" - else "apply -auto-approve" - } \ - -var-file='${config.sops.templates."servarr/config.tfvars".path}' - ''; - - serviceConfig = { - Type = "oneshot"; - User = service; - Group = "media"; - - WorkingDirectory = "/var/lib/${service}ApplyTerraform"; - - EnvironmentFile = [ - config.sops.templates."${service}/config.env".path - ]; - }; - }; - })) - |> lib.mkMerge; - - system.activationScripts.qbittorrent-config = { - deps = lib.optional (!config.sops.useSystemdActivation) "setupSecrets"; - # TODO: If sops-nix is switched to systemd activation, add a systemd unit - # for this install step that runs after sops-install-secrets.service, - # because this activation-script dependency only orders against setupSecrets. - text = '' - install -Dm0600 -o ${config.services.qbittorrent.user} -g ${config.services.qbittorrent.group} \ - ${config.sops.templates."qbittorrent/qBittorrent.conf".path} \ - ${config.services.qbittorrent.profileDir}/qBittorrent/config/qBittorrent.conf - ''; - }; - - users = - cfg - |> lib.mapAttrsToList (service: {enable, ...}: (mkIf enable { - users.${service} = { - isSystemUser = true; - group = lib.mkDefault service; - extraGroups = ["media"]; - }; - groups.${service} = {}; - })) - |> lib.concat [ - { - groups.media = {}; - } - ] - |> lib.mkMerge; - - sops = - cfg - |> lib.mapAttrsToList (service: {enable, ...}: (mkIf enable { - secrets."${service}/apikey" = { - owner = service; - group = "media"; - restartUnits = ["${service}.service"]; - }; - - templates = { - "${service}/config.env" = { - owner = service; - group = "media"; - restartUnits = ["${service}.service"]; - content = '' - ${lib.toUpper service}__AUTH__APIKEY="${config.sops.placeholder."${service}/apikey"}" - ''; - }; - }; - })) - |> lib.concat [ - { - secrets = { - "qbittorrent/password" = {}; - "qbittorrent/password_hash" = { - owner = "qbittorrent"; - group = "media"; - }; - "sabnzbd/apikey" = {}; - "sabnzbd/nzbkey" = {}; - "sabnzbd/sunnyweb/username" = {}; - "sabnzbd/sunnyweb/password" = {}; - }; - - templates = { - "servarr/config.tfvars" = { - owner = "media"; - group = "media"; - mode = "0440"; - restartUnits = cfg |> lib.attrNames |> lib.map (s: "${s}.service"); - content = '' - ${ - cfg - |> lib.attrNames - |> lib.map (s: "${s}_api_key = \"${config.sops.placeholder."${s}/apikey"}\"") - |> lib.join "\n" - } - qbittorrent_api_key = "${config.sops.placeholder."qbittorrent/password"}" - sabnzbd_api_key = "${config.sops.placeholder."sabnzbd/apikey"}" - ''; - }; - "qbittorrent/qBittorrent.conf" = { - owner = "qbittorrent"; - group = "media"; - mode = "0600"; - restartUnits = ["qbittorrent.service"]; - content = '' - [LegalNotice] - Accepted=true - - [Preferences] - WebUI\AlternativeUIEnabled=true - WebUI\RootFolder=${pkgs.vuetorrent}/share/vuetorrent - WebUI\Username=admin - WebUI\Password_PBKDF2=${config.sops.placeholder."qbittorrent/password_hash"} - ''; - }; - "sabnzbd/config.ini" = { - owner = "sabnzbd"; - group = "media"; - mode = "0660"; - content = '' - [misc] - api_key = ${config.sops.placeholder."sabnzbd/apikey"} - nzb_key = ${config.sops.placeholder."sabnzbd/nzbkey"} - - [servers] - [[news.sunnyusenet.com]] - username = ${config.sops.placeholder."sabnzbd/sunnyweb/username"} - password = ${config.sops.placeholder."sabnzbd/sunnyweb/password"} - ''; - }; - }; - } - ] - |> lib.mkMerge; - }; -} diff --git a/modules/nixos/services/media/servarr/lib.nix b/modules/nixos/services/media/servarr/lib.nix deleted file mode 100644 index 8ee412b..0000000 --- a/modules/nixos/services/media/servarr/lib.nix +++ /dev/null @@ -1,2 +0,0 @@ -{lib, ...}: { -} diff --git a/modules/nixos/services/networking/caddy/default.nix b/modules/nixos/services/networking/caddy/default.nix deleted file mode 100644 index 21ab908..0000000 --- a/modules/nixos/services/networking/caddy/default.nix +++ /dev/null @@ -1,42 +0,0 @@ -{ - config, - pkgs, - lib, - namespace, - ... -}: let - inherit (builtins) length; - inherit (lib) mkIf mkEnableOption mkOption types attrNames mapAttrs; - - cfg = config.${namespace}.services.networking.caddy; - hasHosts = (cfg.hosts |> attrNames |> length) > 0; -in { - options.${namespace}.services.networking.caddy = { - enable = mkEnableOption "enable caddy" // {default = true;}; - - hosts = mkOption { - type = types.attrsOf types.str; - }; - - extraConfig = mkOption { - type = types.str; - }; - }; - - config = mkIf hasHosts { - networking.firewall.allowedTCPPorts = [80 443]; - - services.caddy = { - enable = cfg.enable; - - package = pkgs.caddy.withPlugins { - plugins = ["github.com/corazawaf/coraza-caddy/v2@v2.1.0"]; - hash = "sha256-pSXjLaZoRtKV3eFl2ySRSjl3yxi514G1Cb7pfrpxxtE="; - }; - - virtualHosts = - cfg.hosts - |> mapAttrs (host: extraConfig: {inherit extraConfig;}); - }; - }; -} diff --git a/modules/nixos/services/networking/wireguard/default.nix b/modules/nixos/services/networking/wireguard/default.nix deleted file mode 100644 index 0cf5320..0000000 --- a/modules/nixos/services/networking/wireguard/default.nix +++ /dev/null @@ -1,47 +0,0 @@ -{ - config, - pkgs, - lib, - namespace, - ... -}: let - inherit (builtins) length; - inherit (lib) mkIf mkEnableOption mkOption types attrNames attrsToList listToAttrs; - - cfg = config.${namespace}.services.networking.wireguard; - hasPeers = (cfg.peer |> attrNames |> length) > 0; -in { - options.${namespace}.services.networking.wireguard = { - # enable = mkEnableOption "enable wireguard" // {default = true;}; - - peer = mkOption { - type = types.attrsOf (types.submodule { - options = { - port = mkOption { - type = types.port; - description = ''''; - }; - - address = mkOption { - type = types.listOf types.str; - default = []; - description = ''''; - }; - }; - }); - }; - }; - - config = mkIf hasPeers { - # networking.firewall.allowedUDPPorts = cfg.peer |> lib.attrValues |> lib.map (p: p.port); - # networking.wq-quick = { - # # enable = cfg.enable; - - # interfaces = - # cfg.peer - # |> attrsToList - # |> imap0 (i: { name, value }: (namevaluepair "wg${i}" (value // {}))) - # |> listToAttrs; - # }; - }; -} diff --git a/modules/nixos/services/observability/alloy/default.nix b/modules/nixos/services/observability/alloy/default.nix deleted file mode 100644 index 3b64f2e..0000000 --- a/modules/nixos/services/observability/alloy/default.nix +++ /dev/null @@ -1,83 +0,0 @@ -{ - config, - lib, - namespace, - ... -}: let - inherit (builtins) toString; - inherit (lib) mkEnableOption mkIf; - - cfg = config.${namespace}.services.observability.alloy; - - httpPort = 9070; - otlpGrpcPort = 9071; - otlpHttpPort = 9072; - tempoOtlpGrpcPort = 9062; -in { - options.${namespace}.services.observability.alloy = { - enable = mkEnableOption "enable Grafana Alloy"; - }; - - config = mkIf cfg.enable { - services.alloy = { - enable = true; - configPath = "/etc/alloy"; - extraFlags = [ - "--disable-reporting" - "--server.http.listen-addr=[::]:${toString httpPort}" - "--storage.path=/var/lib/alloy" - ]; - }; - - environment.etc."alloy/config.alloy".text = '' - otelcol.receiver.otlp "default" { - grpc { - endpoint = "[::1]:${toString otlpGrpcPort}" - } - - http { - endpoint = "[::1]:${toString otlpHttpPort}" - } - - output { - metrics = [otelcol.processor.batch.metrics.input] - traces = [otelcol.processor.batch.traces.input] - } - } - - otelcol.processor.batch "metrics" { - output { - metrics = [otelcol.exporter.prometheus.default.input] - } - } - - otelcol.processor.batch "traces" { - output { - traces = [otelcol.exporter.otlp.tempo.input] - } - } - - otelcol.exporter.prometheus "default" { - forward_to = [prometheus.remote_write.local.receiver] - } - - prometheus.remote_write "local" { - endpoint { - url = "http://[::1]:${toString config.services.prometheus.port}/api/v1/write" - } - } - - otelcol.exporter.otlp "tempo" { - client { - endpoint = "[::1]:${toString tempoOtlpGrpcPort}" - - tls { - insecure = true - } - } - } - ''; - - networking.firewall.allowedTCPPorts = [httpPort]; - }; -} diff --git a/modules/nixos/services/observability/grafana/dashboards/default.json b/modules/nixos/services/observability/grafana/dashboards/default.json deleted file mode 100644 index f8ea8dc..0000000 --- a/modules/nixos/services/observability/grafana/dashboards/default.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Default Dash", - "description": "The default dashboard", - "timezone": "browser", - "editable": false, - "panels": [] -} diff --git a/modules/nixos/services/observability/grafana/default.nix b/modules/nixos/services/observability/grafana/default.nix deleted file mode 100644 index 879ecdc..0000000 --- a/modules/nixos/services/observability/grafana/default.nix +++ /dev/null @@ -1,174 +0,0 @@ -{ - pkgs, - config, - lib, - namespace, - ... -}: let - inherit (lib.modules) mkIf; - inherit (lib.options) mkEnableOption; - - cfg = config.${namespace}.services.observability.grafana; - - db_user = "grafana"; - db_name = "grafana"; -in { - options.${namespace}.services.observability.grafana = { - enable = mkEnableOption "enable Grafana"; - }; - - config = mkIf cfg.enable { - services = { - grafana = { - enable = true; - openFirewall = true; - - settings = { - server = { - http_port = 9010; - http_addr = "::"; - domain = "ulmo"; - }; - - security = { - secret_key = "$__file{${config.sops.secrets."grafana/secret_key".path}}"; - }; - - auth = { - disable_login_form = false; - }; - - "auth.basic".enable = false; - "auth.generic_oauth" = { - enable = true; - name = "Zitadel"; - client_id = "$__file{${config.sops.secrets."grafana/oidc_id".path}}"; - client_secret = "$__file{${config.sops.secrets."grafana/oidc_secret".path}}"; - scopes = "openid email profile offline_access urn:zitadel:iam:org:project:roles"; - email_attribute_path = "email"; - login_attribute_path = "username"; - name_attribute_path = "full_name"; - role_attribute_path = "contains(urn:zitadel:iam:org:project:roles[*], 'owner') && 'GrafanaAdmin' || contains(urn:zitadel:iam:org:project:roles[*], 'contributer') && 'Editor' || 'Viewer'"; - auth_url = "https://auth.kruining.eu/oauth/v2/authorize"; - token_url = "https://auth.kruining.eu/oauth/v2/token"; - api_url = "https://auth.kruining.eu/oidc/v1/userinfo"; - allow_sign_up = true; - auto_login = true; - use_pkce = true; - usr_refresh_token = true; - allow_assign_grafana_admin = true; - }; - - database = { - type = "postgres"; - host = "/var/run/postgresql:5432"; - name = db_name; - user = db_user; - ssl_mode = "disable"; - }; - - users = { - allow_sign_up = false; - allow_org_create = false; - viewers_can_edit = false; - - default_theme = "system"; - }; - - analytics = { - reporting_enabled = false; - check_for_updates = false; - check_for_plugin_updates = false; - feedback_links_enabled = false; - }; - }; - - provision = { - enable = true; - - dashboards.settings = { - apiVersion = 1; - providers = [ - { - name = "Default Dashboard"; - disableDeletion = true; - allowUiUpdates = false; - options = { - path = "/etc/grafana/dashboards"; - foldersFromFilesStructure = true; - }; - } - ]; - }; - - datasources.settings.datasources = [ - { - name = "Prometheus"; - uid = "prometheus"; - type = "prometheus"; - url = "http://[::1]:9020"; - isDefault = true; - editable = false; - } - - { - name = "Loki"; - uid = "loki"; - type = "loki"; - url = "http://[::1]:9030"; - editable = false; - } - - { - name = "Tempo"; - uid = "tempo"; - type = "tempo"; - url = "http://localhost:9060"; - editable = false; - jsonData = { - nodeGraph.enabled = true; - serviceMap.datasourceUid = "prometheus"; - tracesToLogsV2 = { - datasourceUid = "loki"; - filterByTraceID = true; - spanStartTimeShift = "-1h"; - spanEndTimeShift = "1h"; - }; - }; - } - ]; - }; - }; - - postgresql = { - enable = true; - ensureDatabases = [db_name]; - ensureUsers = [ - { - name = db_user; - ensureDBOwnership = true; - } - ]; - }; - }; - - environment.etc."/grafana/dashboards/default.json".source = ./dashboards/default.json; - - sops = { - secrets = { - "grafana/secret_key" = { - owner = "grafana"; - group = "grafana"; - }; - "grafana/oidc_id" = { - owner = "grafana"; - group = "grafana"; - }; - "grafana/oidc_secret" = { - owner = "grafana"; - group = "grafana"; - }; - }; - }; - }; -} diff --git a/modules/nixos/services/observability/loki/default.nix b/modules/nixos/services/observability/loki/default.nix deleted file mode 100644 index bab5b3f..0000000 --- a/modules/nixos/services/observability/loki/default.nix +++ /dev/null @@ -1,49 +0,0 @@ -{ pkgs, config, lib, namespace, ... }: -let - inherit (lib.modules) mkIf; - inherit (lib.options) mkEnableOption; - - cfg = config.${namespace}.services.observability.loki; -in -{ - options.${namespace}.services.observability.loki = { - enable = mkEnableOption "enable Grafana Loki"; - }; - - config = mkIf cfg.enable { - services.loki = { - enable = true; - configuration = { - auth_enabled = false; - - server = { - http_listen_port = 9030; - }; - - common = { - ring = { - instance_addr = "127.0.0.1"; - kvstore.store = "inmemory"; - }; - replication_factor = 1; - path_prefix = "/tmp/loki"; - }; - - schema_config.configs = [ - { - from = "2025-01-01"; - store = "tsdb"; - object_store = "filesystem"; - schema = "v13"; - index = { - prefix = "index_"; - period = "24h"; - }; - } - ]; - }; - }; - - networking.firewall.allowedTCPPorts = [ 9030 ]; - }; -} diff --git a/modules/nixos/services/observability/prometheus/default.nix b/modules/nixos/services/observability/prometheus/default.nix deleted file mode 100644 index c092286..0000000 --- a/modules/nixos/services/observability/prometheus/default.nix +++ /dev/null @@ -1,67 +0,0 @@ -{ pkgs, config, lib, namespace, ... }: -let - inherit (builtins) toString; - inherit (lib) mkEnableOption mkIf optionals; - - cfg = config.${namespace}.services.observability.prometheus; -in -{ - options.${namespace}.services.observability.prometheus = { - enable = mkEnableOption "enable Prometheus"; - }; - - config = mkIf cfg.enable { - services.prometheus = { - enable = true; - port = 9020; - extraFlags = optionals config.${namespace}.services.observability.alloy.enable [ - "--web.enable-remote-write-receiver" - ]; - - globalConfig.scrape_interval = "15s"; - - scrapeConfigs = [ - { - job_name = "prometheus"; - static_configs = [ - { targets = [ "localhost:9020" ]; } - ]; - } - - { - job_name = "node"; - static_configs = [ - { targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ]; } - ]; - } - ] - ++ optionals config.${namespace}.services.observability.alloy.enable [ - { - job_name = "alloy"; - static_configs = [ - { targets = [ "localhost:9070" ]; } - ]; - } - ] - ++ optionals config.${namespace}.services.observability.tempo.enable [ - { - job_name = "tempo"; - static_configs = [ - { targets = [ "localhost:9060" ]; } - ]; - } - ]; - - exporters = { - node = { - enable = true; - port = 9021; - enabledCollectors = [ "systemd" ]; - openFirewall = true; - }; - }; - }; - - networking.firewall.allowedTCPPorts = [ 9020 ]; - }; -} diff --git a/modules/nixos/services/observability/promtail/default.nix b/modules/nixos/services/observability/promtail/default.nix deleted file mode 100644 index b852f1f..0000000 --- a/modules/nixos/services/observability/promtail/default.nix +++ /dev/null @@ -1,65 +0,0 @@ -{ - pkgs, - config, - lib, - namespace, - ... -}: let - inherit (lib.modules) mkIf; - inherit (lib.options) mkEnableOption; - - cfg = config.${namespace}.services.observability.promtail; -in { - options.${namespace}.services.observability.promtail = { - enable = mkEnableOption "enable Grafana Promtail"; - }; - - config = mkIf cfg.enable { - services.promtail = { - enable = true; - - # Ensures proper permissions - extraFlags = [ - "-config.expand-env=true" - ]; - - configuration = { - server = { - http_listen_port = 9040; - grpc_listen_port = 0; - }; - - positions = { - filename = "filename"; - }; - - clients = [ - { - url = "http://[::1]:9030/loki/api/v1/push"; - } - ]; - - scrape_configs = [ - { - job_name = "journal"; - journal = { - max_age = "12h"; - labels = { - job = "systemd-journal"; - host = "ulmo"; - }; - }; - relabel_configs = [ - { - source_labels = ["__journal__systemd_unit"]; - target_label = "unit"; - } - ]; - } - ]; - }; - }; - - networking.firewall.allowedTCPPorts = [9040]; - }; -} diff --git a/modules/nixos/services/observability/tempo/default.nix b/modules/nixos/services/observability/tempo/default.nix deleted file mode 100644 index 46339bc..0000000 --- a/modules/nixos/services/observability/tempo/default.nix +++ /dev/null @@ -1,51 +0,0 @@ -{ - config, - lib, - namespace, - ... -}: let - inherit (lib) mkEnableOption mkIf; - - cfg = config.${namespace}.services.observability.tempo; - - httpPort = 9060; - grpcPort = 9061; - otlpGrpcPort = 9062; - otlpHttpPort = 9063; -in { - options.${namespace}.services.observability.tempo = { - enable = mkEnableOption "enable Grafana Tempo"; - }; - - config = mkIf cfg.enable { - services.tempo = { - enable = true; - settings = { - auth_enabled = false; - search_enabled = true; - - server = { - http_listen_address = "[::]"; - http_listen_port = httpPort; - grpc_listen_address = "[::1]"; - grpc_listen_port = grpcPort; - }; - - distributor.receivers.otlp.protocols = { - grpc.endpoint = "[::1]:${builtins.toString otlpGrpcPort}"; - http.endpoint = "[::1]:${builtins.toString otlpHttpPort}"; - }; - - storage.trace = { - backend = "local"; - wal.path = "/var/lib/tempo/wal"; - local.path = "/var/lib/tempo/traces"; - }; - - compactor.compaction.block_retention = "168h"; - }; - }; - - networking.firewall.allowedTCPPorts = [httpPort]; - }; -} diff --git a/modules/nixos/services/observability/uptime-kuma/default.nix b/modules/nixos/services/observability/uptime-kuma/default.nix deleted file mode 100644 index af0cfa8..0000000 --- a/modules/nixos/services/observability/uptime-kuma/default.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ pkgs, config, lib, namespace, ... }: -let - inherit (builtins) toString; - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.services.observability.uptime-kuma; -in -{ - options.${namespace}.services.observability.uptime-kuma = { - enable = mkEnableOption "enable uptime kuma"; - }; - - config = mkIf cfg.enable { - services.uptime-kuma = { - enable = true; - - settings = { - PORT = toString 9050; - HOST = "0.0.0.0"; - }; - }; - - networking.firewall.allowedTCPPorts = [ 9050 ]; - }; -} diff --git a/modules/nixos/services/persistance/postgesql/default.nix b/modules/nixos/services/persistance/postgesql/default.nix deleted file mode 100644 index 403c07c..0000000 --- a/modules/nixos/services/persistance/postgesql/default.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ - config, - lib, - pkgs, - namespace, - ... -}: let - inherit (lib) mkIf mkEnableOption; - - cfg = config.${namespace}.services.persistance.postgresql; -in { - options.${namespace}.services.persistance.postgresql = { - enable = mkEnableOption "Postgresql"; - }; - - # Access db with `psql -U postgres` - config = mkIf cfg.enable { - services = { - postgresql = { - enable = true; - authentication = '' - # Generated file, do not edit! - # TYPE DATABASE USER ADDRESS METHOD - local all all trust - host all all 127.0.0.1/32 trust - host all all ::1/128 trust - ''; - }; - }; - }; -} diff --git a/modules/nixos/services/security/vaultwarden/default.nix b/modules/nixos/services/security/vaultwarden/default.nix index 1660736..6870606 100644 --- a/modules/nixos/services/security/vaultwarden/default.nix +++ b/modules/nixos/services/security/vaultwarden/default.nix @@ -1,216 +1,28 @@ -{ - pkgs, - config, - lib, - namespace, - ... -}: let - inherit (builtins) toString; - inherit (lib) mkIf mkEnableOption mkOption types getAttrs toUpper concatMapAttrsStringSep; +{ pkgs, config, lib, namespace, ... }: +let + inherit (lib.modules) mkIf; + inherit (lib.options) mkEnableOption; cfg = config.${namespace}.services.security.vaultwarden; - - databaseProviderSqlite = types.submodule ({...}: { - options = { - type = mkOption { - type = types.enum ["sqlite"]; - }; - - file = mkOption { - type = types.path; - description = '' - Path to sqlite database file. - ''; - }; - }; - }); - - databaseProviderPostgresql = types.submodule ({...}: let - urlOptions = lib.${namespace}.options.mkUrlOptions { - host = { - description = '' - Hostname of the postgresql server - ''; - }; - - port = { - default = 5432; - example = "5432"; - description = '' - Port of the postgresql server - ''; - }; - - protocol = mkOption { - default = "postgres"; - example = "postgres"; - }; - }; - in { - options = - { - type = mkOption { - type = types.enum ["postgresql"]; - }; - - sslMode = mkOption { - type = types.enum ["verify-ca" "verify-full" "require" "prefer" "allow" "disabled"]; - default = "verify-full"; - example = "verify-ca"; - description = '' - How to verify the server's ssl - - | mode | eavesdropping protection | MITM protection | Statement | - |-------------|--------------------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------| - | disable | No | No | I don't care about security, and I don't want to pay the overhead of encryption. | - | allow | Maybe | No | I don't care about security, but I will pay the overhead of encryption if the server insists on it. | - | prefer | Maybe | No | I don't care about encryption, but I wish to pay the overhead of encryption if the server supports it. | - | require | Yes | No | I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want. | - | verify-ca | Yes | Depends on CA policy | I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server that I trust. | - | verify-full | Yes | Yes | I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server I trust, and that it's the one I specify. | - - [Source](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS) - ''; - }; - } - // (urlOptions |> getAttrs ["protocol" "host" "port"]); - }); -in { +in +{ options.${namespace}.services.security.vaultwarden = { enable = mkEnableOption "enable vaultwarden"; - - database = mkOption { - type = types.oneOf [ - (types.addCheck databaseProviderSqlite (x: x ? type && x.type == "sqlite")) - (types.addCheck databaseProviderPostgresql (x: x ? type && x.type == "postgresql")) - null - ]; - default = null; - description = ''''; - }; }; config = mkIf cfg.enable { - ${namespace}.services.networking.caddy.hosts = { - "vault.kruining.eu" = '' - encode zstd gzip - - handle_path /admin { - respond 401 { - close - } - } - - reverse_proxy http://localhost:${toString config.services.vaultwarden.config.ROCKET_PORT} { - header_up X-Real-IP {remote_host} - } - ''; - }; - - systemd.tmpfiles.rules = [ - "d '/var/lib/vaultwarden' 0700 vaultwarden vaultwarden - -" + environment.systemPackages = with pkgs; [ + vaultwarden + vaultwarden-postgresql ]; - # systemd.services.vaultwarden.wants = [ "zitadelApplyTerraform.service" ]; + services.vaultwarden = { + enable = true; + dbBackend = "postgresql"; - services = { - vaultwarden = { - enable = true; - dbBackend = "postgresql"; - - package = pkgs.vaultwarden-postgresql; - - config = { - SIGNUPS_ALLOWED = false; - DOMAIN = "https://vault.kruining.eu"; - - DATABASE_URL = "postgres://localhost:5432/vaultwarden?sslmode=disable"; - - WEB_VAULT_ENABLED = true; - - SSO_ENABLED = true; - SSO_ONLY = true; - SSO_PKCE = true; - SSO_AUTH_ONLY_NOT_SESSION = false; - SSO_ROLES_ENABLED = true; - SSO_ORGANIZATIONS_ENABLED = true; - SSO_ORGANIZATIONS_REVOCATION = true; - SSO_AUTHORITY = "https://auth.kruining.eu"; - SSO_SCOPES = "email profile offline_access"; - - ROCKET_ADDRESS = "::1"; - ROCKET_PORT = 8222; - ROCKET_LOG = "critical"; - - SMTP_HOST = "black-mail.nl"; - SMTP_PORT = 587; - SMTP_SECURITY = "starttls"; - SMTP_USERNAME = "chris@kruining.eu"; - SMTP_FROM = "chris@kruining.eu"; - SMTP_FROM_NAME = "Chris' Vaultwarden"; - }; - - environmentFile = [ - "/var/lib/zitadel/clients/nix_ulmo_vaultwarden" - config.sops.templates."vaultwarden/config.env".path - ]; - }; - - postgresql = { - enable = true; - ensureDatabases = ["vaultwarden"]; - ensureUsers = [ - { - name = "vaultwarden"; - ensureDBOwnership = true; - } - ]; - }; - }; - - sops = { - secrets = { - "vaultwarden/email" = { - owner = config.users.users.vaultwarden.name; - group = config.users.users.vaultwarden.name; - key = "email/chris_kruining_eu"; - restartUnits = ["vaultwarden.service"]; - }; - }; - - templates = { - "vaultwarden/config.env" = { - content = '' - SMTP_PASSWORD='${config.sops.placeholder."vaultwarden/email"}'; - ''; - owner = config.users.users.vaultwarden.name; - group = config.users.groups.vaultwarden.name; - }; - temp-db-output.content = let - config = - cfg.database - |> ( - {type, ...} @ db: - if type == "sqlite" - then {inherit (db) type file;} - else if type == "postgresql" - then { - inherit (db) type; - url = lib.${namespace}.strings.toUrl { - inherit (db) protocol host port; - path = "vaultwarden"; - query = { - sslmode = db.sslMode; - }; - }; - } - else {} - ) - |> concatMapAttrsStringSep "\n" (n: v: "${toUpper n}=${v}"); - in '' - # GENERATED VALUES - ${config} - ''; + config = { + SIGNUPS_ALLOWED = false; + DOMAIN = "https://passwords.kruining.eu"; }; }; }; diff --git a/modules/nixos/services/virtualisation/podman/default.nix b/modules/nixos/services/virtualisation/podman/default.nix index 0faf8ce..9b9dc89 100644 --- a/modules/nixos/services/virtualisation/podman/default.nix +++ b/modules/nixos/services/virtualisation/podman/default.nix @@ -12,7 +12,6 @@ in config = mkIf cfg.enable { virtualisation = { containers.enable = true; - oci-containers.backend = "podman"; podman = { enable = true; diff --git a/modules/nixos/shells/default.nix b/modules/nixos/shells/default.nix index ea8f50d..6b5c058 100644 --- a/modules/nixos/shells/default.nix +++ b/modules/nixos/shells/default.nix @@ -1,2 +1,2 @@ -{...}: { -} +{ ... }: +{} \ No newline at end of file diff --git a/modules/nixos/system/security/sops/default.nix b/modules/nixos/system/security/sops/default.nix index bee7b3c..ebceca3 100644 --- a/modules/nixos/system/security/sops/default.nix +++ b/modules/nixos/system/security/sops/default.nix @@ -1,4 +1,4 @@ -{ pkgs, config, namespace, inputs, system, ... }: +{ pkgs, config, namespace, inputs, ... }: let cfg = config.${namespace}.system.security.sops; in @@ -13,14 +13,10 @@ in environment.systemPackages = with pkgs; [ sops ]; sops = { - defaultSopsFormat = "yaml"; - defaultSopsFile = inputs.self + "/systems/${system}/${config.networking.hostName}/secrets.yml"; + age.keyFile = "/home/.sops-key.age"; - age = { - # keyFile = "~/.config/sops/age/keys.txt"; - # sshKeyPaths = [ "~/.ssh/id_ed25519" ]; - # generateKey = true; - }; + defaultSopsFile = ../../../../systems/x86_64-linux/${config.networking.hostName}/secrets.yaml; + defaultSopsFormat = "yaml"; }; }; } \ No newline at end of file diff --git a/modules/nixos/system/security/sudo/default.nix b/modules/nixos/system/security/sudo/default.nix index b79efbc..6dedf50 100644 --- a/modules/nixos/system/security/sudo/default.nix +++ b/modules/nixos/system/security/sudo/default.nix @@ -14,8 +14,9 @@ in sudo-rs = { enable = true; - execWheelOnly = true; - extraConfig = ''Defaults env_keep += "EDITOR PATH DISPLAY"''; + extraConfig = '' + Defaults env_keep += "EDITOR PATH DISPLAY" + ''; }; }; }; diff --git a/modules/nixos/temp/services/arrtrix/default.nix b/modules/nixos/temp/services/arrtrix/default.nix deleted file mode 100644 index 6bb1d9f..0000000 --- a/modules/nixos/temp/services/arrtrix/default.nix +++ /dev/null @@ -1,214 +0,0 @@ -{ - config, - lib, - pkgs, - namespace, - ... -}: let - inherit (lib) mkEnableOption mkPackageOption mkIf mkOption optionalAttrs recursiveUpdate types baseNameOf; - - cfg = config.services.arrtrix; - dataDir = "/var/lib/arrtrix"; - registrationFile = "${dataDir}/arrtrix-registration.yaml"; - settingsFile = "${dataDir}/config.yaml"; - settingsFileUnsubstituted = settingsFormat.generate "arrtrix-config-unsubstituted.json" cfg.settings; - settingsFormat = pkgs.formats.json {}; - - defaultConfig = { - bridge = { - command_prefix = "!arr"; - relay.enabled = true; - permissions."*" = "relay"; - }; - database = { - type = "sqlite3-fk-wal"; - uri = "file:${dataDir}/arrtrix.db?_txlock=immediate"; - }; - homeserver = { - address = "http://localhost:8448"; - domain = config.services.matrix-synapse.settings.server_name or "example.com"; - }; - appservice = { - hostname = "[::]"; - port = 29329; - id = "arrtrix"; - bot = { - username = "arrtrixbot"; - displayname = "arrtrix Bot"; - }; - as_token = ""; - hs_token = ""; - username_template = "arrtrix_{{.}}"; - }; - logging = { - min_level = "info"; - writers = lib.singleton { - type = "stdout"; - format = "pretty-colored"; - time_format = " "; - }; - }; - observability = { - otlp_grpc_endpoint = ""; - service_name = "arrtrix"; - resource_attributes = {}; - }; - network.content = { - movies = { - url = ""; - api_key = ""; - root_folder_path = ""; - quality_profile_id = 0; - minimum_availability = "released"; - search_on_add = true; - }; - series = { - url = ""; - api_key = ""; - root_folder_path = ""; - quality_profile_id = 0; - language_profile_id = 0; - season_folder = true; - series_type = "standard"; - search_on_add = true; - }; - }; - }; -in { - options.services.arrtrix = { - enable = mkEnableOption "Arr-focused Matrix appservice foundation"; - - package = mkPackageOption pkgs.${namespace} "arrtrix" {}; - - registerToSynapse = mkOption { - type = types.bool; - default = config.services.matrix-synapse.enable; - defaultText = lib.literalExpression '' - config.services.matrix-synapse.enable - ''; - description = '' - Whether to add the bridge's app service registration file to - `services.matrix-synapse.settings.app_service_config_files`. - ''; - }; - - settings = mkOption { - apply = lib.recursiveUpdate defaultConfig; - type = settingsFormat.type; - default = defaultConfig; - description = '' - {file}`config.yaml` configuration as a Nix attribute set. - Configuration options should match those described in the example configuration. - Get an example configuration by executing `arrtrix -c example.yaml --generate-example-config` - Secret tokens should be specified using {option}`environmentFile` - instead of this world-readable attribute set. - ''; - example = {}; - }; - - environmentFile = mkOption { - type = types.nullOr types.path; - default = null; - description = '' - File containing environment variables to be passed to the arrtrix service. - If an environment variable `ARRTRIX_BRIDGE_LOGIN_SHARED_SECRET` is set, - then its value will be used in the configuration file for the option - `double_puppet.secrets` without leaking it to the store, using the configured - `homeserver.domain` as key. - ''; - }; - - serviceDependencies = lib.mkOption { - type = with lib.types; listOf str; - default = - (lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit) - ++ (lib.optional config.services.matrix-conduit.enable "conduit.service"); - defaultText = lib.literalExpression '' - (optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit) - ++ (optional config.services.matrix-conduit.enable "conduit.service") - ''; - description = '' - List of systemd units to require and wait for when starting the application service. - ''; - }; - }; - - config = mkIf cfg.enable { - users = { - users."arrtrix" = { - isSystemUser = true; - group = "arrtrix"; - }; - groups."arrtrix" = {}; - }; - - services.matrix-synapse = lib.mkIf cfg.registerToSynapse { - settings.app_service_config_files = [registrationFile]; - }; - systemd.services.matrix-synapse = lib.mkIf cfg.registerToSynapse { - serviceConfig.SupplementaryGroups = ["arrtrix"]; - }; - - systemd.services.arrtrix = { - description = "arrtrix, A *arr stack to matrix bridge for *arr-notifications"; - - wantedBy = ["multi-user.target"]; - after = ["network-online.target"]; - wants = ["network-online.target"]; - restartTriggers = [settingsFileUnsubstituted]; - - preStart = '' - # substitute the settings file by environment variables - # in this case read from EnvironmentFile - test -f '${settingsFile}' && rm -f '${settingsFile}' - - old_umask=$(umask) - umask 0177 - ${lib.getExe pkgs.envsubst} -o '${settingsFile}' -i '${settingsFileUnsubstituted}' - umask $old_umask - - if [ ! -f '${registrationFile}' ]; then - ${lib.getExe cfg.package} --generate-registration --config='${settingsFile}' --registration='${registrationFile}' - fi - chmod 640 ${registrationFile} - ''; - - serviceConfig = { - Type = "simple"; - User = "arrtrix"; - Group = "arrtrix"; - - StateDirectory = baseNameOf dataDir; - WorkingDirectory = dataDir; - EnvironmentFile = cfg.environmentFile; - - ExecStart = '' - ${lib.getExe cfg.package} --config='${settingsFile}' --registration='${registrationFile}' - ''; - - Restart = "on-failure"; - RestartSec = "30s"; - - NoNewPrivileges = true; - PrivateTmp = true; - ProtectHome = true; - ProtectSystem = "strict"; - ProtectClock = true; - ProtectControlGroups = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - LockPersonality = true; - MemoryDenyWriteExecute = true; - - SystemCallArchitectures = "native"; - SystemCallErrorNumber = "EPERM"; - SystemCallFilter = ["@system-service"]; - UMask = "0027"; - }; - }; - }; -} diff --git a/packages/arrtrix/cmd/arrtrix/main.go b/packages/arrtrix/cmd/arrtrix/main.go deleted file mode 100644 index 3fa476f..0000000 --- a/packages/arrtrix/cmd/arrtrix/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "sneeuwvlok/packages/arrtrix/pkg/connector" - "sneeuwvlok/packages/arrtrix/pkg/runtime" -) - -var ( - Tag = "unknown" - Commit = "unknown" - BuildTime = "unknown" -) - -var m = runtime.Main{ - Name: "arrtrix", - URL: "https://github.com/chris-kruining/sneeuwvlok", - Description: "An Arr-focused Matrix appservice bridge.", - Version: "0.1.0", - Connector: &connector.ArrtrixConnector{}, -} - -func main() { - m.InitVersion(Tag, Commit, BuildTime) - m.Run() -} diff --git a/packages/arrtrix/default.nix b/packages/arrtrix/default.nix deleted file mode 100644 index 0113edb..0000000 --- a/packages/arrtrix/default.nix +++ /dev/null @@ -1,33 +0,0 @@ -{ - buildGoModule, - lib, - olm, - versionCheckHook, -}: -buildGoModule rec { - pname = "arrtrix"; - version = "0.1.0"; - tag = "v0.1.0"; - - src = lib.cleanSource ./.; - - vendorHash = "sha256-UYRit+v41djnCx+GFdEl/8WQsp2DzF4ywT9iv3m1pSc="; - subPackages = ["cmd/arrtrix"]; - - buildInputs = [olm]; - - ldflags = [ - "-X main.Tag=${tag}" - ]; - - doInstallCheck = true; - nativeInstallCheckInputs = [versionCheckHook]; - - meta = { - description = "*arr-stack Matrix bridge"; - homepage = "https://github.com/chris-kruining/sneeuwvlok"; - license = lib.licenses.mit; - maintainers = []; - mainProgram = "arrtrix"; - }; -} diff --git a/packages/arrtrix/go.mod b/packages/arrtrix/go.mod deleted file mode 100644 index 81a6c93..0000000 --- a/packages/arrtrix/go.mod +++ /dev/null @@ -1,60 +0,0 @@ -module sneeuwvlok/packages/arrtrix - -go 1.25.0 - -require ( - github.com/rs/zerolog v1.34.0 - go.mau.fi/util v0.9.7 - go.mau.fi/zeroconfig v0.2.0 - go.opentelemetry.io/otel v1.43.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 - go.opentelemetry.io/otel/log v0.19.0 - go.opentelemetry.io/otel/metric v1.43.0 - go.opentelemetry.io/otel/sdk v1.43.0 - go.opentelemetry.io/otel/sdk/log v0.19.0 - go.opentelemetry.io/otel/sdk/metric v1.43.0 - go.opentelemetry.io/otel/trace v1.43.0 - gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mauflag v1.0.0 - maunium.net/go/mautrix v0.26.4 -) - -require ( - filippo.io/edwards25519 v1.2.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coder/websocket v1.8.14 // indirect - github.com/coreos/go-systemd/v22 v22.6.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect - github.com/lib/pq v1.11.2 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.34 // indirect - github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect - github.com/rs/xid v1.6.0 // indirect - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.2.0 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect - github.com/yuin/goldmark v1.7.16 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/proto/otlp v1.10.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect - golang.org/x/net v0.52.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/grpc v1.80.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect -) diff --git a/packages/arrtrix/go.sum b/packages/arrtrix/go.sum deleted file mode 100644 index 8d8f5ab..0000000 --- a/packages/arrtrix/go.sum +++ /dev/null @@ -1,142 +0,0 @@ -filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= -filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= -github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= -github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= -github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= -github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= -github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE= -github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= -github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= -github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg= -go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE= -go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= -go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= -go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= -go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= -go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= -go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= -maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.26.4 h1:enHSnkf0L2V9+VnfJfNhKSReSW6pBKS/x3Su+v+Vovs= -maunium.net/go/mautrix v0.26.4/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M= diff --git a/packages/arrtrix/pkg/arr/catalog.go b/packages/arrtrix/pkg/arr/catalog.go deleted file mode 100644 index eb2f833..0000000 --- a/packages/arrtrix/pkg/arr/catalog.go +++ /dev/null @@ -1,76 +0,0 @@ -package arr - -import ( - "fmt" - "slices" - "strings" -) - -type ContentType string - -const ( - ContentTypeMovies ContentType = "movies" - ContentTypeSeries ContentType = "series" -) - -var supportedContentTypes = []ContentType{ - ContentTypeMovies, - ContentTypeSeries, -} - -var supportedEvents = map[ContentType][]string{ - ContentTypeMovies: {"Test", "Grab", "Download", "Rename", "MovieFileDelete", "MovieDelete"}, - ContentTypeSeries: {"Test", "Grab", "Download", "Rename", "EpisodeFileDelete", "SeriesDelete"}, -} - -func SupportedContentTypes() []ContentType { - return append([]ContentType(nil), supportedContentTypes...) -} - -func SupportedEventTypes(contentType ContentType) []string { - return append([]string(nil), supportedEvents[contentType]...) -} - -func ParseContentType(value string) (ContentType, error) { - contentType := ContentType(strings.ToLower(strings.TrimSpace(value))) - if slices.Contains(supportedContentTypes, contentType) { - return contentType, nil - } - return "", fmt.Errorf("unsupported content type %q (expected one of: %s)", value, Strings()) -} - -func ParseEventType(contentType ContentType, value string) (string, error) { - value = strings.TrimSpace(value) - if strings.EqualFold(value, "all") { - return "all", nil - } - for _, eventType := range supportedEvents[contentType] { - if strings.EqualFold(eventType, value) { - return eventType, nil - } - } - return "", fmt.Errorf("unsupported event type %q for %s", value, contentType) -} - -func SupportsEventType(contentType ContentType, eventType string) bool { - return slices.Contains(supportedEvents[contentType], strings.TrimSpace(eventType)) -} - -func (c ContentType) Label() string { - switch c { - case ContentTypeMovies: - return "movies" - case ContentTypeSeries: - return "series" - default: - return string(c) - } -} - -func Strings() string { - values := make([]string, 0, len(supportedContentTypes)) - for _, contentType := range supportedContentTypes { - values = append(values, string(contentType)) - } - return strings.Join(values, ", ") -} diff --git a/packages/arrtrix/pkg/arr/catalog_test.go b/packages/arrtrix/pkg/arr/catalog_test.go deleted file mode 100644 index e3c2784..0000000 --- a/packages/arrtrix/pkg/arr/catalog_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package arr - -import "testing" - -func TestParseContentType(t *testing.T) { - contentType, err := ParseContentType("Movies") - if err != nil { - t.Fatalf("ParseContentType returned error: %v", err) - } - if contentType != ContentTypeMovies { - t.Fatalf("expected movies content type, got %q", contentType) - } -} - -func TestParseEventType(t *testing.T) { - eventType, err := ParseEventType(ContentTypeSeries, "download") - if err != nil { - t.Fatalf("ParseEventType returned error: %v", err) - } - if eventType != "Download" { - t.Fatalf("expected Download event type, got %q", eventType) - } -} diff --git a/packages/arrtrix/pkg/arrclient/client.go b/packages/arrtrix/pkg/arrclient/client.go deleted file mode 100644 index fc7fb53..0000000 --- a/packages/arrtrix/pkg/arrclient/client.go +++ /dev/null @@ -1,363 +0,0 @@ -package arrclient - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "html" - "io" - "mime" - "net/http" - "net/url" - "path" - "path/filepath" - "strings" - - "sneeuwvlok/packages/arrtrix/pkg/arr" -) - -type Client interface { - ContentType() arr.ContentType - Search(context.Context, string) ([]SearchResult, error) - List(context.Context, string) ([]ManagedItem, error) - Add(context.Context, SearchResult) (*ManagedItem, error) - SetMonitored(context.Context, int64, bool) (*ManagedItem, error) - Delete(context.Context, int64) error - FetchImage(context.Context, ManagedItem) (*MediaAsset, error) -} - -type SearchResult struct { - LookupID int64 - Title string - Year int - Overview string -} - -type ManagedItem struct { - ID int64 - LookupID int64 - Title string - Year int - Monitored bool - Path string - ImageURL string -} - -type MediaAsset struct { - Data []byte - FileName string - MimeType string -} - -type RadarrConfig struct { - URL string `yaml:"url"` - APIKey string `yaml:"api_key"` - RootFolderPath string `yaml:"root_folder_path"` - QualityProfileID int64 `yaml:"quality_profile_id"` - MinimumAvailability string `yaml:"minimum_availability"` - SearchOnAdd *bool `yaml:"search_on_add"` -} - -type SonarrConfig struct { - URL string `yaml:"url"` - APIKey string `yaml:"api_key"` - RootFolderPath string `yaml:"root_folder_path"` - QualityProfileID int64 `yaml:"quality_profile_id"` - LanguageProfileID int64 `yaml:"language_profile_id"` - SeasonFolder *bool `yaml:"season_folder"` - SeriesType string `yaml:"series_type"` - SearchOnAdd *bool `yaml:"search_on_add"` -} - -type httpClient struct { - baseURL *url.URL - apiKey string - httpClient *http.Client -} - -type mediaImage struct { - CoverType string `json:"coverType"` - URL string `json:"url"` - RemoteURL string `json:"remoteUrl"` -} - -func (c *RadarrConfig) ApplyDefaults() { - if c.MinimumAvailability == "" { - c.MinimumAvailability = "released" - } -} - -func (c RadarrConfig) Enabled() bool { - return strings.TrimSpace(c.URL) != "" || strings.TrimSpace(c.APIKey) != "" -} - -func (c RadarrConfig) Validate() error { - if !c.Enabled() { - return nil - } - switch { - case strings.TrimSpace(c.URL) == "": - return fmt.Errorf("network.content.movies.url must be set when movies content is configured") - case strings.TrimSpace(c.APIKey) == "": - return fmt.Errorf("network.content.movies.api_key must be set when movies content is configured") - case strings.TrimSpace(c.RootFolderPath) == "": - return fmt.Errorf("network.content.movies.root_folder_path must be set when movies content is configured") - case c.QualityProfileID <= 0: - return fmt.Errorf("network.content.movies.quality_profile_id must be set when movies content is configured") - case strings.TrimSpace(c.MinimumAvailability) == "": - return fmt.Errorf("network.content.movies.minimum_availability must not be empty") - default: - return nil - } -} - -func (c RadarrConfig) SearchOnAddValue() bool { - return boolValue(c.SearchOnAdd, true) -} - -func (c *SonarrConfig) ApplyDefaults() { - if c.SeriesType == "" { - c.SeriesType = "standard" - } -} - -func (c SonarrConfig) Enabled() bool { - return strings.TrimSpace(c.URL) != "" || strings.TrimSpace(c.APIKey) != "" -} - -func (c SonarrConfig) Validate() error { - if !c.Enabled() { - return nil - } - switch { - case strings.TrimSpace(c.URL) == "": - return fmt.Errorf("network.content.series.url must be set when series content is configured") - case strings.TrimSpace(c.APIKey) == "": - return fmt.Errorf("network.content.series.api_key must be set when series content is configured") - case strings.TrimSpace(c.RootFolderPath) == "": - return fmt.Errorf("network.content.series.root_folder_path must be set when series content is configured") - case c.QualityProfileID <= 0: - return fmt.Errorf("network.content.series.quality_profile_id must be set when series content is configured") - case c.LanguageProfileID <= 0: - return fmt.Errorf("network.content.series.language_profile_id must be set when series content is configured") - case strings.TrimSpace(c.SeriesType) == "": - return fmt.Errorf("network.content.series.series_type must not be empty") - default: - return nil - } -} - -func (c SonarrConfig) SeasonFolderValue() bool { - return boolValue(c.SeasonFolder, true) -} - -func (c SonarrConfig) SearchOnAddValue() bool { - return boolValue(c.SearchOnAdd, true) -} - -func newHTTPClient(rawURL, apiKey string) (*httpClient, error) { - parsedURL, err := url.Parse(strings.TrimRight(strings.TrimSpace(rawURL), "/")) - if err != nil { - return nil, err - } - return &httpClient{ - baseURL: parsedURL, - apiKey: apiKey, - httpClient: http.DefaultClient, - }, nil -} - -func (c *httpClient) do(ctx context.Context, method, requestPath string, query url.Values, body any, dest any) error { - endpoint := *c.baseURL - endpoint.Path = path.Join(endpoint.Path, requestPath) - if len(query) > 0 { - endpoint.RawQuery = query.Encode() - } - - var payload io.Reader - if body != nil { - data, err := json.Marshal(body) - if err != nil { - return err - } - payload = bytes.NewReader(data) - } - - req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), payload) - if err != nil { - return err - } - req.Header.Set("X-Api-Key", c.apiKey) - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) - return fmt.Errorf("%s %s returned %d: %s", method, endpoint.String(), resp.StatusCode, strings.TrimSpace(string(data))) - } - if dest == nil { - return nil - } - return json.NewDecoder(resp.Body).Decode(dest) -} - -func boolValue(value *bool, fallback bool) bool { - if value == nil { - return fallback - } - return *value -} - -func containsFold(haystack, needle string) bool { - return strings.Contains(strings.ToLower(haystack), strings.ToLower(strings.TrimSpace(needle))) -} - -func FormatSearchResult(result SearchResult) string { - if result.Year != 0 { - return fmt.Sprintf("%s (%d)", result.Title, result.Year) - } - return result.Title -} - -func FormatManagedItem(item ManagedItem) string { - if item.Year != 0 { - return fmt.Sprintf("%s (%d)", item.Title, item.Year) - } - return item.Title -} - -func EscapeText(text string) string { - return html.EscapeString(text) -} - -func (c *httpClient) FetchImage(ctx context.Context, item ManagedItem) (*MediaAsset, error) { - imageURL := strings.TrimSpace(item.ImageURL) - if imageURL == "" { - return nil, nil - } - - endpoint, err := url.Parse(imageURL) - if err != nil { - return nil, fmt.Errorf("parse image URL: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL.ResolveReference(endpoint).String(), nil) - if err != nil { - return nil, err - } - if sameHost(req.URL, c.baseURL) { - req.Header.Set("X-Api-Key", c.apiKey) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - data, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) - return nil, fmt.Errorf("GET %s returned %d: %s", req.URL.String(), resp.StatusCode, strings.TrimSpace(string(data))) - } - - data, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) - if err != nil { - return nil, err - } - - mimeType := strings.TrimSpace(resp.Header.Get("Content-Type")) - if idx := strings.Index(mimeType, ";"); idx >= 0 { - mimeType = strings.TrimSpace(mimeType[:idx]) - } - if mimeType == "" { - mimeType = http.DetectContentType(data) - } - - return &MediaAsset{ - Data: data, - FileName: imageFileName(item, endpoint, mimeType), - MimeType: mimeType, - }, nil -} - -func (c *httpClient) imageURL(images []mediaImage) string { - for _, coverType := range []string{"poster", "cover", "fanart"} { - for _, image := range images { - if !strings.EqualFold(image.CoverType, coverType) { - continue - } - if resolved := c.resolveMediaURL(image); resolved != "" { - return resolved - } - } - } - return "" -} - -func (c *httpClient) resolveMediaURL(image mediaImage) string { - switch { - case strings.TrimSpace(image.URL) != "": - ref, err := url.Parse(strings.TrimSpace(image.URL)) - if err != nil { - return "" - } - return c.baseURL.ResolveReference(ref).String() - case strings.TrimSpace(image.RemoteURL) != "": - return strings.TrimSpace(image.RemoteURL) - default: - return "" - } -} - -func imageFileName(item ManagedItem, endpoint *url.URL, mimeType string) string { - baseName := sanitizeFileName(strings.TrimSpace(item.Title)) - if baseName == "" { - baseName = fmt.Sprintf("arrtrix-%d", item.ID) - } - - ext := strings.TrimSpace(filepath.Ext(endpoint.Path)) - if ext == "" && mimeType != "" { - if extensions, err := mime.ExtensionsByType(mimeType); err == nil && len(extensions) > 0 { - ext = extensions[0] - } - } - if ext == "" { - ext = ".jpg" - } - if item.ID != 0 { - return fmt.Sprintf("%s-%d%s", baseName, item.ID, ext) - } - return baseName + ext -} - -func sanitizeFileName(value string) string { - replacer := strings.NewReplacer( - "<", "", - ">", "", - ":", "", - "\"", "", - "/", "-", - "\\", "-", - "|", "-", - "?", "", - "*", "", - ) - value = replacer.Replace(value) - value = strings.Join(strings.Fields(value), "-") - return strings.Trim(value, ".- ") -} - -func sameHost(left, right *url.URL) bool { - if left == nil || right == nil { - return false - } - return strings.EqualFold(left.Scheme, right.Scheme) && strings.EqualFold(left.Host, right.Host) -} diff --git a/packages/arrtrix/pkg/arrclient/client_test.go b/packages/arrtrix/pkg/arrclient/client_test.go deleted file mode 100644 index ecce6c3..0000000 --- a/packages/arrtrix/pkg/arrclient/client_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package arrclient - -import ( - "context" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" -) - -func TestImageURLPrefersPosterAndResolvesRelativePath(t *testing.T) { - baseURL, err := url.Parse("https://radarr.example") - if err != nil { - t.Fatalf("failed to parse base URL: %v", err) - } - - client := &httpClient{baseURL: baseURL} - imageURL := client.imageURL([]mediaImage{ - {CoverType: "fanart", URL: "/MediaCover/1/fanart.jpg"}, - {CoverType: "poster", URL: "/MediaCover/1/poster.jpg"}, - }) - if imageURL != "https://radarr.example/MediaCover/1/poster.jpg" { - t.Fatalf("unexpected image URL %q", imageURL) - } -} - -func TestImageURLFallsBackToRemoteURL(t *testing.T) { - baseURL, err := url.Parse("https://sonarr.example") - if err != nil { - t.Fatalf("failed to parse base URL: %v", err) - } - - client := &httpClient{baseURL: baseURL} - imageURL := client.imageURL([]mediaImage{ - {CoverType: "poster", RemoteURL: "https://images.example/poster.jpg"}, - }) - if imageURL != "https://images.example/poster.jpg" { - t.Fatalf("unexpected remote image URL %q", imageURL) - } -} - -func TestFetchImageUsesAPIKeyForSameHost(t *testing.T) { - headers := make(chan string, 1) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - headers <- r.Header.Get("X-Api-Key") - w.Header().Set("Content-Type", "image/jpeg") - _, _ = w.Write([]byte("jpeg-bytes")) - })) - defer server.Close() - - client, err := newHTTPClient(server.URL, "secret") - if err != nil { - t.Fatalf("failed to create client: %v", err) - } - - asset, err := client.FetchImage(context.Background(), ManagedItem{ - ID: 42, - Title: "Dune Part Two", - ImageURL: server.URL + "/MediaCover/42/poster.jpg", - }) - if err != nil { - t.Fatalf("failed to fetch image: %v", err) - } - if asset == nil { - t.Fatal("expected media asset") - } - if got := <-headers; got != "secret" { - t.Fatalf("expected API key header, got %q", got) - } - if got := string(asset.Data); got != "jpeg-bytes" { - t.Fatalf("unexpected media bytes %q", got) - } - if asset.MimeType != "image/jpeg" { - t.Fatalf("unexpected mime type %q", asset.MimeType) - } - if !strings.HasPrefix(asset.FileName, "Dune-Part-Two-42") || !strings.HasSuffix(asset.FileName, ".jpg") { - t.Fatalf("unexpected filename %q", asset.FileName) - } -} diff --git a/packages/arrtrix/pkg/arrclient/radarr.go b/packages/arrtrix/pkg/arrclient/radarr.go deleted file mode 100644 index e214ce3..0000000 --- a/packages/arrtrix/pkg/arrclient/radarr.go +++ /dev/null @@ -1,172 +0,0 @@ -package arrclient - -import ( - "context" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - - "sneeuwvlok/packages/arrtrix/pkg/arr" -) - -type RadarrClient struct { - http *httpClient - config RadarrConfig -} - -type radarrMovie struct { - ID int64 `json:"id"` - Title string `json:"title"` - Year int `json:"year"` - TMDBID int64 `json:"tmdbId"` - Overview string `json:"overview"` - Monitored bool `json:"monitored"` - Path string `json:"path"` - Images []mediaImage `json:"images"` -} - -func NewRadarrClient(config RadarrConfig) (*RadarrClient, error) { - config.ApplyDefaults() - if err := config.Validate(); err != nil { - return nil, err - } - httpClient, err := newHTTPClient(config.URL, config.APIKey) - if err != nil { - return nil, err - } - return &RadarrClient{http: httpClient, config: config}, nil -} - -func (c *RadarrClient) ContentType() arr.ContentType { - return arr.ContentTypeMovies -} - -func (c *RadarrClient) Search(ctx context.Context, query string) ([]SearchResult, error) { - var response []radarrMovie - if err := c.http.do(ctx, http.MethodGet, "/api/v3/movie/lookup", url.Values{"term": {strings.TrimSpace(query)}}, nil, &response); err != nil { - return nil, err - } - - results := make([]SearchResult, 0, len(response)) - for _, movie := range response { - if movie.TMDBID == 0 { - continue - } - results = append(results, SearchResult{ - LookupID: movie.TMDBID, - Title: movie.Title, - Year: movie.Year, - Overview: movie.Overview, - }) - } - return results, nil -} - -func (c *RadarrClient) List(ctx context.Context, query string) ([]ManagedItem, error) { - var response []radarrMovie - if err := c.http.do(ctx, http.MethodGet, "/api/v3/movie", nil, nil, &response); err != nil { - return nil, err - } - - items := make([]ManagedItem, 0, len(response)) - for _, movie := range response { - if query != "" && !containsFold(movie.Title, query) && !containsFold(strconv.Itoa(movie.Year), query) { - continue - } - items = append(items, ManagedItem{ - ID: movie.ID, - LookupID: movie.TMDBID, - Title: movie.Title, - Year: movie.Year, - Monitored: movie.Monitored, - Path: movie.Path, - ImageURL: c.http.imageURL(movie.Images), - }) - } - return items, nil -} - -func (c *RadarrClient) Add(ctx context.Context, result SearchResult) (*ManagedItem, error) { - payload := map[string]any{ - "title": result.Title, - "tmdbId": result.LookupID, - "year": result.Year, - "qualityProfileId": c.config.QualityProfileID, - "rootFolderPath": c.config.RootFolderPath, - "minimumAvailability": c.config.MinimumAvailability, - "monitored": true, - "addOptions": map[string]any{ - "searchForMovie": c.config.SearchOnAddValue(), - }, - } - - var response radarrMovie - if err := c.http.do(ctx, http.MethodPost, "/api/v3/movie", nil, payload, &response); err != nil { - return nil, err - } - item := ManagedItem{ - ID: response.ID, - LookupID: response.TMDBID, - Title: response.Title, - Year: response.Year, - Monitored: response.Monitored, - Path: response.Path, - ImageURL: c.http.imageURL(response.Images), - } - return &item, nil -} - -func (c *RadarrClient) SetMonitored(ctx context.Context, id int64, monitored bool) (*ManagedItem, error) { - var movie map[string]any - endpoint := "/api/v3/movie/" + strconv.FormatInt(id, 10) - if err := c.http.do(ctx, http.MethodGet, endpoint, nil, nil, &movie); err != nil { - return nil, err - } - movie["monitored"] = monitored - - var response radarrMovie - if err := c.http.do(ctx, http.MethodPut, endpoint, nil, movie, &response); err != nil { - return nil, err - } - item := ManagedItem{ - ID: response.ID, - LookupID: response.TMDBID, - Title: response.Title, - Year: response.Year, - Monitored: response.Monitored, - Path: response.Path, - ImageURL: c.http.imageURL(response.Images), - } - return &item, nil -} - -func (c *RadarrClient) Delete(ctx context.Context, id int64) error { - return c.http.do(ctx, http.MethodDelete, "/api/v3/movie/"+strconv.FormatInt(id, 10), url.Values{ - "deleteFiles": {"false"}, - "addImportExclusion": {"false"}, - }, nil, nil) -} - -func (c *RadarrClient) FetchImage(ctx context.Context, item ManagedItem) (*MediaAsset, error) { - return c.http.FetchImage(ctx, item) -} - -func PickSingleResult(results []SearchResult, query string) (SearchResult, error) { - switch len(results) { - case 0: - return SearchResult{}, fmt.Errorf("no matching result found for %q", query) - case 1: - return results[0], nil - default: - normalized := strings.TrimSpace(strings.ToLower(query)) - for _, result := range results { - title := strings.ToLower(FormatSearchResult(result)) - if title == normalized { - return result, nil - } - } - return SearchResult{}, fmt.Errorf("multiple results matched %q", query) - } -} diff --git a/packages/arrtrix/pkg/arrclient/sonarr.go b/packages/arrtrix/pkg/arrclient/sonarr.go deleted file mode 100644 index caa6cec..0000000 --- a/packages/arrtrix/pkg/arrclient/sonarr.go +++ /dev/null @@ -1,157 +0,0 @@ -package arrclient - -import ( - "context" - "net/http" - "net/url" - "strconv" - "strings" - - "sneeuwvlok/packages/arrtrix/pkg/arr" -) - -type SonarrClient struct { - http *httpClient - config SonarrConfig -} - -type sonarrSeries struct { - ID int64 `json:"id"` - Title string `json:"title"` - Year int `json:"year"` - TVDBID int64 `json:"tvdbId"` - Overview string `json:"overview"` - Monitored bool `json:"monitored"` - Path string `json:"path"` - Images []mediaImage `json:"images"` -} - -func NewSonarrClient(config SonarrConfig) (*SonarrClient, error) { - config.ApplyDefaults() - if err := config.Validate(); err != nil { - return nil, err - } - httpClient, err := newHTTPClient(config.URL, config.APIKey) - if err != nil { - return nil, err - } - return &SonarrClient{http: httpClient, config: config}, nil -} - -func (c *SonarrClient) ContentType() arr.ContentType { - return arr.ContentTypeSeries -} - -func (c *SonarrClient) Search(ctx context.Context, query string) ([]SearchResult, error) { - var response []sonarrSeries - if err := c.http.do(ctx, http.MethodGet, "/api/v3/series/lookup", url.Values{"term": {strings.TrimSpace(query)}}, nil, &response); err != nil { - return nil, err - } - - results := make([]SearchResult, 0, len(response)) - for _, series := range response { - if series.TVDBID == 0 { - continue - } - results = append(results, SearchResult{ - LookupID: series.TVDBID, - Title: series.Title, - Year: series.Year, - Overview: series.Overview, - }) - } - return results, nil -} - -func (c *SonarrClient) List(ctx context.Context, query string) ([]ManagedItem, error) { - var response []sonarrSeries - if err := c.http.do(ctx, http.MethodGet, "/api/v3/series", nil, nil, &response); err != nil { - return nil, err - } - - items := make([]ManagedItem, 0, len(response)) - for _, series := range response { - if query != "" && !containsFold(series.Title, query) && !containsFold(strconv.Itoa(series.Year), query) { - continue - } - items = append(items, ManagedItem{ - ID: series.ID, - LookupID: series.TVDBID, - Title: series.Title, - Year: series.Year, - Monitored: series.Monitored, - Path: series.Path, - ImageURL: c.http.imageURL(series.Images), - }) - } - return items, nil -} - -func (c *SonarrClient) Add(ctx context.Context, result SearchResult) (*ManagedItem, error) { - payload := map[string]any{ - "title": result.Title, - "tvdbId": result.LookupID, - "qualityProfileId": c.config.QualityProfileID, - "languageProfileId": c.config.LanguageProfileID, - "rootFolderPath": c.config.RootFolderPath, - "seasonFolder": c.config.SeasonFolderValue(), - "monitored": true, - "seriesType": c.config.SeriesType, - "addOptions": map[string]any{ - "searchForMissingEpisodes": c.config.SearchOnAddValue(), - }, - } - if result.Year != 0 { - payload["year"] = result.Year - } - - var response sonarrSeries - if err := c.http.do(ctx, http.MethodPost, "/api/v3/series", nil, payload, &response); err != nil { - return nil, err - } - item := ManagedItem{ - ID: response.ID, - LookupID: response.TVDBID, - Title: response.Title, - Year: response.Year, - Monitored: response.Monitored, - Path: response.Path, - ImageURL: c.http.imageURL(response.Images), - } - return &item, nil -} - -func (c *SonarrClient) SetMonitored(ctx context.Context, id int64, monitored bool) (*ManagedItem, error) { - var series map[string]any - endpoint := "/api/v3/series/" + strconv.FormatInt(id, 10) - if err := c.http.do(ctx, http.MethodGet, endpoint, nil, nil, &series); err != nil { - return nil, err - } - series["monitored"] = monitored - - var response sonarrSeries - if err := c.http.do(ctx, http.MethodPut, endpoint, nil, series, &response); err != nil { - return nil, err - } - item := ManagedItem{ - ID: response.ID, - LookupID: response.TVDBID, - Title: response.Title, - Year: response.Year, - Monitored: response.Monitored, - Path: response.Path, - ImageURL: c.http.imageURL(response.Images), - } - return &item, nil -} - -func (c *SonarrClient) Delete(ctx context.Context, id int64) error { - return c.http.do(ctx, http.MethodDelete, "/api/v3/series/"+strconv.FormatInt(id, 10), url.Values{ - "deleteFiles": {"false"}, - "addImportListExclusion": {"false"}, - }, nil, nil) -} - -func (c *SonarrClient) FetchImage(ctx context.Context, item ManagedItem) (*MediaAsset, error) { - return c.http.FetchImage(ctx, item) -} diff --git a/packages/arrtrix/pkg/config/config.go b/packages/arrtrix/pkg/config/config.go deleted file mode 100644 index ff97e98..0000000 --- a/packages/arrtrix/pkg/config/config.go +++ /dev/null @@ -1,65 +0,0 @@ -package config - -import ( - "go.mau.fi/util/dbutil" - "go.mau.fi/zeroconfig" - "gopkg.in/yaml.v3" - - "maunium.net/go/mautrix/bridgev2/bridgeconfig" - - "sneeuwvlok/packages/arrtrix/pkg/observability" -) - -type Config struct { - Network yaml.Node `yaml:"network"` - - Bridge bridgeconfig.BridgeConfig `yaml:"bridge"` - Database dbutil.Config `yaml:"database"` - Homeserver bridgeconfig.HomeserverConfig `yaml:"homeserver"` - AppService bridgeconfig.AppserviceConfig `yaml:"appservice"` - Logging zeroconfig.Config `yaml:"logging"` - - Observability observability.Config `yaml:"observability"` - EnvConfigPrefix string `yaml:"env_config_prefix"` - ManagementTexts bridgeconfig.ManagementRoomTexts `yaml:"management_room_texts"` -} - -func Load(data []byte) (*Config, error) { - var cfg Config - if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, err - } - cfg.applyDefaults() - return &cfg, nil -} - -func (c *Config) applyDefaults() { - if c.Homeserver.Software == "" { - c.Homeserver.Software = bridgeconfig.SoftwareStandard - } - c.Observability.ApplyDefaults() -} - -func (c *Config) Compile() bridgeconfig.Config { - return bridgeconfig.Config{ - Network: c.Network, - Bridge: c.Bridge, - Database: c.Database, - Homeserver: c.Homeserver, - AppService: c.AppService, - Logging: c.Logging, - EnvConfigPrefix: c.EnvConfigPrefix, - ManagementRoomTexts: c.ManagementTexts, - Matrix: bridgeconfig.MatrixConfig{ - MessageStatusEvents: false, - DeliveryReceipts: false, - MessageErrorNotices: true, - SyncDirectChatList: false, - FederateRooms: true, - }, - DoublePuppet: bridgeconfig.DoublePuppetConfig{ - Servers: map[string]string{}, - Secrets: map[string]string{}, - }, - } -} diff --git a/packages/arrtrix/pkg/config/config_test.go b/packages/arrtrix/pkg/config/config_test.go deleted file mode 100644 index 84b09df..0000000 --- a/packages/arrtrix/pkg/config/config_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package config - -import ( - "testing" - - "maunium.net/go/mautrix/bridgev2/bridgeconfig" -) - -func TestLoadDefaultsHomeserverSoftware(t *testing.T) { - cfg, err := Load([]byte(` -bridge: - command_prefix: "!arr" -homeserver: - address: http://127.0.0.1:8008 - domain: test.local -appservice: - id: arrtrix - bot: - username: arrtrixbot - displayname: Arrtrix Bot - username_template: arrtrix_{{.}} -database: - type: sqlite3-fk-wal - uri: file:arrtrix.db?_txlock=immediate -logging: - min_level: info - writers: - - type: stdout - format: pretty-colored -`)) - if err != nil { - t.Fatalf("Load returned error: %v", err) - } - - if cfg.Homeserver.Software != bridgeconfig.SoftwareStandard { - t.Fatalf("expected homeserver software default %q, got %q", bridgeconfig.SoftwareStandard, cfg.Homeserver.Software) - } -} - -func TestCompileSetsInternalDefaultsForHiddenSections(t *testing.T) { - cfg, err := Load([]byte(` -bridge: - command_prefix: "!arr" - permissions: - "*": relay -homeserver: - address: http://127.0.0.1:8008 - domain: test.local -appservice: - id: arrtrix - bot: - username: arrtrixbot - displayname: Arrtrix Bot - username_template: arrtrix_{{.}} -database: - type: sqlite3-fk-wal - uri: file:arrtrix.db?_txlock=immediate -logging: - min_level: info - writers: - - type: stdout - format: pretty-colored -`)) - if err != nil { - t.Fatalf("Load returned error: %v", err) - } - - runtimeCfg := cfg.Compile() - if !runtimeCfg.Matrix.MessageErrorNotices { - t.Fatalf("expected message error notices to stay enabled") - } - if !runtimeCfg.Matrix.FederateRooms { - t.Fatalf("expected federated rooms to stay enabled") - } - if runtimeCfg.DoublePuppet.Servers == nil || runtimeCfg.DoublePuppet.Secrets == nil { - t.Fatalf("expected hidden double puppet maps to be initialized") - } -} - -func TestLoadIgnoresLegacyHiddenSections(t *testing.T) { - cfg, err := Load([]byte(` -bridge: - command_prefix: "!arr" -homeserver: - address: http://127.0.0.1:8008 - domain: test.local -appservice: - id: arrtrix - bot: - username: arrtrixbot - displayname: Arrtrix Bot - username_template: arrtrix_{{.}} -database: - type: sqlite3-fk-wal - uri: file:arrtrix.db?_txlock=immediate -logging: - min_level: info - writers: - - type: stdout - format: pretty-colored -matrix: - federate_rooms: false -provisioning: - shared_secret: ignored -double_puppet: - secrets: - test.local: secret -encryption: - allow: true -`)) - if err != nil { - t.Fatalf("Load returned error: %v", err) - } - - runtimeCfg := cfg.Compile() - if !runtimeCfg.Matrix.FederateRooms { - t.Fatalf("expected runtime defaults to win for hidden legacy sections") - } - if len(runtimeCfg.DoublePuppet.Secrets) != 0 { - t.Fatalf("expected hidden double puppet secrets to stay internal-only") - } -} - -func TestLoadIgnoresLegacyWebhookSettings(t *testing.T) { - cfg, err := Load([]byte(` -network: - webhooks: - radarr: - enabled: true - path: /_arrtrix/webhooks/radarr - secret: legacy-secret -bridge: - command_prefix: "!arr" -homeserver: - address: http://127.0.0.1:8008 - domain: test.local -appservice: - id: arrtrix - bot: - username: arrtrixbot - displayname: Arrtrix Bot - username_template: arrtrix_{{.}} -database: - type: sqlite3-fk-wal - uri: file:arrtrix.db?_txlock=immediate -logging: - min_level: info - writers: - - type: stdout - format: pretty-colored -`)) - if err != nil { - t.Fatalf("Load returned error: %v", err) - } - - if cfg == nil { - t.Fatal("expected config to load") - } -} diff --git a/packages/arrtrix/pkg/connector/config.go b/packages/arrtrix/pkg/connector/config.go deleted file mode 100644 index 149fd32..0000000 --- a/packages/arrtrix/pkg/connector/config.go +++ /dev/null @@ -1,74 +0,0 @@ -package connector - -import ( - _ "embed" - "fmt" - "net/http" - - up "go.mau.fi/util/configupgrade" - "maunium.net/go/mautrix/bridgev2" - - "sneeuwvlok/packages/arrtrix/pkg/arr" - "sneeuwvlok/packages/arrtrix/pkg/arrclient" - "sneeuwvlok/packages/arrtrix/pkg/subscriptions" - "sneeuwvlok/packages/arrtrix/pkg/webhook" -) - -//go:embed example-config.yaml -var ExampleConfig string - -type Config struct { - Content ContentConfig `yaml:"content"` -} - -type ContentConfig struct { - Movies arrclient.RadarrConfig `yaml:"movies"` - Series arrclient.SonarrConfig `yaml:"series"` -} - -func upgradeConfig(helper up.Helper) {} - -func (s *ArrtrixConnector) GetConfig() (string, any, up.Upgrader) { - return ExampleConfig, &s.Config, up.SimpleUpgrader(upgradeConfig) -} - -func (s *ArrtrixConnector) ValidateConfig() error { - s.Config.Content.Movies.ApplyDefaults() - s.Config.Content.Series.ApplyDefaults() - if err := s.Config.Content.Movies.Validate(); err != nil { - return err - } - if err := s.Config.Content.Series.Validate(); err != nil { - return err - } - return nil -} - -func (s *ArrtrixConnector) MountRoutes(router *http.ServeMux) error { - if s.Bridge == nil { - return fmt.Errorf("bridge is not initialized") - } - return webhook.MountArr(router, s.Bridge, s.Subscriptions()) -} - -var _ bridgev2.ConfigValidatingNetwork = (*ArrtrixConnector)(nil) -var _ webhook.SubscriptionFilter = (*subscriptions.Repository)(nil) - -func (c ContentConfig) Client(contentType arr.ContentType) (arrclient.Client, bool, error) { - switch contentType { - case arr.ContentTypeMovies: - if !c.Movies.Enabled() { - return nil, false, nil - } - client, err := arrclient.NewRadarrClient(c.Movies) - return client, true, err - case arr.ContentTypeSeries: - if !c.Series.Enabled() { - return nil, false, nil - } - client, err := arrclient.NewSonarrClient(c.Series) - return client, true, err - default: - return nil, false, fmt.Errorf("unsupported content type %q", contentType) - } -} diff --git a/packages/arrtrix/pkg/connector/config_test.go b/packages/arrtrix/pkg/connector/config_test.go deleted file mode 100644 index 9516e37..0000000 --- a/packages/arrtrix/pkg/connector/config_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package connector - -import "testing" - -func TestValidateConfigRejectsPartialMoviesConfig(t *testing.T) { - conn := &ArrtrixConnector{ - Config: Config{ - Content: ContentConfig{}, - }, - } - conn.Config.Content.Movies.URL = "http://radarr.test" - - if err := conn.ValidateConfig(); err == nil { - t.Fatal("expected partial movies config to fail validation") - } -} - -func TestValidateConfigAllowsEmptyContentConfig(t *testing.T) { - conn := &ArrtrixConnector{} - if err := conn.ValidateConfig(); err != nil { - t.Fatalf("ValidateConfig returned error: %v", err) - } -} diff --git a/packages/arrtrix/pkg/connector/connector.go b/packages/arrtrix/pkg/connector/connector.go deleted file mode 100644 index 4be007a..0000000 --- a/packages/arrtrix/pkg/connector/connector.go +++ /dev/null @@ -1,135 +0,0 @@ -package connector - -import ( - "context" - "fmt" - "net/http" - - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/database" - "maunium.net/go/mautrix/bridgev2/networkid" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" - - "sneeuwvlok/packages/arrtrix/pkg/arr" - "sneeuwvlok/packages/arrtrix/pkg/arrclient" - "sneeuwvlok/packages/arrtrix/pkg/subscriptions" -) - -type ArrtrixConnector struct { - Bridge *bridgev2.Bridge - Config Config - clients map[arr.ContentType]arrclient.Client - subscriptions *subscriptions.Repository -} - -var _ bridgev2.NetworkConnector = (*ArrtrixConnector)(nil) -var _ interface{ MountRoutes(*http.ServeMux) error } = (*ArrtrixConnector)(nil) - -func (s *ArrtrixConnector) GetName() bridgev2.BridgeName { - return bridgev2.BridgeName{ - DisplayName: "Arrtrix", - NetworkURL: "https://wiki.servarr.com/", - NetworkID: "arrtrix", - BeeperBridgeType: "arrtrix", - DefaultPort: 29329, - DefaultCommandPrefix: "!arr", - } -} - -func (s *ArrtrixConnector) Init(bridge *bridgev2.Bridge) { - s.Bridge = bridge - s.subscriptions = subscriptions.NewRepository(bridge.DB.Database, string(bridge.ID)) - s.clients = make(map[arr.ContentType]arrclient.Client) - for _, contentType := range arr.SupportedContentTypes() { - client, ok, err := s.Config.Content.Client(contentType) - if err != nil { - panic(err) - } - if ok { - s.clients[contentType] = client - } - } -} - -func (s *ArrtrixConnector) Start(context.Context) error { - return nil -} - -func (s *ArrtrixConnector) GetDBMetaTypes() database.MetaTypes { - return database.MetaTypes{} -} - -func (s *ArrtrixConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities { - return &bridgev2.NetworkGeneralCapabilities{} -} - -func (s *ArrtrixConnector) LoadUserLogin(_ context.Context, login *bridgev2.UserLogin) error { - login.Client = &ArrtrixClient{ - Main: s, - UserLogin: login, - } - return nil -} - -func (s *ArrtrixConnector) GetLoginFlows() []bridgev2.LoginFlow { - return nil -} - -func (s *ArrtrixConnector) CreateLogin(_ context.Context, _ *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) { - return nil, fmt.Errorf("login flow %q is not implemented", flowID) -} - -func (s *ArrtrixConnector) GetBridgeInfoVersion() (info, capabilities int) { - return 1, 1 -} - -type ArrtrixClient struct { - Main *ArrtrixConnector - UserLogin *bridgev2.UserLogin -} - -var _ bridgev2.NetworkAPI = (*ArrtrixClient)(nil) - -func (c *ArrtrixClient) Connect(context.Context) {} - -func (c *ArrtrixClient) Disconnect() {} - -func (c *ArrtrixClient) IsLoggedIn() bool { - return false -} - -func (c *ArrtrixClient) LogoutRemote(context.Context) {} - -func (c *ArrtrixClient) IsThisUser(context.Context, networkid.UserID) bool { - return false -} - -func (c *ArrtrixClient) GetChatInfo(context.Context, *bridgev2.Portal) (*bridgev2.ChatInfo, error) { - return &bridgev2.ChatInfo{}, nil -} - -func (c *ArrtrixClient) GetUserInfo(context.Context, *bridgev2.Ghost) (*bridgev2.UserInfo, error) { - return &bridgev2.UserInfo{}, nil -} - -func (c *ArrtrixClient) GetCapabilities(context.Context, *bridgev2.Portal) *event.RoomFeatures { - return &event.RoomFeatures{} -} - -func (c *ArrtrixClient) HandleMatrixMessage(context.Context, *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) { - return nil, fmt.Errorf("bridging Matrix messages is not implemented") -} - -func (c *ArrtrixClient) GenerateTransactionID(userID id.UserID, roomID id.RoomID, eventType event.Type) networkid.RawTransactionID { - return networkid.RawTransactionID("") -} - -func (s *ArrtrixConnector) ContentClient(contentType arr.ContentType) (arrclient.Client, bool) { - client, ok := s.clients[contentType] - return client, ok -} - -func (s *ArrtrixConnector) Subscriptions() *subscriptions.Repository { - return s.subscriptions -} diff --git a/packages/arrtrix/pkg/connector/example-config.yaml b/packages/arrtrix/pkg/connector/example-config.yaml deleted file mode 100644 index a917e23..0000000 --- a/packages/arrtrix/pkg/connector/example-config.yaml +++ /dev/null @@ -1,23 +0,0 @@ -content: - movies: - # Radarr connection for movie management commands. - url: "" - api_key: "" - root_folder_path: "" - quality_profile_id: 0 - minimum_availability: released - search_on_add: true - - series: - # Sonarr connection for series management commands. - url: "" - api_key: "" - root_folder_path: "" - quality_profile_id: 0 - language_profile_id: 0 - season_folder: true - series_type: standard - search_on_add: true - -# Arr-stack webhooks are exposed automatically on the fixed built-in path: -# POST /_arrtrix/webhook diff --git a/packages/arrtrix/pkg/matrixcmd/download.go b/packages/arrtrix/pkg/matrixcmd/download.go deleted file mode 100644 index 23414b1..0000000 --- a/packages/arrtrix/pkg/matrixcmd/download.go +++ /dev/null @@ -1,260 +0,0 @@ -package matrixcmd - -import ( - "fmt" - "strconv" - "strings" - - "maunium.net/go/mautrix/id" - - "sneeuwvlok/packages/arrtrix/pkg/arr" - "sneeuwvlok/packages/arrtrix/pkg/arrclient" - "sneeuwvlok/packages/arrtrix/pkg/subscriptions" -) - -type commandServiceProvider interface { - ContentClient(arr.ContentType) (arrclient.Client, bool) - Subscriptions() *subscriptions.Repository -} - -func NewDownloadHandler() Handler { - return NewHandler(Meta{ - Name: "download", - Description: "Manage monitored movies and series in Arr.", - Usage: " [...]", - }, func(ctx *Context) { - if len(ctx.Args) < 2 { - ctx.Reply("Usage: `download [...]`") - return - } - - contentType, err := arr.ParseContentType(ctx.Args[1]) - if err != nil { - ctx.Reply(err.Error()) - return - } - - client, ok := contentClient(ctx, contentType) - if !ok { - ctx.Reply("No %s client is configured yet.", contentType.Label()) - return - } - - switch strings.ToLower(ctx.Args[0]) { - case "list": - handleDownloadList(ctx, client, contentType) - case "search": - handleDownloadSearch(ctx, client, contentType) - case "add": - handleDownloadAdd(ctx, client, contentType) - case "monitor": - handleDownloadMonitor(ctx, client, contentType) - case "remove": - handleDownloadRemove(ctx, client, contentType) - default: - ctx.Reply("Unknown download subcommand `%s`.", ctx.Args[0]) - } - }) -} - -func handleDownloadList(ctx *Context, client arrclient.Client, contentType arr.ContentType) { - query := strings.TrimSpace(strings.Join(ctx.Args[2:], " ")) - items, err := client.List(ctx.Ctx, query) - if err != nil { - ctx.Reply("Failed to list %s: %v", contentType.Label(), err) - return - } - if len(items) == 0 { - if query == "" { - ctx.Reply("No monitored %s are currently tracked.", contentType.Label()) - } else { - ctx.Reply("No %s matched `%s`.", contentType.Label(), query) - } - return - } - - count := len(items) - if count > 12 { - count = 12 - } - ctx.Reply("Tracked %s (showing %d of %d):", contentType.Label(), count, len(items)) - for i, item := range items { - if i == 12 { - break - } - if err := replyWithManagedItem(ctx, client, item); err != nil { - ctx.Log.Err(err).Int64("item_id", item.ID).Str("content_type", contentType.Label()).Msg("Failed to send Matrix-native image for download listing") - } - } - if len(items) > 12 { - ctx.Reply("…and %d more.", len(items)-12) - } -} - -func handleDownloadSearch(ctx *Context, client arrclient.Client, contentType arr.ContentType) { - query := strings.TrimSpace(strings.Join(ctx.Args[2:], " ")) - if query == "" { - ctx.Reply("Usage: `download search %s `", contentType.Label()) - return - } - results, err := client.Search(ctx.Ctx, query) - if err != nil { - ctx.Reply("Failed to search %s: %v", contentType.Label(), err) - return - } - replyWithSearchResults(ctx, contentType, query, results) -} - -func handleDownloadAdd(ctx *Context, client arrclient.Client, contentType arr.ContentType) { - query := strings.TrimSpace(strings.Join(ctx.Args[2:], " ")) - if query == "" { - ctx.Reply("Usage: `download add %s `", contentType.Label()) - return - } - results, err := client.Search(ctx.Ctx, query) - if err != nil { - ctx.Reply("Failed to search %s: %v", contentType.Label(), err) - return - } - result, err := arrclient.PickSingleResult(results, query) - if err != nil { - replyWithSearchResults(ctx, contentType, query, results) - return - } - item, err := client.Add(ctx.Ctx, result) - if err != nil { - ctx.Reply("Failed to add %s: %v", contentType.Label(), err) - return - } - ctx.Reply("Added %s to %s with id `%d`.", formatManagedItem(*item), contentType.Label(), item.ID) -} - -func handleDownloadMonitor(ctx *Context, client arrclient.Client, contentType arr.ContentType) { - if len(ctx.Args) < 4 { - ctx.Reply("Usage: `download monitor %s `", contentType.Label()) - return - } - itemID, err := strconv.ParseInt(ctx.Args[2], 10, 64) - if err != nil { - ctx.Reply("Invalid %s id `%s`.", contentType.Label(), ctx.Args[2]) - return - } - - state, err := parseEnabled(ctx.Args[3]) - if err != nil { - ctx.Reply(err.Error()) - return - } - item, err := client.SetMonitored(ctx.Ctx, itemID, state) - if err != nil { - ctx.Reply("Failed to update %s monitoring: %v", contentType.Label(), err) - return - } - ctx.Reply("%s is now monitored=%t.", formatManagedItem(*item), item.Monitored) -} - -func handleDownloadRemove(ctx *Context, client arrclient.Client, contentType arr.ContentType) { - if len(ctx.Args) < 3 { - ctx.Reply("Usage: `download remove %s `", contentType.Label()) - return - } - itemID, err := strconv.ParseInt(ctx.Args[2], 10, 64) - if err != nil { - ctx.Reply("Invalid %s id `%s`.", contentType.Label(), ctx.Args[2]) - return - } - if err = client.Delete(ctx.Ctx, itemID); err != nil { - ctx.Reply("Failed to remove %s: %v", contentType.Label(), err) - return - } - ctx.Reply("Removed `%d` from %s.", itemID, contentType.Label()) -} - -func contentClient(ctx *Context, contentType arr.ContentType) (arrclient.Client, bool) { - provider, ok := ctx.Bridge.Network.(commandServiceProvider) - if !ok { - return nil, false - } - return provider.ContentClient(contentType) -} - -func contentSubscriptions(ctx *Context) *subscriptions.Repository { - provider, ok := ctx.Bridge.Network.(commandServiceProvider) - if !ok { - return nil - } - return provider.Subscriptions() -} - -func replyWithSearchResults(ctx *Context, contentType arr.ContentType, query string, results []arrclient.SearchResult) { - if len(results) == 0 { - ctx.Reply("No %s matched `%s`.", contentType.Label(), query) - return - } - - var builder strings.Builder - builder.WriteString(fmt.Sprintf("Search results for `%s` in %s:\n", query, contentType.Label())) - for i, result := range results { - if i == 8 { - builder.WriteString("…\n") - break - } - builder.WriteString(fmt.Sprintf("- `%d` %s\n", result.LookupID, arrclient.FormatSearchResult(result))) - } - builder.WriteString(fmt.Sprintf("\nRefine the query and rerun `download add %s ` until only one match remains.", contentType.Label())) - ctx.Reply(builder.String()) -} - -func formatManagedItem(item arrclient.ManagedItem) string { - return arrclient.FormatManagedItem(item) -} - -func parseEnabled(value string) (bool, error) { - switch strings.ToLower(strings.TrimSpace(value)) { - case "on", "true", "yes", "enabled": - return true, nil - case "off", "false", "no", "disabled": - return false, nil - default: - return false, fmt.Errorf("expected `on` or `off`, got `%s`", value) - } -} - -func userIDString(userID id.UserID) string { - return userID.String() -} - -func replyWithManagedItem(ctx *Context, client arrclient.Client, item arrclient.ManagedItem) error { - details := formatDownloadListCaption(item) - if item.ImageURL != "" { - asset, err := client.FetchImage(ctx.Ctx, item) - if err != nil { - ctx.Log.Err(err).Int64("item_id", item.ID).Msg("Failed to fetch poster for Matrix listing") - } else if asset != nil { - if err := ctx.SendImage(asset, details); err != nil { - ctx.Log.Err(err).Int64("item_id", item.ID).Msg("Failed to upload poster for Matrix listing") - } else { - return nil - } - } else { - ctx.Log.Debug().Int64("item_id", item.ID).Msg("Poster was empty for Matrix listing") - } - } - ctx.Reply(details) - return nil -} - -func formatDownloadListCaption(item arrclient.ManagedItem) string { - return fmt.Sprintf("%s %s", monitoredIcon(item.Monitored), arrclient.FormatManagedItem(item)) -} - -func formatDownloadListFallbackCard(item arrclient.ManagedItem) string { - return formatDownloadListCaption(item) -} - -func monitoredIcon(monitored bool) string { - if monitored { - return "👁" - } - return "🚫" -} diff --git a/packages/arrtrix/pkg/matrixcmd/download_test.go b/packages/arrtrix/pkg/matrixcmd/download_test.go deleted file mode 100644 index 19b93b9..0000000 --- a/packages/arrtrix/pkg/matrixcmd/download_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package matrixcmd - -import ( - "testing" - - "sneeuwvlok/packages/arrtrix/pkg/arrclient" -) - -func TestFormatDownloadListFallbackCardUsesMonitoredIcon(t *testing.T) { - item := arrclient.ManagedItem{ - ID: 1, - Title: "Severance", - Year: 2022, - Monitored: true, - } - - fallback := formatDownloadListFallbackCard(item) - if fallback != "👁 Severance (2022)" { - t.Fatalf("unexpected monitored fallback %q", fallback) - } -} - -func TestFormatDownloadListFallbackCardUsesUnmonitoredIcon(t *testing.T) { - item := arrclient.ManagedItem{ - ID: 7, - Title: "Andor", - Year: 2022, - Monitored: false, - } - - fallback := formatDownloadListFallbackCard(item) - if fallback != "🚫 Andor (2022)" { - t.Fatalf("unexpected unmonitored fallback %q", fallback) - } -} - -func TestMonitoredIcon(t *testing.T) { - if monitoredIcon(true) != "👁" { - t.Fatalf("expected monitored icon, got %q", monitoredIcon(true)) - } - if monitoredIcon(false) != "🚫" { - t.Fatalf("expected unmonitored icon, got %q", monitoredIcon(false)) - } -} diff --git a/packages/arrtrix/pkg/matrixcmd/help.go b/packages/arrtrix/pkg/matrixcmd/help.go deleted file mode 100644 index 7da0d84..0000000 --- a/packages/arrtrix/pkg/matrixcmd/help.go +++ /dev/null @@ -1,60 +0,0 @@ -package matrixcmd - -import ( - "fmt" - "sort" - "strings" -) - -func NewHelpHandler(proc *Processor) Handler { - return NewHandler(Meta{ - Name: "help", - Description: "Show this help message.", - }, func(ctx *Context) { - ctx.Reply(formatHelp(proc, ctx)) - }) -} - -func formatHelp(proc *Processor, ctx *Context) string { - var builder strings.Builder - - switch { - case ctx.RoomID == ctx.User.ManagementRoom: - builder.WriteString(fmt.Sprintf("This is your management room: prefixing commands with `%s` is not required.\n", ctx.Bridge.Config.CommandPrefix)) - case ctx.Portal != nil: - builder.WriteString(fmt.Sprintf("**This is a portal room**: you must always prefix commands with `%s`. Management commands will not be bridged.\n", ctx.Bridge.Config.CommandPrefix)) - default: - builder.WriteString(fmt.Sprintf("This is not your management room: prefixing commands with `%s` is required.\n", ctx.Bridge.Config.CommandPrefix)) - } - - builder.WriteString("Parameters in [square brackets] are optional, while parameters in are required.\n\n") - builder.WriteString("#### General\n") - - handlers := proc.Handlers() - sort.SliceStable(handlers, func(i, j int) bool { - return handlers[i].Meta().Name < handlers[j].Meta().Name - }) - for _, handler := range handlers { - meta := handler.Meta() - builder.WriteString("**") - builder.WriteString(meta.Name) - builder.WriteString("**") - if meta.Usage != "" { - builder.WriteByte(' ') - builder.WriteString(meta.Usage) - } - if meta.Description != "" { - builder.WriteString(" - ") - builder.WriteString(meta.Description) - } - builder.WriteByte('\n') - } - - if extra := strings.TrimSpace(ctx.Processor.texts.AdditionalHelp); extra != "" { - builder.WriteByte('\n') - builder.WriteString(extra) - builder.WriteByte('\n') - } - - return builder.String() -} diff --git a/packages/arrtrix/pkg/matrixcmd/help_test.go b/packages/arrtrix/pkg/matrixcmd/help_test.go deleted file mode 100644 index 817f7ed..0000000 --- a/packages/arrtrix/pkg/matrixcmd/help_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package matrixcmd - -import ( - "strings" - "testing" - - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/bridgeconfig" - "maunium.net/go/mautrix/bridgev2/database" - "maunium.net/go/mautrix/id" -) - -func TestFormatHelpManagementRoom(t *testing.T) { - roomID := id.RoomID("!arrtrix:test") - proc := &Processor{ - texts: bridgeconfig.ManagementRoomTexts{AdditionalHelp: "Extra help text."}, - command: make(map[string]Handler), - alias: make(map[string]string), - } - proc.Add(NewHelpHandler(proc)) - proc.Add(NewDownloadHandler()) - proc.Add(NewSubscriptionsHandler()) - - out := formatHelp(proc, &Context{ - Bridge: &bridgev2.Bridge{ - Config: &bridgeconfig.BridgeConfig{ - CommandPrefix: "!arr", - }, - }, - RoomID: roomID, - User: &bridgev2.User{User: &database.User{ManagementRoom: roomID}}, - Processor: proc, - }) - - for _, fragment := range []string{ - "prefixing commands with `!arr` is not required", - "**download** [...] - Manage monitored movies and series in Arr.", - "**help** - Show this help message.", - "**subscriptions** [movies|series] [event-type|all] - Manage notification subscriptions by content type and event type.", - "Extra help text.", - } { - if !strings.Contains(out, fragment) { - t.Fatalf("expected help output to contain %q, got:\n%s", fragment, out) - } - } -} diff --git a/packages/arrtrix/pkg/matrixcmd/processor.go b/packages/arrtrix/pkg/matrixcmd/processor.go deleted file mode 100644 index e9d3980..0000000 --- a/packages/arrtrix/pkg/matrixcmd/processor.go +++ /dev/null @@ -1,270 +0,0 @@ -package matrixcmd - -import ( - "context" - "fmt" - "runtime/debug" - "sort" - "strings" - - "github.com/rs/zerolog" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/bridgeconfig" - "maunium.net/go/mautrix/bridgev2/status" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/format" - "maunium.net/go/mautrix/id" - - "sneeuwvlok/packages/arrtrix/pkg/arrclient" - "sneeuwvlok/packages/arrtrix/pkg/observability" -) - -type Handler interface { - Meta() Meta - Run(*Context) -} - -type Meta struct { - Name string - Description string - Usage string - Aliases []string -} - -type HandlerFunc struct { - meta Meta - run func(*Context) -} - -func NewHandler(meta Meta, run func(*Context)) Handler { - return HandlerFunc{meta: meta, run: run} -} - -func (h HandlerFunc) Meta() Meta { - return h.meta -} - -func (h HandlerFunc) Run(ctx *Context) { - h.run(ctx) -} - -type Processor struct { - bridge *bridgev2.Bridge - bot bridgev2.MatrixAPI - texts bridgeconfig.ManagementRoomTexts - command map[string]Handler - alias map[string]string - order []string -} - -type Context struct { - Bridge *bridgev2.Bridge - Bot bridgev2.MatrixAPI - RoomID id.RoomID - OrigRoomID id.RoomID - EventID id.EventID - ReplyTo id.EventID - User *bridgev2.User - Portal *bridgev2.Portal - Command string - Args []string - RawArgs string - Ctx context.Context - Log *zerolog.Logger - Processor *Processor -} - -var _ bridgev2.CommandProcessor = (*Processor)(nil) - -func NewProcessor(bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts) *Processor { - proc := &Processor{ - bridge: bridge, - bot: bridge.Bot, - texts: texts, - command: make(map[string]Handler), - alias: make(map[string]string), - } - proc.Add(NewHelpHandler(proc)) - proc.Add(NewDownloadHandler()) - proc.Add(NewSubscriptionsHandler()) - return proc -} - -func (p *Processor) Add(handler Handler) { - meta := handler.Meta() - p.command[meta.Name] = handler - p.order = append(p.order, meta.Name) - for _, alias := range meta.Aliases { - p.alias[alias] = meta.Name - } -} - -func (p *Processor) Handlers() []Handler { - names := append([]string(nil), p.order...) - sort.Strings(names) - - handlers := make([]Handler, 0, len(names)) - for _, name := range names { - handler, ok := p.command[name] - if ok { - handlers = append(handlers, handler) - } - } - return handlers -} - -func (p *Processor) Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user *bridgev2.User, message string, replyTo id.EventID) { - ctx, span := observability.StartSpan(ctx, "arrtrix.matrix.command") - defer span.End() - - ms := &bridgev2.MessageStatus{ - Step: status.MsgStepCommand, - Status: event.MessageStatusSuccess, - } - - logCopy := zerolog.Ctx(ctx).With().Logger() - log := &logCopy - outcome := "success" - commandName := "unknown-command" - - defer func() { - statusInfo := &bridgev2.MessageStatusEventInfo{ - RoomID: roomID, - SourceEventID: eventID, - EventType: event.EventMessage, - Sender: user.MXID, - } - - if recovered := recover(); recovered != nil { - logEvt := log.Error().Bytes(zerolog.ErrorStackFieldName, debug.Stack()) - if err, ok := recovered.(error); ok { - logEvt = logEvt.Err(err) - ms.InternalError = err - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - } else { - logEvt = logEvt.Any(zerolog.ErrorFieldName, recovered) - ms.InternalError = fmt.Errorf("%v", recovered) - span.SetStatus(codes.Error, "panic") - } - logEvt.Msg("Panic in arrtrix Matrix command handler") - ms.Status = event.MessageStatusFail - ms.IsCertain = true - ms.ErrorAsMessage = true - outcome = "panic" - } - - observability.RecordCommand(ctx, commandName, outcome) - p.bridge.Matrix.SendMessageStatus(ctx, ms, statusInfo) - }() - - args := strings.Fields(message) - if len(args) == 0 { - args = []string{"unknown-command"} - } - - commandName = strings.ToLower(args[0]) - if actual, ok := p.alias[commandName]; ok { - commandName = actual - } - span.SetAttributes( - attribute.String("arrtrix.matrix.command.name", commandName), - attribute.String("matrix.room_id", roomID.String()), - ) - - portal, err := p.bridge.GetPortalByMXID(ctx, roomID) - if err != nil { - log.Err(err).Msg("Failed to get portal") - } - - commandCtx := &Context{ - Bridge: p.bridge, - Bot: p.bot, - RoomID: roomID, - OrigRoomID: roomID, - EventID: eventID, - ReplyTo: replyTo, - User: user, - Portal: portal, - Command: commandName, - Args: args[1:], - RawArgs: strings.TrimSpace(strings.TrimPrefix(message, args[0])), - Ctx: ctx, - Log: log, - Processor: p, - } - - handler, ok := p.command[commandName] - if !ok { - log.Debug().Str("mx_command", commandName).Msg("Received unknown Matrix room command") - span.SetStatus(codes.Error, "unknown command") - outcome = "unknown" - commandCtx.Reply("Unknown command, use the `help` command for help.") - return - } - - log.UpdateContext(func(c zerolog.Context) zerolog.Context { - return c.Str("mx_command", commandName) - }) - log.Debug().Msg("Received Matrix room command") - handler.Run(commandCtx) - span.SetStatus(codes.Ok, "") -} - -func (c *Context) Reply(message string, args ...any) { - message = strings.ReplaceAll(message, "$cmdprefix ", c.Bridge.Config.CommandPrefix+" ") - if len(args) > 0 { - message = fmt.Sprintf(message, args...) - } - - content := format.RenderMarkdown(message, true, false) - content.MsgType = event.MsgNotice - if err := c.sendNotice(&content); err != nil { - c.Log.Err(err).Msg("Failed to reply to Matrix room command") - } -} - -func (c *Context) ReplyFormatted(body, formattedBody string) { - content := &event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: body, - Format: event.FormatHTML, - FormattedBody: formattedBody, - } - if err := c.sendNotice(content); err != nil { - c.Log.Err(err).Msg("Failed to reply to Matrix room command") - } -} - -func (c *Context) SendImage(asset *arrclient.MediaAsset, body string) error { - if asset == nil || len(asset.Data) == 0 { - return nil - } - - mxcURL, file, err := c.Bot.UploadMedia(c.Ctx, c.OrigRoomID, asset.Data, asset.FileName, asset.MimeType) - if err != nil { - return err - } - - content := &event.MessageEventContent{ - MsgType: event.MsgImage, - Body: body, - FileName: asset.FileName, - URL: mxcURL, - File: file, - Info: &event.FileInfo{ - MimeType: asset.MimeType, - Size: len(asset.Data), - }, - } - _, err = c.Bot.SendMessage(c.Ctx, c.OrigRoomID, event.EventMessage, &event.Content{Parsed: content}, nil) - return err -} - -func (c *Context) sendNotice(content *event.MessageEventContent) error { - _, err := c.Bot.SendMessage(c.Ctx, c.OrigRoomID, event.EventMessage, &event.Content{Parsed: content}, nil) - return err -} diff --git a/packages/arrtrix/pkg/matrixcmd/subscriptions.go b/packages/arrtrix/pkg/matrixcmd/subscriptions.go deleted file mode 100644 index ed1a11f..0000000 --- a/packages/arrtrix/pkg/matrixcmd/subscriptions.go +++ /dev/null @@ -1,107 +0,0 @@ -package matrixcmd - -import ( - "context" - "fmt" - "strings" - - "maunium.net/go/mautrix/id" - - "sneeuwvlok/packages/arrtrix/pkg/arr" - "sneeuwvlok/packages/arrtrix/pkg/subscriptions" -) - -func NewSubscriptionsHandler() Handler { - return NewHandler(Meta{ - Name: "subscriptions", - Aliases: []string{"subscription", "notify"}, - Description: "Manage notification subscriptions by content type and event type.", - Usage: " [movies|series] [event-type|all]", - }, func(ctx *Context) { - repo := contentSubscriptions(ctx) - if repo == nil { - ctx.Reply("Subscription storage is not available.") - return - } - if len(ctx.Args) == 0 || strings.EqualFold(ctx.Args[0], "list") { - handleSubscriptionList(ctx, repo) - return - } - if len(ctx.Args) < 3 { - ctx.Reply("Usage: `subscriptions `") - return - } - - contentType, err := arr.ParseContentType(ctx.Args[1]) - if err != nil { - ctx.Reply(err.Error()) - return - } - eventType, err := arr.ParseEventType(contentType, ctx.Args[2]) - if err != nil { - ctx.Reply(err.Error()) - return - } - - switch strings.ToLower(ctx.Args[0]) { - case "enable": - handleSubscriptionSet(ctx, repo, contentType, eventType, true) - case "disable": - handleSubscriptionSet(ctx, repo, contentType, eventType, false) - default: - ctx.Reply("Unknown subscriptions subcommand `%s`.", ctx.Args[0]) - } - }) -} - -func handleSubscriptionList(ctx *Context, repo subscriptionRepo) { - preferences, err := repo.List(ctx.Ctx, ctx.User.MXID) - if err != nil { - ctx.Reply("Failed to load subscriptions: %v", err) - return - } - - var builder strings.Builder - builder.WriteString("Current notification subscriptions:\n") - for _, contentType := range arr.SupportedContentTypes() { - builder.WriteString(fmt.Sprintf("\n**%s**\n", strings.Title(contentType.Label()))) - for _, eventType := range arr.SupportedEventTypes(contentType) { - enabled := findPreference(preferences, contentType, eventType) - builder.WriteString(fmt.Sprintf("- `%s`: %t\n", eventType, enabled)) - } - } - ctx.Reply(builder.String()) -} - -func handleSubscriptionSet(ctx *Context, repo subscriptionRepo, contentType arr.ContentType, eventType string, enabled bool) { - var err error - if eventType == "all" { - err = repo.SetAll(ctx.Ctx, ctx.User.MXID, contentType, enabled) - } else { - err = repo.Set(ctx.Ctx, ctx.User.MXID, contentType, eventType, enabled) - } - if err != nil { - ctx.Reply("Failed to update subscriptions: %v", err) - return - } - if eventType == "all" { - ctx.Reply("Set all `%s` notifications for %s to %t.", contentType.Label(), userIDString(ctx.User.MXID), enabled) - return - } - ctx.Reply("Set `%s/%s` notifications to %t.", contentType.Label(), eventType, enabled) -} - -type subscriptionRepo interface { - List(ctx context.Context, userID id.UserID) ([]subscriptions.Preference, error) - Set(ctx context.Context, userID id.UserID, contentType arr.ContentType, eventType string, enabled bool) error - SetAll(ctx context.Context, userID id.UserID, contentType arr.ContentType, enabled bool) error -} - -func findPreference(preferences []subscriptions.Preference, contentType arr.ContentType, eventType string) bool { - for _, preference := range preferences { - if preference.ContentType == contentType && preference.EventType == eventType { - return preference.Enabled - } - } - return true -} diff --git a/packages/arrtrix/pkg/observability/config.go b/packages/arrtrix/pkg/observability/config.go deleted file mode 100644 index 187c5b5..0000000 --- a/packages/arrtrix/pkg/observability/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package observability - -import "strings" - -type Config struct { - OTLPGRPCEndpoint string `yaml:"otlp_grpc_endpoint"` - ServiceName string `yaml:"service_name"` - ResourceAttributes map[string]string `yaml:"resource_attributes"` -} - -func (c *Config) ApplyDefaults() { - if c.ServiceName == "" { - c.ServiceName = "arrtrix" - } - if c.ResourceAttributes == nil { - c.ResourceAttributes = map[string]string{} - } -} - -func (c Config) Enabled() bool { - return strings.TrimSpace(c.OTLPGRPCEndpoint) != "" -} diff --git a/packages/arrtrix/pkg/observability/otel.go b/packages/arrtrix/pkg/observability/otel.go deleted file mode 100644 index 2fe46ef..0000000 --- a/packages/arrtrix/pkg/observability/otel.go +++ /dev/null @@ -1,397 +0,0 @@ -package observability - -import ( - "context" - "errors" - "fmt" - "net/url" - "strings" - "sync" - "time" - - "github.com/rs/zerolog" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" - "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - otellog "go.opentelemetry.io/otel/log" - logglobal "go.opentelemetry.io/otel/log/global" - otelmetric "go.opentelemetry.io/otel/metric" - sdklog "go.opentelemetry.io/otel/sdk/log" - sdkmetric "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" -) - -const ( - instrumentationScope = "sneeuwvlok/packages/arrtrix" - logScope = instrumentationScope + "/logs" -) - -type Runtime struct { - traceProvider *sdktrace.TracerProvider - meterProvider *sdkmetric.MeterProvider - logProvider *sdklog.LoggerProvider - logHook zerolog.Hook -} - -type exporterEndpoint struct { - raw string - insecure bool -} - -type instruments struct { - webhookRequests otelCounter - webhookLatency otelHistogram - commandInvocations otelCounter - inviteEvents otelCounter - startupDuration otelHistogram -} - -type otelCounter interface { - Add(context.Context, int64, ...otelmetric.AddOption) -} - -type otelHistogram interface { - Record(context.Context, float64, ...otelmetric.RecordOption) -} - -var ( - mu sync.RWMutex - current instruments - tracer = otel.Tracer(instrumentationScope) - currentReady bool -) - -func Setup(ctx context.Context, cfg Config, version string) (*Runtime, error) { - cfg.ApplyDefaults() - if !cfg.Enabled() { - resetInstruments() - return &Runtime{}, nil - } - - res, err := buildResource(cfg, version) - if err != nil { - return nil, err - } - endpoint, err := parseEndpoint(cfg.OTLPGRPCEndpoint) - if err != nil { - return nil, err - } - - traceExporter, err := otlptracegrpc.New(ctx, traceOptions(endpoint)...) - if err != nil { - return nil, fmt.Errorf("create trace exporter: %w", err) - } - metricExporter, err := otlpmetricgrpc.New(ctx, metricOptions(endpoint)...) - if err != nil { - return nil, fmt.Errorf("create metric exporter: %w", err) - } - logExporter, err := otlploggrpc.New(ctx, logOptions(endpoint)...) - if err != nil { - return nil, fmt.Errorf("create log exporter: %w", err) - } - - traceProvider := sdktrace.NewTracerProvider( - sdktrace.WithResource(res), - sdktrace.WithBatcher(traceExporter), - ) - meterProvider := sdkmetric.NewMeterProvider( - sdkmetric.WithResource(res), - sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter, sdkmetric.WithInterval(30*time.Second))), - ) - logProvider := sdklog.NewLoggerProvider( - sdklog.WithResource(res), - sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)), - ) - - otel.SetTracerProvider(traceProvider) - otel.SetMeterProvider(meterProvider) - logglobal.SetLoggerProvider(logProvider) - - if err = setInstruments(meterProvider); err != nil { - _ = traceProvider.Shutdown(ctx) - _ = meterProvider.Shutdown(ctx) - _ = logProvider.Shutdown(ctx) - return nil, err - } - - tracer = otel.Tracer(instrumentationScope) - return &Runtime{ - traceProvider: traceProvider, - meterProvider: meterProvider, - logProvider: logProvider, - logHook: newLogHook(logglobal.Logger(logScope)), - }, nil -} - -func (r *Runtime) Enabled() bool { - return r != nil && r.traceProvider != nil -} - -func (r *Runtime) LoggerHook() zerolog.Hook { - if r == nil { - return nil - } - return r.logHook -} - -func (r *Runtime) Shutdown(ctx context.Context) error { - if r == nil || !r.Enabled() { - resetInstruments() - return nil - } - - var errs []error - if err := r.logProvider.Shutdown(ctx); err != nil { - errs = append(errs, fmt.Errorf("shutdown log provider: %w", err)) - } - if err := r.meterProvider.Shutdown(ctx); err != nil { - errs = append(errs, fmt.Errorf("shutdown meter provider: %w", err)) - } - if err := r.traceProvider.Shutdown(ctx); err != nil { - errs = append(errs, fmt.Errorf("shutdown trace provider: %w", err)) - } - resetInstruments() - return errors.Join(errs...) -} - -func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - return tracer.Start(ctx, name, opts...) -} - -func RecordWebhook(ctx context.Context, eventType, outcome string, statusCode int, duration time.Duration) { - mu.RLock() - inst := current - ready := currentReady - mu.RUnlock() - if !ready { - return - } - attrs := otelmetric.WithAttributes( - attribute.String("event_type", eventType), - attribute.String("outcome", outcome), - attribute.Int("http.status_code", statusCode), - ) - inst.webhookRequests.Add(ctx, 1, attrs) - inst.webhookLatency.Record(ctx, duration.Seconds(), attrs) -} - -func RecordCommand(ctx context.Context, name, outcome string) { - mu.RLock() - inst := current - ready := currentReady - mu.RUnlock() - if !ready { - return - } - inst.commandInvocations.Add(ctx, 1, otelmetric.WithAttributes( - attribute.String("command", name), - attribute.String("outcome", outcome), - )) -} - -func RecordInvite(ctx context.Context, outcome string) { - mu.RLock() - inst := current - ready := currentReady - mu.RUnlock() - if !ready { - return - } - inst.inviteEvents.Add(ctx, 1, otelmetric.WithAttributes(attribute.String("outcome", outcome))) -} - -func RecordStartupPhase(ctx context.Context, phase, outcome string, duration time.Duration) { - mu.RLock() - inst := current - ready := currentReady - mu.RUnlock() - if !ready { - return - } - inst.startupDuration.Record(ctx, duration.Seconds(), otelmetric.WithAttributes( - attribute.String("phase", phase), - attribute.String("outcome", outcome), - )) -} - -func parseEndpoint(raw string) (exporterEndpoint, error) { - raw = strings.TrimSpace(raw) - if raw == "" { - return exporterEndpoint{}, errors.New("observability.otlp_grpc_endpoint must not be empty when observability is enabled") - } - if strings.Contains(raw, "://") { - u, err := url.Parse(raw) - if err != nil { - return exporterEndpoint{}, fmt.Errorf("parse observability.otlp_grpc_endpoint: %w", err) - } - if u.Scheme == "" || u.Host == "" { - return exporterEndpoint{}, fmt.Errorf("invalid observability.otlp_grpc_endpoint %q", raw) - } - return exporterEndpoint{raw: raw, insecure: u.Scheme == "http"}, nil - } - return exporterEndpoint{raw: "http://" + raw, insecure: true}, nil -} - -func buildResource(cfg Config, version string) (*resource.Resource, error) { - attrs := []attribute.KeyValue{ - attribute.String("service.name", cfg.ServiceName), - } - if version != "" { - attrs = append(attrs, attribute.String("service.version", version)) - } - for key, value := range cfg.ResourceAttributes { - attrs = append(attrs, attribute.String(key, value)) - } - return resource.Merge(resource.Default(), resource.NewWithAttributes("", attrs...)) -} - -func setInstruments(provider *sdkmetric.MeterProvider) error { - meter := provider.Meter(instrumentationScope) - - webhookRequests, err := meter.Int64Counter( - "arrtrix.webhook.requests", - otelmetric.WithDescription("Number of Arr webhook requests handled by arrtrix."), - ) - if err != nil { - return fmt.Errorf("create webhook request counter: %w", err) - } - webhookLatency, err := meter.Float64Histogram( - "arrtrix.webhook.duration.seconds", - otelmetric.WithDescription("Duration of Arr webhook request handling."), - otelmetric.WithUnit("s"), - ) - if err != nil { - return fmt.Errorf("create webhook duration histogram: %w", err) - } - commandInvocations, err := meter.Int64Counter( - "arrtrix.matrix.commands", - otelmetric.WithDescription("Number of Matrix management-room commands handled by arrtrix."), - ) - if err != nil { - return fmt.Errorf("create command counter: %w", err) - } - inviteEvents, err := meter.Int64Counter( - "arrtrix.matrix.invites", - otelmetric.WithDescription("Number of management-room invite flows observed by arrtrix."), - ) - if err != nil { - return fmt.Errorf("create invite counter: %w", err) - } - startupDuration, err := meter.Float64Histogram( - "arrtrix.runtime.phase.duration.seconds", - otelmetric.WithDescription("Duration of arrtrix runtime startup and shutdown phases."), - otelmetric.WithUnit("s"), - ) - if err != nil { - return fmt.Errorf("create runtime duration histogram: %w", err) - } - - mu.Lock() - current = instruments{ - webhookRequests: webhookRequests, - webhookLatency: webhookLatency, - commandInvocations: commandInvocations, - inviteEvents: inviteEvents, - startupDuration: startupDuration, - } - currentReady = true - mu.Unlock() - return nil -} - -func resetInstruments() { - mu.Lock() - current = instruments{} - currentReady = false - mu.Unlock() -} - -func traceOptions(endpoint exporterEndpoint) []otlptracegrpc.Option { - opts := []otlptracegrpc.Option{otlptracegrpc.WithEndpointURL(endpoint.raw)} - if endpoint.insecure { - opts = append(opts, otlptracegrpc.WithInsecure()) - } - return opts -} - -func metricOptions(endpoint exporterEndpoint) []otlpmetricgrpc.Option { - opts := []otlpmetricgrpc.Option{otlpmetricgrpc.WithEndpointURL(endpoint.raw)} - if endpoint.insecure { - opts = append(opts, otlpmetricgrpc.WithInsecure()) - } - return opts -} - -func logOptions(endpoint exporterEndpoint) []otlploggrpc.Option { - opts := []otlploggrpc.Option{otlploggrpc.WithEndpointURL(endpoint.raw)} - if endpoint.insecure { - opts = append(opts, otlploggrpc.WithInsecure()) - } - return opts -} - -type otelLogHook struct { - logger otellog.Logger -} - -func newLogHook(logger otellog.Logger) zerolog.Hook { - return otelLogHook{logger: logger} -} - -func (h otelLogHook) Run(e *zerolog.Event, level zerolog.Level, message string) { - if h.logger == nil { - return - } - ctx := e.GetCtx() - if ctx == nil { - ctx = context.Background() - } - - severity := mapSeverity(level) - if !h.logger.Enabled(ctx, otellog.EnabledParameters{Severity: severity}) { - return - } - - now := time.Now() - record := otellog.Record{} - record.SetTimestamp(now) - record.SetObservedTimestamp(now) - record.SetSeverity(severity) - record.SetSeverityText(strings.ToUpper(level.String())) - record.SetBody(otellog.StringValue(message)) - record.AddAttributes(otellog.String("log.scope", logScope)) - - if spanCtx := trace.SpanContextFromContext(ctx); spanCtx.IsValid() { - record.AddAttributes( - otellog.String("trace_id", spanCtx.TraceID().String()), - otellog.String("span_id", spanCtx.SpanID().String()), - ) - } - - h.logger.Emit(ctx, record) -} - -func mapSeverity(level zerolog.Level) otellog.Severity { - switch level { - case zerolog.TraceLevel: - return otellog.SeverityTrace - case zerolog.DebugLevel: - return otellog.SeverityDebug - case zerolog.InfoLevel: - return otellog.SeverityInfo - case zerolog.WarnLevel: - return otellog.SeverityWarn - case zerolog.ErrorLevel: - return otellog.SeverityError - case zerolog.FatalLevel: - return otellog.SeverityFatal - case zerolog.PanicLevel: - return otellog.SeverityFatal4 - default: - return otellog.SeverityUndefined - } -} diff --git a/packages/arrtrix/pkg/observability/otel_test.go b/packages/arrtrix/pkg/observability/otel_test.go deleted file mode 100644 index 4dd8e3e..0000000 --- a/packages/arrtrix/pkg/observability/otel_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package observability - -import "testing" - -func TestConfigDefaults(t *testing.T) { - var cfg Config - cfg.ApplyDefaults() - - if cfg.ServiceName != "arrtrix" { - t.Fatalf("expected default service name arrtrix, got %q", cfg.ServiceName) - } - if cfg.ResourceAttributes == nil { - t.Fatal("expected resource attributes map to be initialized") - } - if cfg.Enabled() { - t.Fatal("expected observability to be disabled by default") - } -} - -func TestParseEndpointSupportsURLAndBareHost(t *testing.T) { - tests := []struct { - name string - input string - wantRaw string - insecure bool - wantError bool - }{ - {name: "https url", input: "https://otel.example:4317", wantRaw: "https://otel.example:4317"}, - {name: "http url", input: "http://127.0.0.1:4317", wantRaw: "http://127.0.0.1:4317", insecure: true}, - {name: "bare host", input: "collector:4317", wantRaw: "http://collector:4317", insecure: true}, - {name: "invalid", input: "://bad", wantError: true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseEndpoint(tt.input) - if tt.wantError { - if err == nil { - t.Fatal("expected error") - } - return - } - if err != nil { - t.Fatalf("parseEndpoint returned error: %v", err) - } - if got.raw != tt.wantRaw { - t.Fatalf("expected raw endpoint %q, got %q", tt.wantRaw, got.raw) - } - if got.insecure != tt.insecure { - t.Fatalf("expected insecure=%t, got %t", tt.insecure, got.insecure) - } - }) - } -} diff --git a/packages/arrtrix/pkg/onboarding/welcome.go b/packages/arrtrix/pkg/onboarding/welcome.go deleted file mode 100644 index e96ea7a..0000000 --- a/packages/arrtrix/pkg/onboarding/welcome.go +++ /dev/null @@ -1,171 +0,0 @@ -package onboarding - -import ( - "context" - "fmt" - "strings" - - "github.com/rs/zerolog" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/bridgeconfig" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/format" - "maunium.net/go/mautrix/id" - - "sneeuwvlok/packages/arrtrix/pkg/observability" -) - -const handledInviteEventType = "com.arrtrix.handled_invite" - -func HandleBotInvite(ctx context.Context, bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts, evt *event.Event) { - if evt.Type != event.StateMember || - evt.GetStateKey() != bridge.Bot.GetMXID().String() || - evt.Content.AsMember().Membership != event.MembershipInvite { - return - } - - ctx, span := observability.StartSpan(ctx, "arrtrix.matrix.invite") - defer span.End() - span.SetAttributes( - attribute.String("matrix.room_id", evt.RoomID.String()), - attribute.String("matrix.sender", evt.Sender.String()), - ) - outcome := "ignored" - defer observability.RecordInvite(ctx, outcome) - - log := zerolog.Ctx(ctx) - sender, err := bridge.GetUserByMXID(ctx, evt.Sender) - if err != nil { - outcome = "user_lookup_failed" - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - log.Err(err).Msg("Failed to load sender for bot invite") - return - } - if !sender.Permissions.Commands { - outcome = "permission_denied" - span.SetStatus(codes.Error, "sender lacks command permission") - return - } - - if err = bridge.Bot.EnsureJoined(ctx, evt.RoomID); err != nil { - outcome = "join_failed" - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - log.Err(err).Msg("Failed to accept invite to room") - return - } - - members, err := bridge.Matrix.GetMembers(ctx, evt.RoomID) - if err != nil { - outcome = "member_lookup_failed" - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - log.Err(err).Msg("Failed to get members of room after accepting invite") - return - } - if len(members) != 2 { - outcome = "non_management_room" - span.SetStatus(codes.Error, "invite room is not a direct management room") - return - } - - assignedManagementRoom := sender.ManagementRoom == "" - if assignedManagementRoom { - sender.ManagementRoom = evt.RoomID - if err = sender.Save(ctx); err != nil { - outcome = "management_room_save_failed" - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - log.Err(err).Msg("Failed to update user's management room in database") - return - } - } - - message := buildWelcomeMessage(bridge, texts, sender, assignedManagementRoom) - content := format.RenderMarkdown(message, true, false) - if _, err = bridge.Bot.SendMessage(ctx, evt.RoomID, event.EventMessage, &event.Content{Parsed: &content}, nil); err != nil { - outcome = "welcome_send_failed" - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - log.Err(err).Msg("Failed to send welcome message to room") - return - } - - outcome = "welcomed" - span.SetStatus(codes.Ok, "") - evt.Type = event.Type{Type: handledInviteEventType} -} - -func buildWelcomeMessage(bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts, sender *bridgev2.User, assignedManagementRoom bool) string { - return composeWelcomeMessage( - bridge.Network.GetName().DisplayName, - bridge.Config.CommandPrefix, - bridge.Bot.GetMXID(), - texts, - sender.GetDefaultLogin() != nil, - assignedManagementRoom, - ) -} - -func composeWelcomeMessage( - bridgeName string, - commandPrefix string, - botMXID id.UserID, - texts bridgeconfig.ManagementRoomTexts, - connected bool, - assignedManagementRoom bool, -) string { - replacer := strings.NewReplacer( - "$cmdprefix", commandPrefix, - "$bridge", bridgeName, - "$bot", string(botMXID), - ) - - var parts []string - - base := strings.TrimSpace(texts.Welcome) - if base == "" { - base = fmt.Sprintf("Hello, I'm the %s bot.", bridgeName) - } - parts = append(parts, replacer.Replace(base)) - - if assignedManagementRoom { - parts = append(parts, "This room has been marked as your management room.") - } else { - parts = append(parts, fmt.Sprintf("Use `%s help` to see available commands in this room.", commandPrefix)) - } - - if connected { - connected := strings.TrimSpace(texts.WelcomeConnected) - if connected == "" { - connected = "You're connected. Use `help` to see the commands available right now." - } - parts = append(parts, replacer.Replace(connected)) - } else { - unconnected := strings.TrimSpace(texts.WelcomeUnconnected) - if unconnected == "" { - unconnected = "Use `help` to see the commands available right now." - } - parts = append(parts, replacer.Replace(unconnected)) - } - - if extra := strings.TrimSpace(texts.AdditionalHelp); extra != "" { - parts = append(parts, replacer.Replace(extra)) - } - - return strings.Join(parts, "\n\n") -} - -func IsHandledInviteEvent(evt *event.Event) bool { - return evt.Type.Type == handledInviteEventType -} - -func IsBotInviteFor(roomBot id.UserID, evt *event.Event) bool { - return evt.Type == event.StateMember && - evt.GetStateKey() == roomBot.String() && - evt.Content.AsMember().Membership == event.MembershipInvite -} diff --git a/packages/arrtrix/pkg/onboarding/welcome_test.go b/packages/arrtrix/pkg/onboarding/welcome_test.go deleted file mode 100644 index de6f42a..0000000 --- a/packages/arrtrix/pkg/onboarding/welcome_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package onboarding - -import ( - "strings" - "testing" - - "maunium.net/go/mautrix/bridgev2/bridgeconfig" - "maunium.net/go/mautrix/id" -) - -func TestComposeWelcomeMessageDefaults(t *testing.T) { - out := composeWelcomeMessage( - "Arrtrix", - "!arr", - id.UserID("@arrtrixbot:test"), - bridgeconfig.ManagementRoomTexts{}, - false, - true, - ) - - for _, fragment := range []string{ - "Hello, I'm the Arrtrix bot.", - "This room has been marked as your management room.", - "Use `help` to see the commands available right now.", - } { - if !strings.Contains(out, fragment) { - t.Fatalf("expected welcome output to contain %q, got:\n%s", fragment, out) - } - } -} - -func TestComposeWelcomeMessageTemplateValues(t *testing.T) { - out := composeWelcomeMessage( - "Arrtrix", - "!arr", - id.UserID("@arrtrixbot:test"), - bridgeconfig.ManagementRoomTexts{ - Welcome: "Welcome to $bridge.", - WelcomeConnected: "Talk to $bot with $cmdprefix help.", - AdditionalHelp: "Custom footer for $bridge.", - }, - true, - false, - ) - - for _, fragment := range []string{ - "Welcome to Arrtrix.", - "Use `!arr help` to see available commands in this room.", - "Talk to @arrtrixbot:test with !arr help.", - "Custom footer for Arrtrix.", - } { - if !strings.Contains(out, fragment) { - t.Fatalf("expected templated welcome output to contain %q, got:\n%s", fragment, out) - } - } -} diff --git a/packages/arrtrix/pkg/runtime/envconfig.go b/packages/arrtrix/pkg/runtime/envconfig.go deleted file mode 100644 index 173de78..0000000 --- a/packages/arrtrix/pkg/runtime/envconfig.go +++ /dev/null @@ -1,256 +0,0 @@ -package runtime - -import ( - "fmt" - "os" - "reflect" - "strconv" - "strings" -) - -const fileEnvPrefix = "READFILE:" - -func updateConfigFromEnv(cfg, networkData any, prefix string) error { - if prefix == "" { - return nil - } - - cfgVal := reflect.ValueOf(cfg) - networkVal := reflect.ValueOf(networkData) - - for _, env := range os.Environ() { - if !strings.HasPrefix(env, prefix) { - continue - } - - keyValue := strings.SplitN(env, "=", 2) - if len(keyValue) != 2 { - continue - } - - key := strings.TrimPrefix(keyValue[0], prefix) - value := keyValue[1] - if strings.HasSuffix(key, "_FILE") { - key = strings.TrimSuffix(key, "_FILE") - value = fileEnvPrefix + value - } - - key = strings.ToLower(key) - lookupKey := key - if !strings.ContainsRune(key, '.') { - key = strings.ReplaceAll(key, "__", ".") - } - - path := strings.Split(key, ".") - field, ok := reflectGetFromMainOrNetwork(cfgVal, networkVal, path) - if !ok && !strings.ContainsRune(lookupKey, '.') { - field, ok = reflectGetFromMainOrNetworkTokens(cfgVal, networkVal, strings.Split(lookupKey, "_")) - } - if !ok { - return fmt.Errorf("%s not found", formatKey(path)) - } - - if strings.HasPrefix(value, fileEnvPrefix) { - filePath := strings.TrimPrefix(value, fileEnvPrefix) - fileData, err := os.ReadFile(filePath) - if err != nil { - return fmt.Errorf("failed to read file %s for %s: %w", filePath, formatKey(path), err) - } - value = strings.TrimSpace(string(fileData)) - } - - if err := setReflectedValue(field, path, value); err != nil { - return err - } - } - - return nil -} - -type reflectedField struct { - value reflect.Value - valueKind reflect.Kind - remainingPath []string -} - -func formatKey(path []string) string { - return strings.Join(path, "->") -} - -func reflectGetFromMainOrNetwork(main, network reflect.Value, path []string) (*reflectedField, bool) { - if len(path) > 0 && path[0] == "network" { - return reflectGetYAML(network, path[1:]) - } - return reflectGetYAML(main, path) -} - -func reflectGetFromMainOrNetworkTokens(main, network reflect.Value, tokens []string) (*reflectedField, bool) { - if len(tokens) > 0 && normalizeKey(tokens[0]) == "network" { - return reflectGetYAMLTokens(network, tokens[1:]) - } - return reflectGetYAMLTokens(main, tokens) -} - -func reflectGetYAML(value reflect.Value, path []string) (*reflectedField, bool) { - if len(path) == 0 { - return &reflectedField{value: value, valueKind: value.Kind()}, true - } - if value.Kind() == reflect.Ptr { - value = value.Elem() - } - - switch value.Kind() { - case reflect.Map: - return &reflectedField{ - value: value, - valueKind: value.Type().Elem().Kind(), - remainingPath: path, - }, true - case reflect.Struct: - fields := reflect.VisibleFields(value.Type()) - for _, field := range fields { - if yamlFieldName(field) != path[0] { - continue - } - return reflectGetYAML(value.FieldByIndex(field.Index), path[1:]) - } - } - - return nil, false -} - -func reflectGetYAMLTokens(value reflect.Value, tokens []string) (*reflectedField, bool) { - if len(tokens) == 0 { - return &reflectedField{value: value, valueKind: value.Kind()}, true - } - if value.Kind() == reflect.Ptr { - value = value.Elem() - } - - switch value.Kind() { - case reflect.Map: - return &reflectedField{ - value: value, - valueKind: value.Type().Elem().Kind(), - remainingPath: []string{strings.Join(tokens, "_")}, - }, true - case reflect.Struct: - fields := reflect.VisibleFields(value.Type()) - for _, field := range fields { - name := yamlFieldName(field) - if name == "" { - continue - } - normalizedFieldName := normalizeKey(name) - for i := len(tokens); i >= 1; i-- { - if normalizeKey(strings.Join(tokens[:i], "_")) != normalizedFieldName { - continue - } - return reflectGetYAMLTokens(value.FieldByIndex(field.Index), tokens[i:]) - } - } - } - - return nil, false -} - -func yamlFieldName(field reflect.StructField) string { - parts := strings.SplitN(field.Tag.Get("yaml"), ",", 2) - switch name := parts[0]; { - case name == "-" && len(parts) == 1: - return "" - case name == "": - return strings.ToLower(field.Name) - default: - return name - } -} - -func normalizeKey(value string) string { - return strings.ReplaceAll(strings.ToLower(value), "_", "") -} - -func setReflectedValue(field *reflectedField, path []string, raw string) error { - parsed, err := parseValue(field.valueKind, raw, path) - if err != nil { - return err - } - - value := field.value - if value.Kind() == reflect.Ptr { - if value.IsNil() { - value.Set(reflect.New(value.Type().Elem())) - } - value = value.Elem() - } - - if value.Kind() == reflect.Map { - if value.Type().Key().Kind() != reflect.String { - return fmt.Errorf("unsupported map key type %s in %s", value.Type().Key().Kind(), formatKey(path)) - } - key := strings.Join(field.remainingPath, ".") - value.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(parsed)) - return nil - } - - value.Set(reflect.ValueOf(parsed)) - return nil -} - -func parseValue(kind reflect.Kind, raw string, path []string) (any, error) { - switch kind { - case reflect.String: - return raw, nil - case reflect.Bool: - parsed, err := strconv.ParseBool(raw) - if err != nil { - return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err) - } - return parsed, nil - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - parsed, err := strconv.ParseInt(raw, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err) - } - switch kind { - case reflect.Int8: - return int8(parsed), nil - case reflect.Int16: - return int16(parsed), nil - case reflect.Int32: - return int32(parsed), nil - case reflect.Int64: - return parsed, nil - default: - return int(parsed), nil - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - parsed, err := strconv.ParseUint(raw, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err) - } - switch kind { - case reflect.Uint8: - return uint8(parsed), nil - case reflect.Uint16: - return uint16(parsed), nil - case reflect.Uint32: - return uint32(parsed), nil - case reflect.Uint64: - return parsed, nil - default: - return uint(parsed), nil - } - case reflect.Float32, reflect.Float64: - parsed, err := strconv.ParseFloat(raw, 64) - if err != nil { - return nil, fmt.Errorf("invalid value for %s: %w", formatKey(path), err) - } - if kind == reflect.Float32 { - return float32(parsed), nil - } - return parsed, nil - default: - return nil, fmt.Errorf("unsupported type %s in %s", kind, formatKey(path)) - } -} diff --git a/packages/arrtrix/pkg/runtime/envconfig_test.go b/packages/arrtrix/pkg/runtime/envconfig_test.go deleted file mode 100644 index 6381a47..0000000 --- a/packages/arrtrix/pkg/runtime/envconfig_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package runtime - -import ( - "os" - "testing" - - "maunium.net/go/mautrix/bridgev2/bridgeconfig" - - "sneeuwvlok/packages/arrtrix/pkg/connector" -) - -func TestUpdateConfigFromEnvSupportsFlatUnderscorePaths(t *testing.T) { - t.Setenv("ARRTRIX_NETWORK_CONTENT_MOVIES_APIKEY", "radarr-secret") - - cfg := &bridgeconfig.Config{} - network := &connector.Config{} - if err := updateConfigFromEnv(cfg, network, "ARRTRIX_"); err != nil { - t.Fatalf("updateConfigFromEnv returned error: %v", err) - } - - if network.Content.Movies.APIKey != "radarr-secret" { - t.Fatalf("expected movies api key to be overridden, got %q", network.Content.Movies.APIKey) - } -} - -func TestUpdateConfigFromEnvSupportsExplicitUnderscoredFieldNames(t *testing.T) { - t.Setenv("ARRTRIX_NETWORK_CONTENT_MOVIES_ROOT_FOLDER_PATH", "/data/movies") - - cfg := &bridgeconfig.Config{} - network := &connector.Config{} - if err := updateConfigFromEnv(cfg, network, "ARRTRIX_"); err != nil { - t.Fatalf("updateConfigFromEnv returned error: %v", err) - } - - if network.Content.Movies.RootFolderPath != "/data/movies" { - t.Fatalf("expected root folder path to be overridden, got %q", network.Content.Movies.RootFolderPath) - } -} - -func TestUpdateConfigFromEnvSupportsDoubleUnderscorePaths(t *testing.T) { - t.Setenv("ARRTRIX_NETWORK__CONTENT__SERIES__API_KEY", "sonarr-secret") - - cfg := &bridgeconfig.Config{} - network := &connector.Config{} - if err := updateConfigFromEnv(cfg, network, "ARRTRIX_"); err != nil { - t.Fatalf("updateConfigFromEnv returned error: %v", err) - } - - if network.Content.Series.APIKey != "sonarr-secret" { - t.Fatalf("expected series api key to be overridden, got %q", network.Content.Series.APIKey) - } -} - -func TestMain(m *testing.M) { - code := m.Run() - os.Exit(code) -} diff --git a/packages/arrtrix/pkg/runtime/example.go b/packages/arrtrix/pkg/runtime/example.go deleted file mode 100644 index c8d7ca4..0000000 --- a/packages/arrtrix/pkg/runtime/example.go +++ /dev/null @@ -1,76 +0,0 @@ -package runtime - -import ( - "fmt" - "strings" - - "maunium.net/go/mautrix/bridgev2" -) - -func makeExampleConfig(networkName bridgev2.BridgeName, networkExample string) string { - var builder strings.Builder - - builder.WriteString("# Network-specific config options\n") - builder.WriteString("network:\n") - for _, line := range strings.Split(strings.TrimRight(networkExample, "\n"), "\n") { - if line == "" { - builder.WriteString(" \n") - continue - } - builder.WriteString(" ") - builder.WriteString(line) - builder.WriteByte('\n') - } - builder.WriteByte('\n') - - builder.WriteString(fmt.Sprintf(`bridge: - command_prefix: "%s" - permissions: - "*": relay - "@admin:example.com": admin - -database: - type: sqlite3-fk-wal - uri: file:arrtrix.db?_txlock=immediate - -homeserver: - address: http://example.localhost:8008 - domain: example.com - software: standard - -appservice: - address: http://localhost:%d - hostname: 127.0.0.1 - port: %d - id: %s - bot: - username: %s - displayname: %s - as_token: This value is generated when generating the registration - hs_token: This value is generated when generating the registration - username_template: %s_{{.}} - -logging: - min_level: info - writers: - - type: stdout - format: pretty-colored - -observability: - # OTLP/gRPC endpoint for logs, traces, and metrics. - # Set to e.g. http://127.0.0.1:4317 to enable export. - otlp_grpc_endpoint: "" - service_name: arrtrix - resource_attributes: {} - -management_room_texts: - welcome: "" - welcome_connected: "" - welcome_unconnected: "" - additional_help: "" - -env_config_prefix: "" -`, networkName.DefaultCommandPrefix, networkName.DefaultPort, networkName.DefaultPort, networkName.NetworkID, "arrtrixbot", "Arrtrix Bot", networkName.NetworkID)) - - return builder.String() -} diff --git a/packages/arrtrix/pkg/runtime/main.go b/packages/arrtrix/pkg/runtime/main.go deleted file mode 100644 index c685706..0000000 --- a/packages/arrtrix/pkg/runtime/main.go +++ /dev/null @@ -1,486 +0,0 @@ -package runtime - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "os" - "os/signal" - "runtime" - "strings" - "syscall" - "time" - - "github.com/rs/zerolog" - "go.mau.fi/util/dbutil" - "go.mau.fi/util/exerrors" - "go.mau.fi/util/exzerolog" - "go.mau.fi/util/progver" - "go.opentelemetry.io/otel/codes" - "gopkg.in/yaml.v3" - flag "maunium.net/go/mauflag" - "maunium.net/go/mautrix/appservice" - - "maunium.net/go/mautrix" - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/bridgeconfig" - "maunium.net/go/mautrix/bridgev2/commands" - "maunium.net/go/mautrix/bridgev2/matrix" - "maunium.net/go/mautrix/event" - - arrconfig "sneeuwvlok/packages/arrtrix/pkg/config" - "sneeuwvlok/packages/arrtrix/pkg/matrixcmd" - "sneeuwvlok/packages/arrtrix/pkg/observability" - "sneeuwvlok/packages/arrtrix/pkg/onboarding" - "sneeuwvlok/packages/arrtrix/pkg/subscriptions" -) - -var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String() -var writeExampleConfig = flag.MakeFull("e", "generate-example-config", "Save the example config to the config path and quit.", "false").Bool() -var dontSaveConfig = flag.MakeFull("n", "no-update", "Don't save updated config to disk.", "false").Bool() -var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String() -var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool() -var version = flag.MakeFull("v", "version", "View bridge version and quit.", "false").Bool() -var versionJSON = flag.Make().LongKey("version-json").Usage("Print a JSON object representing the bridge version and quit.").Default("false").Bool() -var ignoreUnsupportedDatabase = flag.Make().LongKey("ignore-unsupported-database").Usage("Run even if the database schema is too new").Default("false").Bool() -var ignoreForeignTables = flag.Make().LongKey("ignore-foreign-tables").Usage("Run even if the database contains tables from other programs (like Synapse)").Default("false").Bool() -var ignoreUnsupportedServer = flag.Make().LongKey("ignore-unsupported-server").Usage("Run even if the Matrix homeserver is outdated").Default("false").Bool() -var wantHelp, _ = flag.MakeHelpFlag() - -type Main struct { - Name string - Description string - URL string - Version string - - Connector bridgev2.NetworkConnector - PostInit func() - PostStart func() - - Log *zerolog.Logger - DB *dbutil.Database - PublicConfig *arrconfig.Config - Config *bridgeconfig.Config - Matrix *matrix.Connector - Bridge *bridgev2.Bridge - OTEL *observability.Runtime - - ConfigPath string - RegistrationPath string - SaveConfig bool - - ver progver.ProgramVersion - manualStop chan int -} - -type versionJSONOutput struct { - progver.ProgramVersion - - OS string - Arch string - - Mautrix struct { - Version string - Commit string - } -} - -type routeMounter interface { - MountRoutes(*http.ServeMux) error -} - -func (m *Main) Run() { - m.PreInit() - m.Init() - m.Start() - exitCode := m.WaitForInterrupt() - m.Stop() - os.Exit(exitCode) -} - -func (m *Main) PreInit() { - m.manualStop = make(chan int, 1) - flag.SetHelpTitles( - fmt.Sprintf("%s - %s", m.Name, m.Description), - fmt.Sprintf("%s [-hgvn] [-c ] [-r ]", m.Name), - ) - - err := flag.Parse() - m.ConfigPath = *configPath - m.RegistrationPath = *registrationPath - m.SaveConfig = !*dontSaveConfig - if err != nil { - _, _ = fmt.Fprintln(os.Stderr, err) - flag.PrintHelp() - os.Exit(1) - } - - switch { - case *wantHelp: - flag.PrintHelp() - os.Exit(0) - case *version: - fmt.Println(m.ver.VersionDescription) - os.Exit(0) - case *versionJSON: - output := versionJSONOutput{ - ProgramVersion: m.ver, - OS: runtime.GOOS, - Arch: runtime.GOARCH, - } - output.Mautrix.Version = mautrix.Version - output.Mautrix.Commit = mautrix.Commit - _ = json.NewEncoder(os.Stdout).Encode(output) - os.Exit(0) - case *writeExampleConfig: - m.writeExampleConfig() - os.Exit(0) - } - - m.LoadConfig() - if *generateRegistration { - m.GenerateRegistration() - os.Exit(0) - } -} - -func (m *Main) writeExampleConfig() { - if *configPath != "-" { - if _, err := os.Stat(*configPath); !errors.Is(err, os.ErrNotExist) { - _, _ = fmt.Fprintln(os.Stderr, *configPath, "already exists, please remove it if you want to generate a new example") - os.Exit(1) - } - } - - networkExample, _, _ := m.Connector.GetConfig() - example := makeExampleConfig(m.Connector.GetName(), networkExample) - if *configPath == "-" { - fmt.Print(example) - return - } - - exerrors.PanicIfNotNil(os.WriteFile(*configPath, []byte(example), 0o600)) - fmt.Println("Wrote example config to", *configPath) -} - -func (m *Main) GenerateRegistration() { - if !m.SaveConfig { - _, _ = fmt.Fprintln(os.Stderr, "--no-update is not compatible with --generate-registration") - os.Exit(5) - } - if m.Config.Homeserver.Domain == "example.com" { - _, _ = fmt.Fprintln(os.Stderr, "Homeserver domain is not set") - os.Exit(20) - } - - registration := m.Config.GenerateRegistration() - if err := registration.Save(m.RegistrationPath); err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to save registration:", err) - os.Exit(21) - } - - if err := m.saveConfig(); err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to save config:", err) - os.Exit(22) - } - - fmt.Println("Registration generated. See https://docs.mau.fi/bridges/general/registering-appservices.html for instructions on installing the registration.") -} - -func (m *Main) LoadConfig() { - configData, err := os.ReadFile(m.ConfigPath) - if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to read config:", err) - os.Exit(10) - } - - publicConfig, err := arrconfig.Load(configData) - if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to parse config:", err) - os.Exit(10) - } - cfg := publicConfig.Compile() - if err = m.loadRegistrationTokens(&cfg); err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to parse registration:", err) - os.Exit(10) - } - - _, networkData, _ := m.Connector.GetConfig() - if networkData != nil { - if err = cfg.Network.Decode(networkData); err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to parse network config:", err) - os.Exit(10) - } - } - - cfg.Bridge.Backfill = cfg.Backfill - if err = updateConfigFromEnv(&cfg, networkData, cfg.EnvConfigPrefix); err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to parse environment variables:", err) - os.Exit(10) - } - - m.PublicConfig = publicConfig - m.Config = &cfg -} - -func (m *Main) loadRegistrationTokens(cfg *bridgeconfig.Config) error { - if m.RegistrationPath == "" { - return nil - } - - data, err := os.ReadFile(m.RegistrationPath) - if errors.Is(err, os.ErrNotExist) { - return nil - } else if err != nil { - return err - } - - var tokens struct { - AppToken string `yaml:"as_token"` - ServerToken string `yaml:"hs_token"` - } - if err = yaml.Unmarshal(data, &tokens); err != nil { - return err - } - - if tokens.AppToken != "" { - cfg.AppService.ASToken = tokens.AppToken - } - if tokens.ServerToken != "" { - cfg.AppService.HSToken = tokens.ServerToken - } - return nil -} - -func (m *Main) Init() { - start := time.Now() - ctx := context.Background() - var err error - m.Log, err = m.Config.Logging.Compile() - if err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to initialize logger:", err) - os.Exit(12) - } - exzerolog.SetupDefaults(m.Log) - - if err = m.validateConfig(); err != nil { - m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Configuration error") - m.Log.Info().Msg("See https://docs.mau.fi/faq/field-unconfigured for more info") - os.Exit(11) - } - - otelCtx, cancel := context.WithTimeout(ctx, 10*time.Second) - m.OTEL, err = observability.Setup(otelCtx, m.PublicConfig.Observability, m.Version) - cancel() - if err != nil { - m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to initialize observability") - os.Exit(15) - } - if hook := m.OTEL.LoggerHook(); hook != nil { - logger := m.Log.Hook(hook) - m.Log = &logger - exzerolog.SetupDefaults(m.Log) - } - - ctx = m.Log.WithContext(context.Background()) - ctx, span := observability.StartSpan(ctx, "arrtrix.runtime.init") - defer func() { - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - observability.RecordStartupPhase(ctx, "init", "error", time.Since(start)) - return - } - span.SetStatus(codes.Ok, "") - observability.RecordStartupPhase(ctx, "init", "ok", time.Since(start)) - }() - defer span.End() - - m.Log.Info(). - Str("name", m.Name). - Str("version", m.ver.FormattedVersion). - Time("built_at", m.ver.BuildTime). - Str("go_version", runtime.Version()). - Msg("Initializing bridge") - - m.initDB() - if err = subscriptions.EnsureSchema(ctx, m.DB); err != nil { - m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to initialize subscription schema") - os.Exit(14) - } - m.Matrix = matrix.NewConnector(m.Config) - m.Matrix.OnWebsocketReplaced = func() { - m.TriggerStop(0) - } - m.Matrix.IgnoreUnsupportedServer = *ignoreUnsupportedServer - m.Bridge = bridgev2.NewBridge("", m.DB, *m.Log, &m.Config.Bridge, m.Matrix, m.Connector, commands.NewProcessor) - m.Bridge.Commands = matrixcmd.NewProcessor(m.Bridge, m.Config.ManagementRoomTexts) - - if m.Matrix.EventProcessor != nil { - if m.Config.AppService.AsyncTransactions { - m.Matrix.EventProcessor.ExecMode = appservice.AsyncLoop - } else { - m.Matrix.EventProcessor.ExecMode = appservice.Sync - } - m.Matrix.EventProcessor.PrependHandler(event.StateMember, func(ctx context.Context, evt *event.Event) { - onboarding.HandleBotInvite(ctx, m.Bridge, m.Config.ManagementRoomTexts, evt) - }) - } - - m.Matrix.AS.DoublePuppetValue = m.Name - if mounter, ok := m.Connector.(routeMounter); ok { - if err = mounter.MountRoutes(m.Matrix.AS.Router); err != nil { - _, _ = fmt.Fprintln(os.Stderr, "Failed to mount HTTP routes:", err) - os.Exit(13) - } - } - - if m.PostInit != nil { - m.PostInit() - } -} - -func (m *Main) Start() { - start := time.Now() - ctx := m.Log.WithContext(context.Background()) - ctx, span := observability.StartSpan(ctx, "arrtrix.runtime.start") - defer func() { - if r := recover(); r != nil { - span.SetStatus(codes.Error, "panic") - observability.RecordStartupPhase(ctx, "start", "panic", time.Since(start)) - span.End() - panic(r) - } - span.End() - }() - if err := m.Bridge.Start(ctx); err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - observability.RecordStartupPhase(ctx, "start", "error", time.Since(start)) - m.Log.Fatal().Err(err).Msg("Failed to start bridge") - } - span.SetStatus(codes.Ok, "") - observability.RecordStartupPhase(ctx, "start", "ok", time.Since(start)) - if m.PostStart != nil { - m.PostStart() - } -} - -func (m *Main) Stop() { - start := time.Now() - ctx := m.Log.WithContext(context.Background()) - ctx, span := observability.StartSpan(ctx, "arrtrix.runtime.stop") - defer span.End() - - m.Bridge.StopWithTimeout(5 * time.Second) - span.SetStatus(codes.Ok, "") - observability.RecordStartupPhase(ctx, "stop", "ok", time.Since(start)) - - if m.OTEL != nil { - shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - if err := m.OTEL.Shutdown(shutdownCtx); err != nil && m.Log != nil { - m.Log.Error().Err(err).Msg("Failed to shut down observability") - } - } -} - -func (m *Main) WaitForInterrupt() int { - interrupts := make(chan os.Signal, 1) - signal.Notify(interrupts, os.Interrupt, syscall.SIGTERM) - select { - case <-interrupts: - m.Log.Info().Msg("Interrupt signal received from OS") - return 0 - case exitCode := <-m.manualStop: - m.Log.Info().Msg("Internal stop signal received") - return exitCode - } -} - -func (m *Main) TriggerStop(exitCode int) { - select { - case m.manualStop <- exitCode: - default: - } -} - -func (m *Main) InitVersion(tag, commit, rawBuildTime string) { - m.ver = progver.ProgramVersion{ - Name: m.Name, - URL: m.URL, - BaseVersion: m.Version, - }.Init(tag, commit, rawBuildTime) - mautrix.DefaultUserAgent = fmt.Sprintf("%s/%s %s", m.Name, m.ver.FormattedVersion, mautrix.DefaultUserAgent) - m.Version = m.ver.FormattedVersion -} - -func (m *Main) validateConfig() error { - switch { - case m.Config.Homeserver.Address == "http://example.localhost:8008": - return errors.New("homeserver.address not configured") - case m.Config.Homeserver.Domain == "example.com": - return errors.New("homeserver.domain not configured") - case !bridgeconfig.AllowedHomeserverSoftware[m.Config.Homeserver.Software]: - return errors.New("invalid value for homeserver.software (use `standard` if you don't know what the field is for)") - case m.Config.AppService.ASToken == "This value is generated when generating the registration": - return errors.New("appservice.as_token not configured. Did you forget to generate the registration?") - case m.Config.AppService.HSToken == "This value is generated when generating the registration": - return errors.New("appservice.hs_token not configured. Did you forget to generate the registration?") - case m.Config.Database.URI == "postgres://user:password@host/database?sslmode=disable": - return errors.New("database.uri not configured") - case !m.Config.Bridge.Permissions.IsConfigured(): - return errors.New("bridge.permissions not configured") - case !strings.Contains(m.Config.AppService.FormatUsername("1234567890"), "1234567890"): - return errors.New("username template is missing user ID placeholder") - default: - if validator, ok := m.Connector.(bridgev2.ConfigValidatingNetwork); ok { - return validator.ValidateConfig() - } - return nil - } -} - -func (m *Main) initDB() { - if m.Config.Database.Type == "sqlite3" { - m.Log.WithLevel(zerolog.FatalLevel).Msg("Invalid database type sqlite3. Use sqlite3-fk-wal instead.") - os.Exit(14) - } - if (m.Config.Database.Type == "sqlite3-fk-wal" || m.Config.Database.Type == "litestream") && - m.Config.Database.MaxOpenConns != 1 && - !strings.Contains(m.Config.Database.URI, "_txlock=immediate") { - var fixedURI string - switch { - case !strings.HasPrefix(m.Config.Database.URI, "file:"): - fixedURI = fmt.Sprintf("file:%s?_txlock=immediate", m.Config.Database.URI) - case !strings.ContainsRune(m.Config.Database.URI, '?'): - fixedURI = fmt.Sprintf("%s?_txlock=immediate", m.Config.Database.URI) - default: - fixedURI = fmt.Sprintf("%s&_txlock=immediate", m.Config.Database.URI) - } - m.Log.Warn().Str("fixed_uri_example", fixedURI).Msg("Using SQLite without _txlock=immediate is not recommended") - } - - var err error - m.DB, err = dbutil.NewFromConfig("megabridge/"+m.Name, m.Config.Database, dbutil.ZeroLogger(m.Log.With().Str("db_section", "main").Logger())) - if err != nil { - m.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to initialize database connection") - os.Exit(14) - } - m.DB.IgnoreUnsupportedDatabase = *ignoreUnsupportedDatabase - m.DB.IgnoreForeignTables = *ignoreForeignTables -} - -func (m *Main) saveConfig() error { - publicConfig := *m.PublicConfig - publicConfig.AppService.ASToken = m.Config.AppService.ASToken - publicConfig.AppService.HSToken = m.Config.AppService.HSToken - - configData, err := yaml.Marshal(&publicConfig) - if err != nil { - return err - } - return os.WriteFile(m.ConfigPath, configData, 0o600) -} diff --git a/packages/arrtrix/pkg/runtime/main_test.go b/packages/arrtrix/pkg/runtime/main_test.go deleted file mode 100644 index f54201b..0000000 --- a/packages/arrtrix/pkg/runtime/main_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package runtime - -import ( - "os" - "path/filepath" - "testing" - - "maunium.net/go/mautrix/bridgev2/bridgeconfig" -) - -func TestLoadRegistrationTokens(t *testing.T) { - tempDir := t.TempDir() - registrationPath := filepath.Join(tempDir, "registration.yaml") - if err := os.WriteFile(registrationPath, []byte("as_token: app-token\nhs_token: hs-token\n"), 0o600); err != nil { - t.Fatalf("failed to write registration file: %v", err) - } - - cfg := &bridgeconfig.Config{} - main := &Main{RegistrationPath: registrationPath} - if err := main.loadRegistrationTokens(cfg); err != nil { - t.Fatalf("loadRegistrationTokens returned error: %v", err) - } - - if cfg.AppService.ASToken != "app-token" { - t.Fatalf("expected as token to be loaded, got %q", cfg.AppService.ASToken) - } - if cfg.AppService.HSToken != "hs-token" { - t.Fatalf("expected hs token to be loaded, got %q", cfg.AppService.HSToken) - } -} diff --git a/packages/arrtrix/pkg/subscriptions/repo.go b/packages/arrtrix/pkg/subscriptions/repo.go deleted file mode 100644 index 85c6b57..0000000 --- a/packages/arrtrix/pkg/subscriptions/repo.go +++ /dev/null @@ -1,141 +0,0 @@ -package subscriptions - -import ( - "context" - "fmt" - - "go.mau.fi/util/dbutil" - "maunium.net/go/mautrix/id" - - "sneeuwvlok/packages/arrtrix/pkg/arr" -) - -type Preference struct { - ContentType arr.ContentType - EventType string - Enabled bool -} - -type Repository struct { - db *dbutil.Database - bridgeID string -} - -func EnsureSchema(ctx context.Context, db *dbutil.Database) error { - _, err := db.Exec(ctx, ` - CREATE TABLE IF NOT EXISTS arrtrix_subscription ( - bridge_id TEXT NOT NULL, - user_mxid TEXT NOT NULL, - content_type TEXT NOT NULL, - event_type TEXT NOT NULL, - enabled BOOLEAN NOT NULL, - PRIMARY KEY (bridge_id, user_mxid, content_type, event_type) - ) - `) - return err -} - -func NewRepository(db *dbutil.Database, bridgeID string) *Repository { - return &Repository{db: db, bridgeID: bridgeID} -} - -func (r *Repository) EnsureDefaults(ctx context.Context, userID id.UserID) error { - var existing int - if err := r.db.QueryRow(ctx, `SELECT COUNT(*) FROM arrtrix_subscription WHERE bridge_id=$1 AND user_mxid=$2`, r.bridgeID, userID.String()).Scan(&existing); err != nil { - return err - } - if existing > 0 { - return nil - } - - for _, contentType := range arr.SupportedContentTypes() { - for _, eventType := range arr.SupportedEventTypes(contentType) { - if _, err := r.db.Exec(ctx, ` - INSERT INTO arrtrix_subscription (bridge_id, user_mxid, content_type, event_type, enabled) - VALUES ($1, $2, $3, $4, TRUE) - `, r.bridgeID, userID.String(), string(contentType), eventType); err != nil { - return err - } - } - } - return nil -} - -func (r *Repository) List(ctx context.Context, userID id.UserID) ([]Preference, error) { - if err := r.EnsureDefaults(ctx, userID); err != nil { - return nil, err - } - - rows, err := r.db.Query(ctx, ` - SELECT content_type, event_type, enabled - FROM arrtrix_subscription - WHERE bridge_id=$1 AND user_mxid=$2 - ORDER BY content_type, event_type - `, r.bridgeID, userID.String()) - if err != nil { - return nil, err - } - defer rows.Close() - - var preferences []Preference - for rows.Next() { - var contentType string - var preference Preference - if err = rows.Scan(&contentType, &preference.EventType, &preference.Enabled); err != nil { - return nil, err - } - preference.ContentType = arr.ContentType(contentType) - preferences = append(preferences, preference) - } - if err = rows.Err(); err != nil { - return nil, err - } - return preferences, nil -} - -func (r *Repository) Set(ctx context.Context, userID id.UserID, contentType arr.ContentType, eventType string, enabled bool) error { - if err := r.EnsureDefaults(ctx, userID); err != nil { - return err - } - if _, err := r.db.Exec(ctx, ` - INSERT INTO arrtrix_subscription (bridge_id, user_mxid, content_type, event_type, enabled) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (bridge_id, user_mxid, content_type, event_type) - DO UPDATE SET enabled=excluded.enabled - `, r.bridgeID, userID.String(), string(contentType), eventType, enabled); err != nil { - return err - } - return nil -} - -func (r *Repository) SetAll(ctx context.Context, userID id.UserID, contentType arr.ContentType, enabled bool) error { - if err := r.EnsureDefaults(ctx, userID); err != nil { - return err - } - for _, eventType := range arr.SupportedEventTypes(contentType) { - if err := r.Set(ctx, userID, contentType, eventType, enabled); err != nil { - return err - } - } - return nil -} - -func (r *Repository) Allows(ctx context.Context, userID id.UserID, contentType arr.ContentType, eventType string) (bool, error) { - if !arr.SupportsEventType(contentType, eventType) { - return true, nil - } - if err := r.EnsureDefaults(ctx, userID); err != nil { - return false, err - } - - var enabled bool - err := r.db.QueryRow(ctx, ` - SELECT enabled - FROM arrtrix_subscription - WHERE bridge_id=$1 AND user_mxid=$2 AND content_type=$3 AND event_type=$4 - `, r.bridgeID, userID.String(), string(contentType), eventType).Scan(&enabled) - if err != nil { - return false, fmt.Errorf("query subscription: %w", err) - } - return enabled, nil -} diff --git a/packages/arrtrix/pkg/webhook/arr.go b/packages/arrtrix/pkg/webhook/arr.go deleted file mode 100644 index 5446825..0000000 --- a/packages/arrtrix/pkg/webhook/arr.go +++ /dev/null @@ -1,349 +0,0 @@ -package webhook - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/format" - "maunium.net/go/mautrix/id" - - "sneeuwvlok/packages/arrtrix/pkg/arr" - "sneeuwvlok/packages/arrtrix/pkg/observability" -) - -const ArrWebhookPath = "/_arrtrix/webhook" - -var ( - ErrNoManagementRoom = errors.New("no management room configured") - ErrAmbiguousManagementRoom = errors.New("multiple management rooms configured") -) - -type payload struct { - EventType string `json:"eventType"` - Movie *movie `json:"movie"` - MovieFile *movieFile `json:"movieFile"` - Series *series `json:"series"` - Episodes []episode `json:"episodes"` - EpisodeFile *episodeFile `json:"episodeFile"` - IsUpgrade bool `json:"isUpgrade"` -} - -type movie struct { - Title string `json:"title"` - Year int `json:"year"` - ImdbID string `json:"imdbId"` - TmdbID int `json:"tmdbId"` - Path string `json:"path"` -} - -type movieFile struct { - Quality string `json:"quality"` - RelativePath string `json:"relativePath"` - SceneName string `json:"sceneName"` - ReleaseGroup string `json:"releaseGroup"` -} - -type series struct { - Title string `json:"title"` - Year int `json:"year"` - Path string `json:"path"` -} - -type episode struct { - SeasonNumber int `json:"seasonNumber"` - EpisodeNumber int `json:"episodeNumber"` - Title string `json:"title"` -} - -type episodeFile struct { - Quality string `json:"quality"` - RelativePath string `json:"relativePath"` - SceneName string `json:"sceneName"` -} - -type managementTarget struct { - UserID id.UserID - RoomID id.RoomID -} - -type roomResolver interface { - ResolveManagementRoom(context.Context) (managementTarget, error) -} - -type noticeSender interface { - SendNotice(context.Context, id.RoomID, string) error -} - -type SubscriptionFilter interface { - Allows(context.Context, id.UserID, arr.ContentType, string) (bool, error) -} - -type ArrHandler struct { - resolver roomResolver - sender noticeSender - subscriptions SubscriptionFilter -} - -func MountArr(router *http.ServeMux, bridge *bridgev2.Bridge, subscriptions SubscriptionFilter) error { - if bridge == nil { - return fmt.Errorf("bridge is not initialized") - } - handler := &ArrHandler{ - resolver: bridgeRoomResolver{bridge: bridge}, - sender: bridgeNoticeSender{bridge: bridge}, - subscriptions: subscriptions, - } - router.Handle(fmt.Sprintf("POST %s", ArrWebhookPath), handler) - return nil -} - -func (h *ArrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - start := time.Now() - ctx, span := observability.StartSpan(r.Context(), "arrtrix.webhook.handle", trace.WithSpanKind(trace.SpanKindServer)) - defer span.End() - - statusCode := http.StatusAccepted - outcome := "ok" - eventType := "" - defer func() { - observability.RecordWebhook(ctx, eventType, outcome, statusCode, time.Since(start)) - }() - - var body payload - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - statusCode = http.StatusBadRequest - outcome = "invalid_payload" - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - http.Error(w, "invalid webhook payload", http.StatusBadRequest) - return - } - if strings.TrimSpace(body.EventType) == "" { - statusCode = http.StatusBadRequest - outcome = "missing_event_type" - span.SetStatus(codes.Error, "missing eventType") - http.Error(w, "missing eventType", http.StatusBadRequest) - return - } - eventType = body.EventType - span.SetAttributes( - attribute.String("arrtrix.webhook.event_type", body.EventType), - attribute.String("http.method", r.Method), - attribute.String("http.route", ArrWebhookPath), - ) - - target, err := h.resolver.ResolveManagementRoom(ctx) - if err != nil { - statusCode = http.StatusInternalServerError - outcome = "resolve_failed" - if errors.Is(err, ErrNoManagementRoom) || errors.Is(err, ErrAmbiguousManagementRoom) { - statusCode = http.StatusConflict - outcome = "routing_conflict" - } - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - http.Error(w, err.Error(), statusCode) - return - } - - contentType, ok := body.ContentType() - if ok && h.subscriptions != nil { - allowed, filterErr := h.subscriptions.Allows(ctx, target.UserID, contentType, body.EventType) - if filterErr != nil { - statusCode = http.StatusInternalServerError - outcome = "subscription_check_failed" - span.RecordError(filterErr) - span.SetStatus(codes.Error, filterErr.Error()) - http.Error(w, "failed to evaluate subscriptions", statusCode) - return - } - if !allowed { - outcome = "filtered" - span.SetStatus(codes.Ok, "filtered") - w.WriteHeader(statusCode) - return - } - } - - if err = h.sender.SendNotice(ctx, target.RoomID, renderNotice(body)); err != nil { - statusCode = http.StatusBadGateway - outcome = "delivery_failed" - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - http.Error(w, "failed to deliver webhook", http.StatusBadGateway) - return - } - - span.SetStatus(codes.Ok, "") - w.WriteHeader(statusCode) -} - -type bridgeRoomResolver struct { - bridge *bridgev2.Bridge -} - -func (r bridgeRoomResolver) ResolveManagementRoom(ctx context.Context) (managementTarget, error) { - ctx, span := observability.StartSpan(ctx, "arrtrix.webhook.resolve_management_room") - defer span.End() - - rows, err := r.bridge.DB.Query(ctx, `SELECT mxid, management_room FROM "user" WHERE bridge_id=$1 AND management_room IS NOT NULL AND management_room <> ''`, r.bridge.ID) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - return managementTarget{}, fmt.Errorf("failed to query management rooms: %w", err) - } - defer rows.Close() - - var target managementTarget - var owners []id.UserID - for rows.Next() { - var mxid, managementRoom string - if err = rows.Scan(&mxid, &managementRoom); err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - return managementTarget{}, fmt.Errorf("failed to scan management room: %w", err) - } - owners = append(owners, id.UserID(mxid)) - if target.RoomID == "" { - target = managementTarget{ - UserID: id.UserID(mxid), - RoomID: id.RoomID(managementRoom), - } - } - } - if err = rows.Err(); err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - return managementTarget{}, fmt.Errorf("failed to iterate management rooms: %w", err) - } - - switch len(owners) { - case 0: - span.SetStatus(codes.Error, ErrNoManagementRoom.Error()) - return managementTarget{}, ErrNoManagementRoom - case 1: - span.SetAttributes(attribute.Int("arrtrix.management_room.count", 1)) - span.SetStatus(codes.Ok, "") - return target, nil - default: - span.SetAttributes(attribute.Int("arrtrix.management_room.count", len(owners))) - span.SetStatus(codes.Error, ErrAmbiguousManagementRoom.Error()) - return managementTarget{}, fmt.Errorf("%w: %s", ErrAmbiguousManagementRoom, strings.Join(convertUserIDs(owners), ", ")) - } -} - -type bridgeNoticeSender struct { - bridge *bridgev2.Bridge -} - -func (s bridgeNoticeSender) SendNotice(ctx context.Context, roomID id.RoomID, markdown string) error { - ctx, span := observability.StartSpan(ctx, "arrtrix.webhook.send_notice") - defer span.End() - span.SetAttributes(attribute.String("matrix.room_id", roomID.String())) - - if err := s.bridge.Bot.EnsureJoined(ctx, roomID); err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - return err - } - content := format.RenderMarkdown(markdown, true, false) - _, err := s.bridge.Bot.SendMessage(ctx, roomID, event.EventMessage, &event.Content{Parsed: &content}, nil) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - return err - } - span.SetStatus(codes.Ok, "") - return err -} - -func renderNotice(body payload) string { - lines := []string{fmt.Sprintf("**Arr %s**", body.EventType)} - - switch contentType, ok := body.ContentType(); { - case ok && contentType == arr.ContentTypeMovies: - title := body.Movie.Title - if body.Movie.Year != 0 { - title = fmt.Sprintf("%s (%d)", title, body.Movie.Year) - } - lines = append(lines, fmt.Sprintf("Movie: %s", title)) - if body.MovieFile != nil && body.MovieFile.Quality != "" { - lines = append(lines, fmt.Sprintf("Quality: %s", body.MovieFile.Quality)) - } - if body.MovieFile != nil && body.MovieFile.RelativePath != "" { - lines = append(lines, fmt.Sprintf("File: `%s`", body.MovieFile.RelativePath)) - } - if body.EventType == "Download" { - lines = append(lines, fmt.Sprintf("Upgrade: %t", body.IsUpgrade)) - } - if body.Movie.ImdbID != "" { - lines = append(lines, fmt.Sprintf("IMDb: `%s`", body.Movie.ImdbID)) - } - case ok && contentType == arr.ContentTypeSeries: - title := body.Series.Title - if body.Series.Year != 0 { - title = fmt.Sprintf("%s (%d)", title, body.Series.Year) - } - lines = append(lines, fmt.Sprintf("Series: %s", title)) - if len(body.Episodes) > 0 { - lines = append(lines, fmt.Sprintf("Episodes: %s", renderEpisodes(body.Episodes))) - } - if body.EpisodeFile != nil && body.EpisodeFile.Quality != "" { - lines = append(lines, fmt.Sprintf("Quality: %s", body.EpisodeFile.Quality)) - } - if body.EpisodeFile != nil && body.EpisodeFile.RelativePath != "" { - lines = append(lines, fmt.Sprintf("File: `%s`", body.EpisodeFile.RelativePath)) - } - default: - if body.EventType != "Test" { - lines = append(lines, "Payload received.") - } - } - - return strings.Join(lines, "\n") -} - -func convertUserIDs(users []id.UserID) []string { - out := make([]string, len(users)) - for i, user := range users { - out[i] = string(user) - } - return out -} - -var _ roomResolver = bridgeRoomResolver{} -var _ noticeSender = bridgeNoticeSender{} -var _ http.Handler = (*ArrHandler)(nil) - -func (p payload) ContentType() (arr.ContentType, bool) { - switch { - case p.Movie != nil: - return arr.ContentTypeMovies, true - case p.Series != nil: - return arr.ContentTypeSeries, true - default: - return "", false - } -} - -func renderEpisodes(episodes []episode) string { - parts := make([]string, 0, len(episodes)) - for _, item := range episodes { - if item.Title != "" { - parts = append(parts, fmt.Sprintf("S%02dE%02d %s", item.SeasonNumber, item.EpisodeNumber, item.Title)) - continue - } - parts = append(parts, fmt.Sprintf("S%02dE%02d", item.SeasonNumber, item.EpisodeNumber)) - } - return strings.Join(parts, ", ") -} diff --git a/packages/arrtrix/pkg/webhook/arr_test.go b/packages/arrtrix/pkg/webhook/arr_test.go deleted file mode 100644 index e7e89f6..0000000 --- a/packages/arrtrix/pkg/webhook/arr_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package webhook - -import ( - "context" - "errors" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "maunium.net/go/mautrix/id" - - "sneeuwvlok/packages/arrtrix/pkg/arr" -) - -type stubRoomResolver struct { - target managementTarget - err error -} - -func (s stubRoomResolver) ResolveManagementRoom(context.Context) (managementTarget, error) { - return s.target, s.err -} - -type stubNoticeSender struct { - roomID id.RoomID - message string - err error -} - -type stubSubscriptionFilter struct { - allowed bool - err error -} - -func (s *stubNoticeSender) SendNotice(_ context.Context, roomID id.RoomID, message string) error { - s.roomID = roomID - s.message = message - return s.err -} - -func (s stubSubscriptionFilter) Allows(context.Context, id.UserID, arr.ContentType, string) (bool, error) { - return s.allowed, s.err -} - -func TestMountArrRequiresBridge(t *testing.T) { - router := http.NewServeMux() - if err := MountArr(router, nil, nil); err == nil { - t.Fatal("expected nil bridge to fail") - } -} - -func TestArrHandlerDeliversNotice(t *testing.T) { - sender := &stubNoticeSender{} - handler := &ArrHandler{ - resolver: stubRoomResolver{target: managementTarget{UserID: "@user:test", RoomID: "!room:test"}}, - sender: sender, - } - - req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Download","movie":{"title":"Dune","year":2021,"imdbId":"tt1160419"},"movieFile":{"quality":"1080p","relativePath":"Dune (2021)/Dune.mkv"},"isUpgrade":false}`)) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusAccepted { - t.Fatalf("expected accepted status, got %d", rec.Code) - } - if sender.roomID != "!room:test" { - t.Fatalf("expected notice sent to management room, got %q", sender.roomID) - } - if !strings.Contains(sender.message, "**Arr Download**") || !strings.Contains(sender.message, "Dune (2021)") { - t.Fatalf("unexpected message: %s", sender.message) - } -} - -func TestArrHandlerReportsAmbiguousManagementRoom(t *testing.T) { - handler := &ArrHandler{ - resolver: stubRoomResolver{err: ErrAmbiguousManagementRoom}, - sender: &stubNoticeSender{}, - } - - req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusConflict { - t.Fatalf("expected conflict status, got %d", rec.Code) - } -} - -func TestRenderNoticeForTestEvent(t *testing.T) { - msg := renderNotice(payload{EventType: "Test"}) - if strings.TrimSpace(msg) != "**Arr Test**" { - t.Fatalf("unexpected test-event message: %q", msg) - } -} - -func TestArrHandlerReturnsBadGatewayOnSendFailure(t *testing.T) { - handler := &ArrHandler{ - resolver: stubRoomResolver{target: managementTarget{UserID: "@user:test", RoomID: "!room:test"}}, - sender: &stubNoticeSender{err: errors.New("send failed")}, - } - - req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Test"}`)) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusBadGateway { - t.Fatalf("expected bad gateway status, got %d", rec.Code) - } -} - -func TestArrHandlerRejectsMissingEventType(t *testing.T) { - handler := &ArrHandler{ - resolver: stubRoomResolver{target: managementTarget{UserID: "@user:test", RoomID: "!room:test"}}, - sender: &stubNoticeSender{}, - } - - req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"movie":{"title":"Dune"}}`)) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusBadRequest { - t.Fatalf("expected bad request status, got %d", rec.Code) - } -} - -func TestArrHandlerFiltersDisabledSubscriptions(t *testing.T) { - sender := &stubNoticeSender{} - handler := &ArrHandler{ - resolver: stubRoomResolver{target: managementTarget{UserID: "@user:test", RoomID: "!room:test"}}, - sender: sender, - subscriptions: stubSubscriptionFilter{allowed: false}, - } - - req := httptest.NewRequest(http.MethodPost, ArrWebhookPath, strings.NewReader(`{"eventType":"Download","movie":{"title":"Dune","year":2021}}`)) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusAccepted { - t.Fatalf("expected accepted status, got %d", rec.Code) - } - if sender.roomID != "" { - t.Fatalf("expected no notice to be sent, got room %q", sender.roomID) - } -} diff --git a/packages/studio/default.nix b/packages/studio/default.nix index cb628c9..e3061d4 100644 --- a/packages/studio/default.nix +++ b/packages/studio/default.nix @@ -1,109 +1,102 @@ -{ - pkgs, - inputs, -}: let +{ pkgs, inputs }: let inherit (builtins) fetchurl; - inherit (pkgs) makeDesktopItem copyDesktopItems wineWow64Packages; + inherit (pkgs) makeDesktopItem copyDesktopItems wineWowPackages; inherit (inputs.erosanix.lib.x86_64-linux) mkWindowsAppNoCC makeDesktopIcon copyDesktopIcons; - wine = wineWow64Packages.base; -in - mkWindowsAppNoCC rec { - inherit wine; + wine = wineWowPackages.base; +in mkWindowsAppNoCC rec { + inherit wine; pname = "studio"; - version = "2.25.12"; + version = "2.25.4"; - src = fetchurl { - url = "https://studio.download.bricklink.info/Studio2.0+EarlyAccess/Archive/2.25.12_1/Studio+2.0+EarlyAccess.exe"; - sha256 = "sha256:1xl3zvzkzr64zphk7rnpfx3whhbaykzw06m3nd5dc12r2p4sdh3v"; - }; + src = fetchurl { + url = "https://studio.download.bricklink.info/Studio2.0+EarlyAccess/Archive/2.25.4_1/Studio+2.0+EarlyAccess.exe"; + sha256 = "sha256:1gw6pyvfr7zr42g21hqgiwkjs88nvhq2c2v40y21frvwv17hja92"; + }; - enableMonoBootPrompt = false; - dontUnpack = true; + enableMonoBootPrompt = false; + dontUnpack = true; - wineArch = "win64"; - enableInstallNotification = true; + wineArch = "win64"; + enableInstallNotification = true; - fileMap = { - "$HOME/.config/${pname}/Stud.io" = "drive_c/users/$USER/AppData/Local/Stud.io"; - "$HOME/.config/${pname}/Bricklink" = "drive_c/users/$USER/AppData/LocalLow/Bricklink"; - }; + fileMap = { + "$HOME/.config/${pname}/Stud.io" = "drive_c/users/$USER/AppData/Local/Stud.io"; + "$HOME/.config/${pname}/Bricklink" = "drive_c/users/$USER/AppData/LocalLow/Bricklink"; + }; - fileMapDuringAppInstall = false; + fileMapDuringAppInstall = false; - persistRegistry = false; - persistRuntimeLayer = true; - inputHashMethod = "version"; + persistRegistry = false; + persistRuntimeLayer = true; + inputHashMethod = "version"; - # Can be used to precisely select the Direct3D implementation. - # - # | enableVulkan | rendererOverride | Direct3D implementation | - # |--------------|------------------|-------------------------| - # | false | null | OpenGL | - # | true | null | Vulkan (DXVK) | - # | * | dxvk-vulkan | Vulkan (DXVK) | - # | * | wine-opengl | OpenGL | - # | * | wine-vulkan | Vulkan (VKD3D) | - enableVulkan = false; - rendererOverride = null; + # Can be used to precisely select the Direct3D implementation. + # + # | enableVulkan | rendererOverride | Direct3D implementation | + # |--------------|------------------|-------------------------| + # | false | null | OpenGL | + # | true | null | Vulkan (DXVK) | + # | * | dxvk-vulkan | Vulkan (DXVK) | + # | * | wine-opengl | OpenGL | + # | * | wine-vulkan | Vulkan (VKD3D) | + enableVulkan = false; + rendererOverride = null; - enableHUD = false; + enableHUD = false; - enabledWineSymlinks = {}; - graphicsDriver = "auto"; - inhibitIdle = false; + enabledWineSymlinks = { }; + graphicsDriver = "auto"; + inhibitIdle = false; - nativeBuildInputs = [copyDesktopIcons copyDesktopItems]; + nativeBuildInputs = [ copyDesktopIcons copyDesktopItems ]; - winAppInstall = '' - wine64 ${src} + winAppInstall = '' + wine64 ${src} - wineserver -W - wine64 reg add 'HKEY_CURRENT_USER\Software\Wine\X11 Driver' /t REG_SZ /v UseTakeFocus /d N /f - ''; + wineserver -W + wine64 reg add 'HKEY_CURRENT_USER\Software\Wine\X11 Driver' /t REG_SZ /v UseTakeFocus /d N /f + ''; - winAppPreRun = '' - wineserver -W - wine64 reg add 'HKEY_CURRENT_USER\Software\Wine\X11 Driver' /t REG_SZ /v UseTakeFocus /d N /f - ''; + winAppPreRun = ''''; - winAppRun = '' - wine64 "$WINEPREFIX/drive_c/Program Files/Studio 2.0/Studio.exe" "$ARGS" - ''; + winAppRun = '' + wine64 "$WINEPREFIX/drive_c/Program Files/Studio 2.0/Studio.exe" "$ARGS" + ''; - winAppPostRun = ""; - installPhase = '' - runHook preInstall + winAppPostRun = ""; + installPhase = '' + runHook preInstall - ln -s $out/bin/.launcher $out/bin/${pname} + ln -s $out/bin/.launcher $out/bin/${pname} - runHook postInstall - ''; + runHook postInstall + ''; - desktopItems = [ - (makeDesktopItem { - mimeTypes = []; + desktopItems = [ + (makeDesktopItem { + mimeTypes = []; - name = pname; - exec = pname; - icon = pname; - desktopName = "Bricklink studio"; - genericName = "Lego creation app"; - categories = []; - }) - ]; - - desktopIcon = makeDesktopIcon { name = pname; - src = ./studio.png; - }; + exec = pname; + icon = pname; + desktopName = "Bricklink studio"; + genericName = "Lego creation app"; + categories = []; + }) + ]; - meta = { - description = "App for creating lego builds"; - homepage = "https://www.bricklink.com/v3/studio/main.page"; - license = ""; - maintainers = []; - platforms = ["x86_64-linux"]; - }; - } + desktopIcon = makeDesktopIcon { + name = pname; + src = ./studio.png; + }; + + meta = { + description = "App for creating lego builds"; + homepage = "https://www.bricklink.com/v3/studio/main.page"; + license = ""; + maintainers = []; + platforms = [ "x86_64-linux" ]; + }; +} diff --git a/sabnzbd.ini b/sabnzbd.ini deleted file mode 100644 index fd60f57..0000000 --- a/sabnzbd.ini +++ /dev/null @@ -1,395 +0,0 @@ -__version__ = 19 -__encoding__ = utf-8 -[misc] -helpful_warnings = 1 -queue_complete = hibernate_pc -queue_complete_pers = 0 -bandwidth_perc = 100 -refresh_rate = 1 -interface_settings = '{"dateFormat":"YYYY-MM-DD HH:mm","extraQueueColumns":[],"extraHistoryColumns":[],"displayCompact":false,"displayFullWidth":false,"confirmDeleteQueue":true,"confirmDeleteHistory":true,"keyboardShortcuts":true}' -queue_limit = 20 -config_lock = 0 -fixed_ports = 1 -notified_new_skin = 2 -direct_unpack_tested = 1 -sorters_converted = 1 -check_new_rel = 1 -auto_browser = 0 -language = en -enable_https_verification = 0 -host = 0.0.0.0 -port = 8080 -https_port = "" -username = "" -password = "" -bandwidth_max = "" -cache_limit = 1G -web_dir = Glitter -web_color = Auto -https_cert = server.cert -https_key = server.key -https_chain = "" -enable_https = 0 -inet_exposure = 0 -api_key = 0052eba0db9d4b4f93a8a96f0cb85198 -nzb_key = 171ebeb3e0044c379dc7719bef6b3144 -socks5_proxy_url = "" -permissions = "" -download_dir = /var/media/downloads/incomplete -download_free = "" -complete_dir = /var/media/downloads/done -complete_free = "" -fulldisk_autoresume = 0 -script_dir = "" -nzb_backup_dir = "" -admin_dir = admin -backup_dir = "" -dirscan_dir = "" -dirscan_speed = 5 -password_file = "" -log_dir = logs -max_art_tries = 3 -top_only = 0 -sfv_check = 1 -script_can_fail = 0 -enable_recursive = 1 -flat_unpack = 0 -par_option = "" -pre_check = 0 -nice = "" -win_process_prio = 3 -ionice = "" -fail_hopeless_jobs = 1 -fast_fail = 1 -auto_disconnect = 1 -pre_script = None -end_queue_script = None -no_dupes = 0 -no_series_dupes = 0 -no_smart_dupes = 0 -dupes_propercheck = 1 -pause_on_pwrar = 1 -ignore_samples = 0 -deobfuscate_final_filenames = 1 -auto_sort = "" -direct_unpack = 0 -propagation_delay = 0 -folder_rename = 1 -replace_spaces = 0 -replace_underscores = 0 -replace_dots = 0 -safe_postproc = 1 -pause_on_post_processing = 0 -enable_all_par = 0 -sanitize_safe = 0 -cleanup_list = , -unwanted_extensions = , -action_on_unwanted_extensions = 0 -unwanted_extensions_mode = 0 -new_nzb_on_failure = 0 -history_retention = "" -history_retention_option = all -history_retention_number = 1 -quota_size = "" -quota_day = "" -quota_resume = 0 -quota_period = m -enable_tv_sorting = 0 -tv_sort_string = "" -tv_categories = tv, -enable_movie_sorting = 0 -movie_sort_string = "" -movie_sort_extra = -cd%1 -movie_categories = movies, -enable_date_sorting = 0 -date_sort_string = "" -date_categories = tv, -schedlines = , -rss_rate = 60 -ampm = 0 -start_paused = 0 -preserve_paused_state = 0 -enable_par_cleanup = 1 -process_unpacked_par2 = 1 -enable_multipar = 1 -enable_unrar = 1 -enable_7zip = 1 -enable_filejoin = 1 -enable_tsjoin = 1 -overwrite_files = 0 -ignore_unrar_dates = 0 -backup_for_duplicates = 0 -empty_postproc = 0 -wait_for_dfolder = 0 -rss_filenames = 0 -api_logging = 1 -html_login = 1 -warn_dupl_jobs = 0 -keep_awake = 1 -tray_icon = 1 -allow_incomplete_nzb = 0 -enable_broadcast = 1 -ipv6_hosting = 0 -ipv6_staging = 0 -api_warnings = 1 -no_penalties = 0 -x_frame_options = 1 -allow_old_ssl_tls = 0 -enable_season_sorting = 1 -verify_xff_header = 0 -rss_odd_titles = nzbindex.nl/, nzbindex.com/, nzbclub.com/ -quick_check_ext_ignore = nfo, sfv, srr -req_completion_rate = 100.2 -selftest_host = self-test.sabnzbd.org -movie_rename_limit = 100M -episode_rename_limit = 20M -size_limit = 0 -direct_unpack_threads = 3 -history_limit = 5 -wait_ext_drive = 5 -max_foldername_length = 246 -nomedia_marker = "" -ipv6_servers = 1 -url_base = /sabnzbd -host_whitelist = usenet.kruining.eu, ulmo -local_ranges = , -max_url_retries = 10 -downloader_sleep_time = 10 -receive_threads = 2 -switchinterval = 0.005 -ssdp_broadcast_interval = 15 -ext_rename_ignore = , -email_server = "" -email_to = , -email_from = "" -email_account = "" -email_pwd = "" -email_endjob = 0 -email_full = 0 -email_dir = "" -email_rss = 0 -email_cats = *, -config_conversion_version = 4 -disable_par2cmdline = 0 -disable_archive = 0 -unrar_parameters = "" -outgoing_nntp_ip = "" -[logging] -log_level = 1 -max_log_size = 5242880 -log_backups = 5 -[ncenter] -ncenter_enable = 0 -ncenter_cats = *, -ncenter_prio_startup = 0 -ncenter_prio_download = 0 -ncenter_prio_pause_resume = 0 -ncenter_prio_pp = 0 -ncenter_prio_complete = 1 -ncenter_prio_failed = 1 -ncenter_prio_disk_full = 1 -ncenter_prio_new_login = 0 -ncenter_prio_warning = 0 -ncenter_prio_error = 0 -ncenter_prio_queue_done = 0 -ncenter_prio_other = 1 -ncenter_prio_quota = 1 -[acenter] -acenter_enable = 0 -acenter_cats = *, -acenter_prio_startup = 0 -acenter_prio_download = 0 -acenter_prio_pause_resume = 0 -acenter_prio_pp = 0 -acenter_prio_complete = 1 -acenter_prio_failed = 1 -acenter_prio_disk_full = 1 -acenter_prio_new_login = 0 -acenter_prio_warning = 0 -acenter_prio_error = 0 -acenter_prio_queue_done = 0 -acenter_prio_other = 1 -acenter_prio_quota = 1 -[ntfosd] -ntfosd_enable = 1 -ntfosd_cats = *, -ntfosd_prio_startup = 0 -ntfosd_prio_download = 0 -ntfosd_prio_pause_resume = 0 -ntfosd_prio_pp = 0 -ntfosd_prio_complete = 1 -ntfosd_prio_failed = 1 -ntfosd_prio_disk_full = 1 -ntfosd_prio_new_login = 0 -ntfosd_prio_warning = 0 -ntfosd_prio_error = 0 -ntfosd_prio_queue_done = 0 -ntfosd_prio_other = 1 -ntfosd_prio_quota = 1 -[prowl] -prowl_enable = 0 -prowl_cats = *, -prowl_apikey = "" -prowl_prio_startup = -3 -prowl_prio_download = -3 -prowl_prio_pause_resume = -3 -prowl_prio_pp = -3 -prowl_prio_complete = 0 -prowl_prio_failed = 1 -prowl_prio_disk_full = 1 -prowl_prio_new_login = -3 -prowl_prio_warning = -3 -prowl_prio_error = -3 -prowl_prio_queue_done = -3 -prowl_prio_other = 0 -prowl_prio_quota = 0 -[pushover] -pushover_token = "" -pushover_userkey = "" -pushover_device = "" -pushover_emergency_expire = 3600 -pushover_emergency_retry = 60 -pushover_enable = 0 -pushover_cats = *, -pushover_prio_startup = -3 -pushover_prio_download = -2 -pushover_prio_pause_resume = -2 -pushover_prio_pp = -3 -pushover_prio_complete = -1 -pushover_prio_failed = -1 -pushover_prio_disk_full = 1 -pushover_prio_new_login = -3 -pushover_prio_warning = 1 -pushover_prio_error = 1 -pushover_prio_queue_done = -3 -pushover_prio_other = -1 -pushover_prio_quota = -1 -[pushbullet] -pushbullet_enable = 0 -pushbullet_cats = *, -pushbullet_apikey = "" -pushbullet_device = "" -pushbullet_prio_startup = 0 -pushbullet_prio_download = 0 -pushbullet_prio_pause_resume = 0 -pushbullet_prio_pp = 0 -pushbullet_prio_complete = 1 -pushbullet_prio_failed = 1 -pushbullet_prio_disk_full = 1 -pushbullet_prio_new_login = 0 -pushbullet_prio_warning = 0 -pushbullet_prio_error = 0 -pushbullet_prio_queue_done = 0 -pushbullet_prio_other = 1 -pushbullet_prio_quota = 1 -[apprise] -apprise_enable = 0 -apprise_cats = *, -apprise_urls = "" -apprise_target_startup = "" -apprise_target_startup_enable = 0 -apprise_target_download = "" -apprise_target_download_enable = 0 -apprise_target_pause_resume = "" -apprise_target_pause_resume_enable = 0 -apprise_target_pp = "" -apprise_target_pp_enable = 0 -apprise_target_complete = "" -apprise_target_complete_enable = 1 -apprise_target_failed = "" -apprise_target_failed_enable = 1 -apprise_target_disk_full = "" -apprise_target_disk_full_enable = 0 -apprise_target_new_login = "" -apprise_target_new_login_enable = 1 -apprise_target_warning = "" -apprise_target_warning_enable = 0 -apprise_target_error = "" -apprise_target_error_enable = 0 -apprise_target_queue_done = "" -apprise_target_queue_done_enable = 0 -apprise_target_other = "" -apprise_target_other_enable = 1 -apprise_target_quota = "" -apprise_target_quota_enable = 1 -[nscript] -nscript_enable = 0 -nscript_cats = *, -nscript_script = "" -nscript_parameters = "" -nscript_prio_startup = 0 -nscript_prio_download = 0 -nscript_prio_pause_resume = 0 -nscript_prio_pp = 0 -nscript_prio_complete = 1 -nscript_prio_failed = 1 -nscript_prio_disk_full = 1 -nscript_prio_new_login = 0 -nscript_prio_warning = 0 -nscript_prio_error = 0 -nscript_prio_queue_done = 0 -nscript_prio_other = 1 -nscript_prio_quota = 1 -[categories] -[[*]] -name = * -order = 0 -pp = 3 -script = None -dir = "" -newzbin = "" -priority = 0 -[[movies]] -name = movies -order = 1 -pp = "" -script = Default -dir = "" -newzbin = "" -priority = -100 -[[tv]] -name = tv -order = 2 -pp = "" -script = Default -dir = "" -newzbin = "" -priority = -100 -[[audio]] -name = audio -order = 3 -pp = "" -script = Default -dir = "" -newzbin = "" -priority = -100 -[[software]] -name = software -order = 4 -pp = "" -script = Default -dir = "" -newzbin = "" -priority = -100 -[servers] -[[news.sunnyusenet.com]] -name = news.sunnyusenet.com -displayname = news.sunnyusenet.com -host = news.sunnyusenet.com -port = 563 -timeout = 60 -username = michiel@hazelhof.nl -password = dasusenet -connections = 8 -ssl = 1 -ssl_verify = 3 -ssl_ciphers = "" -enable = 1 -required = 0 -optional = 0 -retention = 0 -expire_date = "" -quota = "" -usage_at_start = 0 -priority = 1 -notes = "" diff --git a/script/.shared/pwgen b/script/.shared/pwgen deleted file mode 100644 index 85fc69f..0000000 --- a/script/.shared/pwgen +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -pwgen -s 128 1 diff --git a/script/qbittorrent/hash.py b/script/qbittorrent/hash.py deleted file mode 100644 index a92343f..0000000 --- a/script/qbittorrent/hash.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/bash - -import base64 -import hashlib -import sys -import uuid - -password = sys.argv[1] -salt = uuid.uuid4() -salt_bytes = salt.bytes - -password = str.encode(password) -hashed_password = hashlib.pbkdf2_hmac("sha512", password, salt_bytes, 100000, dklen=64) -b64_salt = base64.b64encode(salt_bytes).decode("utf-8") -b64_password = base64.b64encode(hashed_password).decode("utf-8") -password_string = "@ByteArray({salt}:{password})".format( - salt=b64_salt, password=b64_password -) -print(password_string) diff --git a/script/qbittorrent/password b/script/qbittorrent/password deleted file mode 100644 index 85fc69f..0000000 --- a/script/qbittorrent/password +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -pwgen -s 128 1 diff --git a/script/qbittorrent/password_hash b/script/qbittorrent/password_hash deleted file mode 100644 index 86ba315..0000000 --- a/script/qbittorrent/password_hash +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -python ./hash.py "$(just vars get ulmo qbittorrent/password | jq -r)" diff --git a/script/synapse/shared_secret b/script/synapse/shared_secret deleted file mode 100644 index 85fc69f..0000000 --- a/script/synapse/shared_secret +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -pwgen -s 128 1 diff --git a/shells/default/default.nix b/shells/default/default.nix deleted file mode 100644 index 76d15b7..0000000 --- a/shells/default/default.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - mkShell, - inputs, - pkgs, - stdenv, - ... -}: -mkShell { - packages = with pkgs; [ - bash - sops - just - yq - pwgen - alejandra - nil - nixd - openssl - inputs.clan-core.packages.${stdenv.hostPlatform.system}.clan-cli - nix-output-monitor - dos2unix - ]; -} diff --git a/systems/x86_64-linux/aule/secrets.yml b/systems/x86_64-linux/aule/secrets.yml deleted file mode 100644 index 7069c7b..0000000 --- a/systems/x86_64-linux/aule/secrets.yml +++ /dev/null @@ -1,33 +0,0 @@ -sops: - age: - - recipient: age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZMC9nRjVFWnZlMHJJK0Nl - dWFTR0FCUGNBYXIrUHlIUUphZll2QU9IOEZrCitFS3JvK3hYYmpEZ05aRStpdUd1 - L3JjNDl1Z2hQQ3FuNUZNM1hCRUtQUG8KLS0tIEg4VVEvVjZYN3JHSXljQW1xS3E4 - eVpyM1lSWExndlZhMkw2Vis4dVhjSVUKbk+z1h3Hb1A6SEbZ3g5vYui/FfkMyfxx - Zm67JenYittHvQggTIErAgJatTocfVB6Zy4FqJtPCOevTVrRTRkwAg== - -----END AGE ENCRYPTED FILE----- - - recipient: age1ewes0f5snqx3sh5ul6fa6qtxzhd25829v6mf5rx2wnheat6fefps5rme2x - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtOGJXWi9vUzdFbkx2NmVa - YnhITlNMc1RRRXoyOFNPN1B4VWQ5ZDUwNDFBCnVmdDFyUnptekxhOUlwdVcyRjFI - cHRSRkoyWnFVUDJMcXpVcmM5bjRKMkkKLS0tIDROWXR1UFFUa0NxcUtkdEwxQ2Vl - OW50OE9RMWpyT1AvS0QzZ3JVNDViYlkK77H0Uq3eRy0CHgH4bhdo7FVEJpKeR/DB - KZonll74qqsyW4n+hIbIybjaqtF3RBN4kj5ARuIGFmH8sAl6jSyHXA== - -----END AGE ENCRYPTED FILE----- - - recipient: age1jmrmdw4kmjeu9d6z74r2unqt7wpgsx24vqejmdjretsnsn8g4drsl3m98w - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZOURoRmk4QldEZExTRDYx - cXluYkg4OUFUNDNrQUNiNWRwKzhEQkdaemxzCnM3b25GYm5TM3NuNnBsVWRmQzNL - bTRabmx2UzBkN1dadlhwajN5RDIxVW8KLS0tIDhSQ1o4RGZBdlVHaHRKQWFyazU0 - N0lnMjMvREpmNWZvTUdiT0tjMk4vTk0KmIN1a3gjmFzaEwJBu41sw5Z61UgiO5fc - /pkS22BeVonuB12SmJX+77A1CxFz1EwM8HSShFKlpN2hPCJFJL7Nng== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-02-25T07:59:24Z" - mac: ENC[AES256_GCM,data:64AkqWb97nUciWtOOHP/SZhUeo/5ahxa0cN14ILw/jmToFkn8uDrSfY8/ibqBB0mmfhwGzcnI/5QpCLVzCSgG1J68bdPeSsYTZPwy2/0S0ven+GeqYHMfJ2Q1eJE7TONyOEvSdYdUWG+ff5t0qhSet9F2BgFnMSKcNeAaxIY6KU=,iv:aMQXbKk8oKSLBHIZyJLJahu5HHEMysmhcgfpDdZG+Ak=,tag:hqBVXis8MdqRorxttYeQaw==,type:str] - unencrypted_suffix: _unencrypted - version: 3.11.0 diff --git a/systems/x86_64-linux/manwe/README.md b/systems/x86_64-linux/manwe/README.md index 3bb6746..1da7ab1 100644 --- a/systems/x86_64-linux/manwe/README.md +++ b/systems/x86_64-linux/manwe/README.md @@ -1,8 +1,3 @@ # Description -<<<<<<< HEAD My steambox. -======= -My desktop, reasoning for the name being the following chain of thought: -**Manwe -> the king of the valar -> leader -> desktop is main machine** ->>>>>>> 72b0f6f8fad97a4ade1b54dfada26828a170febf diff --git a/systems/x86_64-linux/manwe/default.nix b/systems/x86_64-linux/manwe/default.nix index a1b421b..76d4e6d 100644 --- a/systems/x86_64-linux/manwe/default.nix +++ b/systems/x86_64-linux/manwe/default.nix @@ -1,15 +1,10 @@ -{ pkgs, ...}: { +{ ... }: +{ imports = [ ./disks.nix ./hardware.nix ]; - system.activationScripts.remove-gtkrc.text = "rm -f /home/chris/.gtkrc-2.0"; - - services.logrotate.checkConfig = false; - - environment.systemPackages = with pkgs; [ beyond-all-reason openrct2 ]; - sneeuwvlok = { hardware.has = { gpu.amd = true; @@ -33,6 +28,7 @@ }; }; + services.displayManager.autoLogin = { enable = true; user = "chris"; diff --git a/systems/x86_64-linux/manwe/disks.nix b/systems/x86_64-linux/manwe/disks.nix index f33ec71..e3e449f 100644 --- a/systems/x86_64-linux/manwe/disks.nix +++ b/systems/x86_64-linux/manwe/disks.nix @@ -1,34 +1,59 @@ -{ config, lib, pkgs, modulesPath, ... }: +{ config, lib, pkgs, modulesPath, inputs, ... }: let inherit (lib.modules) mkDefault; in { - # TODO :: Implement disko at some point + imports = [ + inputs.disko.nixosModules.disko + ]; - swapDevices = []; + config = { + swapDevices = []; - boot.supportedFilesystems = [ "nfs" ]; + boot.supportedFilesystems = [ "nfs" ]; - fileSystems = { - "/" = { - device = "/dev/disk/by-label/nixos"; - fsType = "ext4"; + disko.devices = { + disk = { + main = { + device = "/dev/nvme0"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "100M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; }; + + fileSystems = { + "/home/chris/media" = { + device = "ulmo:/"; + fsType = "nfs"; + }; - "/boot" = { - device = "/dev/disk/by-label/boot"; - fsType = "vfat"; - options = [ "fmask=0022" "dmask=0022" ]; + "/home/chris/mandos" = { + device = "mandos:/"; + fsType = "nfs"; + }; }; - - "/home/chris/media" = { - device = "ulmo:/"; - fsType = "nfs"; - }; - - # "/home/chris/mandos" = { - # device = "mandos:/"; - # fsType = "nfs"; - # }; }; } diff --git a/systems/x86_64-linux/manwe/secrets.yaml b/systems/x86_64-linux/manwe/secrets.yaml new file mode 100644 index 0000000..6e2a986 --- /dev/null +++ b/systems/x86_64-linux/manwe/secrets.yaml @@ -0,0 +1,31 @@ +zitadel: + masterKey: ENC[AES256_GCM,data:iSeZOloWLrdP8S+ac7ubIcv9TF3Sm8Ni,iv:8v3/ratFQ5vq2rbZOUMKfPhVTA9uQY2eFQU4IR8s3VU=,tag:9y90aDQ2PfFT//X2i2YvvA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age10c5hmykkduvy75yvqfnchm5lcesr5puarhkwp4l7xdwpykdm397q6xdxuy + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4R0UyWmx5L3hCbGhQVXI0 + NmpkMThPVlgrRHZZMnFrNTAwbzVTY1F6NEVVCjJaRHdhbHV6R1RJM2JIQzc3dkNu + a01FYlM3b1dXbmxGN2tWU3FMdXMveG8KLS0tIG1SSjNXdXZNN2ZyQ2UyZ0pIZXJJ + NmpMS2oySFE1S1RER3J1RGl4MlRQK00Ks+PcxcHmygYz+a+d0ZrzrdUpTQ50NYkA + aDFbtRtukn9e7i3bGUyD4nisSvs4YjfoQxR/pC8hs4k3f5V2jwDh2w== + -----END AGE ENCRYPTED FILE----- + - recipient: age1ewes0f5snqx3sh5ul6fa6qtxzhd25829v6mf5rx2wnheat6fefps5rme2x + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwaTN4clFoWDNwU2lpaHBn + M2pVeU5oM0JRNmp6NEJjQ3BHeWlzeSs3bTI0CnBocngvbzZQUXBsMG9Oc2J6dlBT + MjdtaFdmOHg5ZmZmSkViWGJFYThQYXcKLS0tIFRNd2JiVlFTREtDMTdzR2V0SlVo + Q0d5ZDVDM05LdFp4UnB4dFRPUm5vU0UKR/MAONEWaT6XXyPB1IrSIKqW5PZNIbuB + n7QX3DJIzlajtmq+82/wPFPTBkLvSSjV5FKL5ErMwTDndcIn+NlOhQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2025-08-11T13:11:00Z" + mac: ENC[AES256_GCM,data:P34YsR/Rvc3q4Os5n9hxonJLCXwifMRnKOCM59h5MRMT/aqjl+QlBX+oUADsqDSrhUscQb3N/UlpFeOT6qg+FmJbT/mYMH6v1xK16VD0M7VWydXpmjDu5If+O89lgDHsiEOGDgeR04jkiaY0yzT9U8l9CND5fMvF3I9o5Z1SZQk=,iv:NgUD8gB2bQa5vh0nb0Ngqp5dn0yqskHudWo8xoVjM4Q=,tag:5oTcnailDCHeMvMLz63e1w==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.9.4 diff --git a/systems/x86_64-linux/orome/default.nix b/systems/x86_64-linux/orome/default.nix index e155461..48e049b 100644 --- a/systems/x86_64-linux/orome/default.nix +++ b/systems/x86_64-linux/orome/default.nix @@ -1,22 +1,16 @@ -{pkgs, ...}: { +{ ... }: +{ imports = [ ./disks.nix ./hardware.nix ]; - environment.systemPackages = with pkgs; [ - azure-cli - github-copilot-cli - ]; - sneeuwvlok = { hardware.has = { bluetooth = true; audio = true; }; - authentication.himmelblau.enable = true; - application = { steam.enable = true; }; diff --git a/systems/x86_64-linux/ulmo/default.nix b/systems/x86_64-linux/ulmo/default.nix index fd25824..7a2540f 100644 --- a/systems/x86_64-linux/ulmo/default.nix +++ b/systems/x86_64-linux/ulmo/default.nix @@ -1,257 +1,14 @@ +{ ... }: { - pkgs, - config, - ... -}: { imports = [ ./disks.nix ./hardware.nix ]; - environment.systemPackages = with pkgs; [bup]; - services.postgresqlBackup = { - enable = true; - backupAll = true; - startAt = "*-*-* 01:00:00"; - location = "/var/backup/postgresql"; - }; - - networking = { - interfaces.enp2s0 = { - ipv6.addresses = [ - { - address = "2a0d:6e00:1dc9:0::dead:beef"; - prefixLength = 64; - } - ]; - - useDHCP = true; - }; - - defaultGateway = { - address = "192.168.1.1"; - interface = "enp2s0"; - }; - - defaultGateway6 = { - address = "fe80::1"; - interface = "enp2s0"; - }; - }; - sneeuwvlok = { services = { - backup.borg.enable = true; - - authentication.zitadel = { - enable = true; - - organization = { - nix = { - user = { - chris = { - email = "chris@kruining.eu"; - firstName = "Chris"; - lastName = "Kruining"; - - roles = ["ORG_OWNER"]; - instanceRoles = ["IAM_OWNER"]; - }; - - kaas = { - email = "chris+kaas@kruining.eu"; - firstName = "Kaas"; - lastName = "Kruining"; - }; - }; - - project = { - ulmo = { - projectRoleCheck = true; - projectRoleAssertion = true; - hasProjectCheck = true; - - role = { - jellyfin = { - group = "jellyfin"; - }; - jellyfin_admin = { - group = "jellyfin"; - }; - }; - - assign = { - chris = ["jellyfin" "jellyfin_admin"]; - kaas = ["jellyfin"]; - }; - - application = { - jellyfin = { - redirectUris = ["https://jellyfin.kruining.eu/sso/OID/redirect/zitadel"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - - forgejo = { - redirectUris = ["https://git.amarth.cloud/user/oauth2/zitadel/callback"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - - vaultwarden = { - redirectUris = ["https://vault.kruining.eu/identity/connect/oidc-signin"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - exportMap = { - client_id = "SSO_CLIENT_ID"; - client_secret = "SSO_CLIENT_SECRET"; - }; - }; - - matrix = { - redirectUris = ["https://matrix.kruining.eu/_synapse/client/oidc/callback"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - - mydia = { - redirectUris = ["http://localhost:2100/auth/oidc/callback"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - - grafana = { - redirectUris = ["http://localhost:9010/login/generic_oauth"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - }; - }; - - convex = { - projectRoleCheck = true; - projectRoleAssertion = true; - hasProjectCheck = true; - - application = { - scry = { - redirectUris = ["https://nautical-salamander-320.eu-west-1.convex.cloud/api/auth/callback/zitadel"]; - grantTypes = ["authorizationCode"]; - responseTypes = ["code"]; - }; - }; - }; - }; - - action = { - flattenRoles = { - script = '' - (ctx, api) => { - if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) { - return; - } - - const roles = ctx.v1.user.grants.grants.flatMap(({ roles, projectId }) => roles.map(role => projectId + ':' + role)); - - api.v1.claims.setClaim('nix:zitadel:custom', JSON.stringify({ roles })); - }; - ''; - }; - }; - - triggers = [ - { - flowType = "customiseToken"; - triggerType = "preUserinfoCreation"; - actions = ["flattenRoles"]; - } - { - flowType = "customiseToken"; - triggerType = "preAccessTokenCreation"; - actions = ["flattenRoles"]; - } - ]; - }; - }; - }; - - communication.matrix.enable = true; - - development.forgejo.enable = true; - networking.ssh.enable = true; - networking.caddy.hosts = { - # Expose amarht cloud stuff like this until I have a proper solution - "auth.amarth.cloud" = '' - reverse_proxy http://192.168.1.223:9092 - ''; - - "amarth.cloud" = '' - reverse_proxy http://192.168.1.223:8080 - ''; - }; - media.enable = true; - media.glance.enable = true; - media.mydia.enable = true; - media.nfs.enable = true; - media.jellyfin.enable = true; - media.servarr = { - radarr = { - enable = true; - port = 2010; - rootFolders = [ - "/var/media/movies" - ]; - }; - - sonarr = { - enable = true; - # debug = true; - port = 2020; - rootFolders = [ - "/var/media/series" - ]; - }; - - lidarr = { - enable = true; - debug = true; - port = 2030; - rootFolders = [ - "/var/media/music" - ]; - }; - - prowlarr = { - enable = true; - # debug = true; - port = 2040; - }; - }; - - observability = { - alloy.enable = true; - grafana.enable = true; - loki.enable = true; - prometheus.enable = true; - promtail.enable = true; - tempo.enable = true; - # uptime-kuma.enable = true; - }; - - security.vaultwarden = { - enable = true; - database = { - # type = "sqlite"; - # file = "/var/lib/vaultwarden/state.db"; - - type = "postgresql"; - host = "localhost"; - port = 5432; - sslMode = "disabled"; - }; - }; }; editor = { diff --git a/systems/x86_64-linux/ulmo/disks.nix b/systems/x86_64-linux/ulmo/disks.nix index 0b272f4..a4033f7 100644 --- a/systems/x86_64-linux/ulmo/disks.nix +++ b/systems/x86_64-linux/ulmo/disks.nix @@ -5,7 +5,9 @@ in { # TODO :: Implement disko at some point - swapDevices = []; + swapDevices = [ + { device = "/dev/disk/by-uuid/0ddf001a-5679-482e-b254-04a1b9094794"; } + ]; boot.supportedFilesystems = [ "nfs" ]; diff --git a/systems/x86_64-linux/ulmo/secrets.yml b/systems/x86_64-linux/ulmo/secrets.yml deleted file mode 100644 index 869e63e..0000000 --- a/systems/x86_64-linux/ulmo/secrets.yml +++ /dev/null @@ -1,69 +0,0 @@ -email: - chris_kruining_eu: ENC[AES256_GCM,data:/JS+dQ6ABlkdjRZP+sGeUY3js30swS4=,iv:d5CcoY6DD3DJ/e3t0OU/KUULccJpTN0uBQPQzl/3R0s=,tag:aTN7RdzXkIpci9tEBjevSA==,type:str] - info_amarth_cloud: ENC[AES256_GCM,data:/x7aAFAxXYYf79tB08VQmmuTIy2TvdSTFfAzIWdIr+I=,iv:plNxS6oOin+oEql+1xsePOsUfLJkf+ZPBviPRTbIghE=,tag:hjtK3rysd2NNBA2mWdv8cw==,type:str] -zitadel: - masterKey: ENC[AES256_GCM,data:4MPvBo407qrS7NF4oUTf84tZoPkSRmiHdD7qpkYeHME=,iv:H2NIAN0xBUDqnyco9gA3zYAsKtSeA/JpqYrPhc1eqc0=,tag:6OFGDfsucG5gDerImgpuXA==,type:str] - nix: {} - users: ENC[AES256_GCM,data:ikpAuiQT32i4+aaVPz/nRqlf5ESID3khat2MrOySOfF9duJaQLWBonaKau6JVRljnGb+RGTiEH/EpxzXHnNydfHrir/jS4cDFMUMNV9aee0CyEbfqHAFqbC3B4ReZZE+XCkiq1j5jLnRg7EiGRK5+g+ul2iGIAwJ5SoHiOSSBcJ2E4B+AdkhGVO6Qsf+DW3hUZ/MsoaDsOB3IX15iC6/9z+NT+/Jefz5In6jn/vdYpD2i/zWvNHHPVXIkK1Co8FUidRdOjyWiiCb4+A0DI5v9E69xKe4zl26GHv3+1aK7cTxq2meDI4AXKhaTpak0A/neO/E6Xrc78752rTNRUDre9jJNrip/UPu8KvaCzpUi8Y4aN2Qg6ICF6JudzgouFyOGJ/JyxjVcJhUBOof/vCOcihdmHlo8sgyAi5mn/70VqnEF6Ei4KkRMAMlz9mfEVHDmjWMP1wHLw8eJD+Vhn/AJ76VecSCr51OHYtwgEcQXC6ikyPwBn8XQ5CNae/XGhcs0c8UbAcUXCH40zxvn4DFYHzJCkwurqv2iiV5zRN+rre6SoEWIToByq5KAwzkgLrLIVIbYWcLXlBYLvuMjnHbRknqWndQS72fRds0EWg+/OfjO+0SrPkJIoHkMNiUUmoq17ouwz0mcKVEh3o1Wptrp54ArDLkUjdtbOhaGTEzpGH+y0b+LITiN0erGPFITjf8sgGtvg+fRnoqCxPpex99,iv:+MjTW26sd8csWm4RXscFMgUm3wNY5Yj+qP8Xfg/WvsQ=,tag:mXjrEJqpbuqaVLa8EJpjoQ==,type:str] -forgejo: - action_runner_token: ENC[AES256_GCM,data:yJ6OnRq5kinbuhvH06K5o3l86EafuBoojMwg/qhP+cgeH+BwPeE+Ng==,iv:IeXJahPxgLNIUFmkgp495tLVh8UyQBmJ2SnVEUhlhHs=,tag:XYQi613CxSp8AQeilJMrsg==,type:str] -synapse: - oidc_id: ENC[AES256_GCM,data:XbCpyGq0LeRJWq8dv/5Dipvp,iv:YDhgl26z1NBbIQLoLdGVz0+ze6o1ZcmgVHPfwoRj57I=,tag:y2vUuqnDmtTvVQmZCAlnLg==,type:str] - oidc_secret: ENC[AES256_GCM,data:nVFi5EFbNMZ0mvrDHVYC0NiwJlo2eEw44D+Fcv9SKSb2oO00lGEDkP/oXDj5YgDq6RLQSe3f/SUOn77ntwnZYg==,iv:awe7VNUYOn9ofl1QlQTrEN5d0i5WkVM35qndruL4VXo=,tag:8Yoc9lFF9aWbtAa5fzQGEA==,type:str] - shared_secret: ENC[AES256_GCM,data:IkzZ6QV1gLzChAFSsYsK3HM5dKFD4AoDJ53xgoxNpgt5tb45mMw/LRxu4NArGVLUtVGBy6jk6arU+Nxvi8bxPOC8c2UFCRUF+FM1phICEbb4Chgy5g803VKNFOu6BLaEmwDmuZSQP7CwX1hy8TX8yChboHGp7hH+n5SAZpejrLg=,iv:d+Ab91yCltYwudDWhrWPw0Xod/TKriCsoGD8i6PD4H4=,tag:xOXnzNuajcOz+imjMJr3Dg==,type:str] -radarr: - apikey: ENC[AES256_GCM,data:G141GW4PyS5pbAV39HcVscMw3s30txOgTZzWaL7o+ccZfnfDLv796O6xKXdqGZ8saLsveghLw9Z6a5luusHyQ3Q5ESL6W7SVeZVTuSqSC3i/4jl75FJxhnsgVsfrnYxzLGpKiw==,iv:sZl/XLh6y3WgSAn6nH3sFB6atBifZdghm+QsCNDbcjY=,tag:Tw+R80nrF0T0yDti0Uf+ig==,type:str] -sonarr: - apikey: ENC[AES256_GCM,data:s8bgDJ+LpIH1Mt3KSiIKB8LnxztOkHdc8J6+50o+HoDUAfIIsZkA2oX/m7UecrTSRi6ay8D9yjhe6ZwSNXhJh6wQqTS7gZWn8f6QfrfI+8DKdc9enh91suQxjkz8Q+wnKK0zBg==,iv:LmAe6v+6ItVnHB6gko6mhiGOuVBksBYP4dXfbxpAIPE=,tag:DZ8kwOwaWwWTGWEGu5S0Kg==,type:str] -lidarr: - apikey: ENC[AES256_GCM,data:I2eKaxidmxem7C7ukmyIfwASNqrkS4vEOiCcU5kSNY6DR0pXsYg0PBdgu8vzK6llbXODLdG5t55BordIWvVRJGAauo0FMvtp59NSNpza7cK68tdKGvNefD6bqhUIR06BY11niQ==,iv:48AD7cd17TlWY5yAagepLOIVwgxhD/d13Pnup6GsWDA=,tag:teOVtW8opE99hqAXQwvlrA==,type:str] -prowlarr: - apikey: ENC[AES256_GCM,data:pyZ2WGEs/PlIdhDsQq2TPGJbplkd5fLF0ZkBjITqIJlnAzYHb+rl+KOM4rHqQcI6yAJM8X1Y3ymGrD7vG7GiRxB7yoEG13SKhZIWOddTnxIhbkz81RfrL2fUJIydOaP6sS//9Q==,iv:Tr6MWoC6nC7rdVTOjT1T2itT+lVL4GnUiAr5/+IHAs0=,tag:keIJNuGeVht8+xSN3FnBGA==,type:str] -mydia: - oidc_id: ENC[AES256_GCM,data:LfYWh9EC0aio3w1Xsj/jtU6z,iv:+dX9KkNtfQMYSX4yr83KyXalWMD/aWby7fC8aL4ZT3I=,tag:CvdbMoMTuC9FohTMIE5pmg==,type:str] - oidc_secret: ENC[AES256_GCM,data:PgI4hmP/3wt9uj+1QvCYcT8Wav0hgCRADouzWM3V695SSfXfbwDgez8tA/tm1/1jymAU2F2sZH8G2hZ1cdHyHQ==,iv:h3o3jsTmnoNE3+mGX12J3ZU0/6PlQNjdndEvaj/czj0=,tag:p3+p4E8fBtR7a8UpM8cUsg==,type:str] - secret_key_base: ENC[AES256_GCM,data:yG7HJ5r74Qtxbeyf8F6dA0uHv2pQ8YAJKlKiKjS+m24JRvJWQaTThJ+c5HbuUa6R3e9XtVHchhlVPkF0Is/b+g==,iv:v65xdRr4JdKZmBtjZ08/J3LLqnphSGt9QfVPNQ2x/xg=,tag:n7tD2dhr4IJn1LWM9WW8UA==,type:str] - guardian_secret: ENC[AES256_GCM,data:OjnNFSHlecL+qXwlhTm++itRM6ga5E5KrSJxbgIUpbMEkIWgu3xhRtnPdipXbedgall0XdO/s+jnWCagZX94BA==,iv:DukdKvm9vey8BWUiml20tgA/Vji1XVX4+sUPge9nTk0=,tag:q3HdvgUYqR0APiaFz0ul5Q==,type:str] -grafana: - oidc_id: ENC[AES256_GCM,data:NVdIgCQ6nz4BSUDJYCKyILtK,iv:tcljy9PzC/yyd7TSdngyJt+uh60uXi2PKu47czErbaQ=,tag:zE4q3dD4UQaHIpGeZ1L48Q==,type:str] - oidc_secret: ENC[AES256_GCM,data:b7qILK9ZHW2khtM1Hl/KdjCv3Wq6eOo2Ym/cbjcMB8/3Hn2UelpP4K4lFyiV3bn1/GF6Jl5Z7A0EwMybOx0InA==,iv:3HL/7BiyObwT8DmFxzNPI9CdmCH/4j/4oc9x7qBE1k0=,tag:dBhcq1zLKy6N+jp/v42R4A==,type:str] - secret_key: ENC[AES256_GCM,data:u6IRFV1D/4g+eqQIUPW0QHlkoa+MliymThp34k+QCHqQ247er4bCdgftuWsXgPAPY7DtwFVLG7Do5eBqIiii7g==,iv:FY7LIW0O5/Cp2JvYu17ctInt0rgkzjaPHfxZBs0GTac=,tag:Gtu+ZGAgsi5vzILOKDac1g==,type:str] -sabnzbd: - sunnyweb: - password: ENC[AES256_GCM,data:flw8AahqO1Mx,iv:Qhu8iVWMzzqy18y8dj3aHoBnSZatm74/tYvZ456l2sA=,tag:sCYBdw7kD0zJZFFr5EyPIQ==,type:str] - username: ENC[AES256_GCM,data:IboJ8WDWuVNgvrk7c3V8I5S6Xg==,iv:BRohMuQFQz2S+HFasIaok6npT3C5v/SlhAhbLQXfB0s=,tag:M3/u0WBQ3AufHqe4DCtsrA==,type:str] - apikey: ENC[AES256_GCM,data:j5sPXKbBhMdNHOuoTfZ+c8nGu5JameOgK2z428iLdP01Hi6MvHVaN8Zs8YxMoSBtOjdtIEC8MS+3m1S1rU/P4pCRfZpK5ua1DBHq4l0xROUqokFWjDcAmJJv3pYXl0cQxQcGKQ==,iv:v5hu3gmO1Zn1FfXkHLPGN9f7JOcQjzoQahdqJwfM+xY=,tag:uI1LFcTgcyRgAaTJ1kzKow==,type:str] - nzbkey: ENC[AES256_GCM,data:tGFnZ24XNI7U8pVYq45ENSVTeVkkcWfT5/NewqSJ3sm7Bexxml/PFTMBIl+97mWzNMMFklBurX/115P06NHCj1mxEvIjIc1bF4yuYhZFdSTlqRVWaESE/Ei7gke758FCt37N43wADgaKj4i5jizDHJMIbaw8ncP3qBSCy1F4BAU=,iv:RA+3oYGhVLBG+ikHMwBG3t2iN15lGsncdmlkfF6vJhY=,tag:6FNM18KCSzzpIXYDpQfHSg==,type:str] -whisparr: - apikey: ENC[AES256_GCM,data:kIGCsd4mszm90PoQMzlSEBKw9Ow0GvP1qdLtwXYKkAb6b65l89v8lMWJ2X1MyD2gJX+P+Bv1F/2BSjUFXErq/UYnp4dAjwKi/ezGCbhjMutDM1FvwFWEHRnR3gjd9uXPWJ8Xhg==,iv:98aPQlcZHJovpnzACDs6RtKblLnHg6wyi+Er5DAowj8=,tag:Tl8jz/pWYWAtBCfoztKdyw==,type:str] -coturn: - secret: ENC[AES256_GCM,data:5RmLZ7vQIAvIzvax8oNJkImQ6vXR+MZ2eqxaBJCBlccnFC1rP16/6UtausXVf0eWysw+fpMW5yEmUtAdyxQoPiBCK8lziAZBdkekQnAvFouBaWy8WIZt6XRa71P4xDCDGudpMiGwGGNt+R9yylez+azaLrLyJM3481RPohDMoOM=,iv:2P83lgxGtHwYr+ApAdHopVfRWagxWlC+nt53API/SiQ=,tag:Qv+A03BE1QvEqJMtORiQVA==,type:str] -qbittorrent: - password: ENC[AES256_GCM,data:LIDxh0Ni0JgQGWFix/Ihw7IlUPgzMhrMlWNP5LKkAnEM6EoqA9kFwiPeizB0CZ20+vSqRiL9fikBf8qGLA17L7AKh8I4OTFDlpKpMRtRlMq9S5UBEyOqtOMcvkCSf6/qGoORd1KJSlaitZk47SYRuccOpy/2vAvbMRdLm0SYEqc=,iv:tQdN1N9kXoq7OZbR2eYyy50FltsMAAUI4Lr7U4/SpJE=,tag:3ZOLvjHXD7i7WFy1/Ggqtg==,type:str] - password_hash: ENC[AES256_GCM,data:urufJbSErLqPdU6jLLZk+27fe4k+cKLXcGRGSqroUDdGMzDnhSF+ZWuPxwDlJQR3ws2GnuiEASncwNO/SALKXFDk2V2gsKJ4hsjyiIbsqCwSEFB/XMY0nY/x0xrcIfMVE0HdrNYeQ3zT01Z5jQpSd7wo2M63LaULL/Av498=,iv:tnUVhOgrImKa6iii2hJZn5LKrySM5v47B2zDZMgmUow=,tag:g3xa/4Z+t1Q9Wnd4XzefLg==,type:str] -backup: - ssh-key: ENC[AES256_GCM,data:aRY+9mYssEXPmfJQ2KOYU4wxkgzgYbv3GJ4KUkECSZ6IdQVv4CpKMg75dEhO5/t7MYjiNXze5WibZ0UHSTnUv4OB6NP6Mp1HZjIZb6paCJxjkoul0BVwtF5AKViJe0LIKoh+,iv:kZgZTqgYdqJSD6rO3lj/IFqhO9mYgZ7YYOCS2b+xpXQ=,tag:xPh0yL2uMyqgrioC36PPpA==,type:str] -sops: - age: - - recipient: age19qfpf980tadguqq44zf6xwvjvl428dyrj46ha3n6aeqddwhtnuqqml7etq - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwdDZyZkxvNU4zM3NHb2gx - ZlhLZk5JWUFGMWZGeUVHNkFFU1NtZlBQVVhjCmZGai9NdmdUeU5VcW9ROVZKTW5q - cmZaQ2JlaldaTWduQklocUZLT2FUcGcKLS0tIHlqVU0wdXJ0dTE4dlZSVEczd2Yv - RVFxVHFxbkVNbEZsaVcwYXZCdUc5R1kKQdAN6LEKmGLCSkKhNuEr0YK2zl9Aw1kK - 6C25lN532mG55zIRectZda1Fmi1GMZ/2v3b5qz7x+TDMA9m/47OjmA== - -----END AGE ENCRYPTED FILE----- - - recipient: age1ewes0f5snqx3sh5ul6fa6qtxzhd25829v6mf5rx2wnheat6fefps5rme2x - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoK3lqRDhEMXEvaUp3OWdV - eFlZSGpJcGs0RTdRbllWdmdZTzl3RTlDNlIwCm92R290NjNyK2NNbWpINTBhazNS - NTJYWEw0SGc1TUtrd0NZSmowakMvSlEKLS0tIG5uUEIrZGVORkRNVnBVOHgyMXZG - TTRWaHhpNWlkVDFmMFN4ZTNHMUxyNVkKV693pzTKRkZboQCMPr9IyMGSgxfuHXcb - Y6BNcp6Qg6PWtX5QI7wRkPNINAK1TEbRBba+b8h6gMmVU4DliQyFiQ== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-04-16T05:20:18Z" - mac: ENC[AES256_GCM,data:YqkxwV30uqSHhsn4niFEODxxl9R2ZuiyyX4g8zONVjMvdA52C08zPpxdxjtXnUT9m3sT7iSmWcJJZwhMhRIb8LJ2sdIJ4v+wpG9I4pPokhEXI2ozqbzw3k68GnZOzYu3kePQBJjQx1fmlM63dgILIwx7ytPnpm9arQ1rszZynNs=,iv:hxdhU5oH9h9mRH3m76oFkYVNA68PnivVJpJRjxSRtTw=,tag:Fyyg6cWPb96c/Vap+PifUQ==,type:str] - unencrypted_suffix: _unencrypted - version: 3.11.0