Compare commits
12 commits
main
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aaf0f0a2b | ||
|
|
5d8c897b4d | ||
|
|
3a6672cad9 | ||
|
|
69c6d85754 | ||
|
|
de1bc287d5 | ||
|
|
4bd4327a6d | ||
|
|
7e6beb208d | ||
|
|
cfb9d086b8 | ||
|
|
a1316fdf0e | ||
|
|
98362802d5 | ||
|
|
3921693f84 | ||
|
|
8228418b7f |
135 changed files with 950 additions and 10354 deletions
|
|
@ -1,6 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
2
.envrc
2
.envrc
|
|
@ -1,2 +0,0 @@
|
|||
# shellcheck shell=bash
|
||||
use flake
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
name: Test action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
kaas:
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Echo
|
||||
run: |
|
||||
nix --version
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
|||
* text=auto eol=lf
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
58
.jq/table.jq
58
.jq/table.jq
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
101
.just/users.just
101
.just/users.just
|
|
@ -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")"
|
||||
108
.just/vars.just
108
.just/vars.just
|
|
@ -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
|
||||
36
.justfile
36
.justfile
|
|
@ -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; }
|
||||
13
.sops.yaml
13
.sops.yaml
|
|
@ -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
|
||||
57
.sops.yml
Normal file
57
.sops.yml
Normal file
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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!)
|
||||
748
flake.lock
generated
748
flake.lock
generated
File diff suppressed because it is too large
Load diff
125
flake.nix
125
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
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
21
homes/x86_64-linux/chris@manwe/secrets.yaml
Normal file
21
homes/x86_64-linux/chris@manwe/secrets.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
24
justfile
Normal file
24
justfile
Normal file
|
|
@ -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"
|
||||
|
|
@ -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 {});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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}";
|
||||
};
|
||||
}
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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"}
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ in
|
|||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
home.packages = with pkgs; [
|
||||
# teamspeak3
|
||||
teamspeak6-client
|
||||
];
|
||||
home.packages = with pkgs; [ teamspeak_client ];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ in
|
|||
};
|
||||
|
||||
kwalletrc = {
|
||||
Wallet.Enabled = true;
|
||||
Wallet.Enabled = false;
|
||||
};
|
||||
|
||||
plasmarc = {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
digitalClock = {
|
||||
date = {
|
||||
enable = true;
|
||||
format.custom = "dd-MM-yyyy";
|
||||
format = "shortDate";
|
||||
position = "belowTime";
|
||||
};
|
||||
time = {
|
||||
|
|
@ -106,4 +106,4 @@
|
|||
}
|
||||
];
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
@ -15,7 +15,7 @@ in {
|
|||
programs.zed-editor = {
|
||||
enable = true;
|
||||
|
||||
extensions = [ "nix" "toml" "html" "just-ls" ];
|
||||
extensions = [ "nix" "toml" "html" ];
|
||||
|
||||
userSettings = {
|
||||
assistant.enabled = false;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ in
|
|||
};
|
||||
|
||||
amdgpu = {
|
||||
amdvlk = {
|
||||
enable = true;
|
||||
support32Bit.enable = true;
|
||||
};
|
||||
|
||||
initrd.enable = true;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
{ ... }:
|
||||
{
|
||||
config = {
|
||||
home-manager.backupFileExtension = "homeManagerBackup";
|
||||
};
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
1
modules/nixos/services/authentication/default.nix
Normal file
1
modules/nixos/services/authentication/default.nix
Normal file
|
|
@ -0,0 +1 @@
|
|||
{ ... }: {}
|
||||
|
|
@ -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 = [];
|
||||
};
|
||||
86
modules/nixos/services/authentication/zitadel.nix
Normal file
86
modules/nixos/services/authentication/zitadel.nix
Normal file
|
|
@ -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" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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" = {
|
||||
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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=<secret>, 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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
25
modules/nixos/services/games/palworld.nix
Normal file
25
modules/nixos/services/games/palworld.nix
Normal file
|
|
@ -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=";
|
||||
# });
|
||||
};
|
||||
}
|
||||
|
|
@ -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" = {};
|
||||
};
|
||||
}
|
||||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
|
|
@ -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=""
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
{lib, ...}: {
|
||||
}
|
||||
|
|
@ -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;});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
# };
|
||||
};
|
||||
}
|
||||
|
|
@ -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];
|
||||
};
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"title": "Default Dash",
|
||||
"description": "The default dashboard",
|
||||
"timezone": "browser",
|
||||
"editable": false,
|
||||
"panels": []
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
|
|
@ -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];
|
||||
};
|
||||
}
|
||||
|
|
@ -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];
|
||||
};
|
||||
}
|
||||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ in
|
|||
config = mkIf cfg.enable {
|
||||
virtualisation = {
|
||||
containers.enable = true;
|
||||
oci-containers.backend = "podman";
|
||||
|
||||
podman = {
|
||||
enable = true;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
{...}: {
|
||||
}
|
||||
{ ... }:
|
||||
{}
|
||||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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"
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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=
|
||||
|
|
@ -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, ", ")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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: "<list|search|add|monitor|remove> <movies|series> [...]",
|
||||
}, func(ctx *Context) {
|
||||
if len(ctx.Args) < 2 {
|
||||
ctx.Reply("Usage: `download <list|search|add|monitor|remove> <movies|series> [...]`")
|
||||
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 <query>`", 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 <query>`", 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 <id> <on|off>`", 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 <id>`", 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 <query>` 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 "🚫"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue