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
|
||||||
result-*
|
*.qcow2
|
||||||
|
|
||||||
# Ignore automatically generated direnv output
|
|
||||||
.direnv
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/)
|
- [dafitt/dotfiles](https://github.com/dafitt/dotfiles/)
|
||||||
- [khaneliman/khanelinix](https://github.com/khaneliman/khanelinix)
|
- [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!)
|
- [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
73
flake.nix
73
flake.nix
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
description = "Nixos config flake";
|
description = "Nixos config flake";
|
||||||
|
|
||||||
nixConfig = {
|
|
||||||
warn-dirty = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
|
@ -13,6 +9,11 @@
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
disko = {
|
||||||
|
url = "github:nix-community/disko";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
home-manager = {
|
home-manager = {
|
||||||
url = "github:nix-community/home-manager";
|
url = "github:nix-community/home-manager";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
@ -29,13 +30,13 @@
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
# neovim
|
nixos-wsl = {
|
||||||
nvf.url = "github:notashelf/nvf";
|
url = "github:nix-community/nixos-wsl";
|
||||||
|
inputs = {
|
||||||
# plymouth theme
|
nixpkgs.follows = "nixpkgs";
|
||||||
nixos-boot.url = "github:Melkor333/nixos-boot";
|
flake-compat.follows = "";
|
||||||
|
};
|
||||||
firefox.url = "github:nix-community/flake-firefox-nightly";
|
};
|
||||||
|
|
||||||
stylix.url = "github:nix-community/stylix";
|
stylix.url = "github:nix-community/stylix";
|
||||||
|
|
||||||
|
|
@ -45,10 +46,13 @@
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
zen-browser = {
|
# neovim
|
||||||
url = "github:0xc000022070/zen-browser-flake";
|
nvf.url = "github:notashelf/nvf";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
# plymouth theme
|
||||||
|
nixos-boot.url = "github:Melkor333/nixos-boot";
|
||||||
|
|
||||||
|
zen-browser.url = "github:MarceColl/zen-browser-flake";
|
||||||
|
|
||||||
nix-minecraft.url = "github:Infinidoge/nix-minecraft";
|
nix-minecraft.url = "github:Infinidoge/nix-minecraft";
|
||||||
|
|
||||||
|
|
@ -74,33 +78,9 @@
|
||||||
grub2-themes = {
|
grub2-themes = {
|
||||||
url = "github:vinceliuice/grub2-themes";
|
url = "github:vinceliuice/grub2-themes";
|
||||||
};
|
};
|
||||||
|
|
||||||
nixos-wsl = {
|
|
||||||
url = "github:nix-community/nixos-wsl";
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.follows = "nixpkgs";
|
|
||||||
flake-compat.follows = "";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
terranix = {
|
outputs = inputs: inputs.snowfall-lib.mkFlake {
|
||||||
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;
|
inherit inputs;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
|
|
@ -116,15 +96,8 @@
|
||||||
channels-config = {
|
channels-config = {
|
||||||
allowUnfree = true;
|
allowUnfree = true;
|
||||||
permittedInsecurePackages = [
|
permittedInsecurePackages = [
|
||||||
# Due to *arr stack
|
|
||||||
"dotnet-sdk-6.0.428"
|
"dotnet-sdk-6.0.428"
|
||||||
"aspnetcore-runtime-6.0.36"
|
"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"
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -134,13 +107,9 @@
|
||||||
flux.overlays.default
|
flux.overlays.default
|
||||||
];
|
];
|
||||||
|
|
||||||
systems.modules = with inputs; [
|
|
||||||
clan-core.nixosModules.default
|
|
||||||
];
|
|
||||||
|
|
||||||
homes.modules = with inputs; [
|
homes.modules = with inputs; [
|
||||||
stylix.homeModules.stylix
|
stylix.homeModules.stylix
|
||||||
plasma-manager.homeModules.plasma-manager
|
plasma-manager.homeManagerModules.plasma-manager
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
{osConfig, ...}: {
|
{ osConfig, ... }:
|
||||||
|
{
|
||||||
home.stateVersion = osConfig.system.stateVersion;
|
home.stateVersion = osConfig.system.stateVersion;
|
||||||
|
|
||||||
programs.git = {
|
programs.git = {
|
||||||
settings.user = {
|
userName = "Chris Kruining";
|
||||||
name = "Chris Kruining";
|
userEmail = "chris@kruining.eu";
|
||||||
email = "chris@kruining.eu";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sneeuwvlok = {
|
sneeuwvlok = {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
{osConfig, ...}: {
|
{ osConfig, ... }:
|
||||||
|
{
|
||||||
home.stateVersion = osConfig.system.stateVersion;
|
home.stateVersion = osConfig.system.stateVersion;
|
||||||
|
|
||||||
programs.git = {
|
programs.git = {
|
||||||
settings.user = {
|
userName = "Chris Kruining";
|
||||||
name = "Chris Kruining";
|
userEmail = "chris@kruining.eu";
|
||||||
email = "chris@kruining.eu";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sneeuwvlok = {
|
sneeuwvlok = {
|
||||||
|
|
@ -36,7 +35,6 @@
|
||||||
bitwarden.enable = true;
|
bitwarden.enable = true;
|
||||||
discord.enable = true;
|
discord.enable = true;
|
||||||
ladybird.enable = true;
|
ladybird.enable = true;
|
||||||
matrix.enable = true;
|
|
||||||
obs.enable = true;
|
obs.enable = true;
|
||||||
onlyoffice.enable = true;
|
onlyoffice.enable = true;
|
||||||
signal.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;
|
home.stateVersion = osConfig.system.stateVersion;
|
||||||
|
|
||||||
programs.git = {
|
programs.git = {
|
||||||
settings.user = {
|
userName = "Chris Kruining";
|
||||||
name = "Chris Kruining";
|
userEmail = "chris@kruining.eu";
|
||||||
email = "chris@kruining.eu";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sneeuwvlok = {
|
sneeuwvlok = {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
{osConfig, ...}: {
|
{ osConfig, ... }:
|
||||||
|
{
|
||||||
home.stateVersion = osConfig.system.stateVersion;
|
home.stateVersion = osConfig.system.stateVersion;
|
||||||
|
|
||||||
programs.git = {
|
programs.git = {
|
||||||
settings.user = {
|
userName = "Chris Kruining";
|
||||||
name = "Chris Kruining";
|
userEmail = "chris@kruining.eu";
|
||||||
email = "chris@kruining.eu";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sneeuwvlok = {
|
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, ... }:
|
||||||
inputs,
|
let
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
namespace,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkIf mkEnableOption;
|
inherit (lib) mkIf mkEnableOption;
|
||||||
|
|
||||||
cfg = config.${namespace}.application.onlyoffice;
|
cfg = config.${namespace}.application.onlyoffice;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
options.${namespace}.application.onlyoffice = {
|
options.${namespace}.application.onlyoffice = {
|
||||||
enable = mkEnableOption "enable onlyoffice";
|
enable = mkEnableOption "enable onlyoffice";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
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 {
|
config = mkIf cfg.enable {
|
||||||
home.packages = with pkgs; [ protonup-ng ];
|
home.packages = with pkgs; [ protonup ];
|
||||||
|
|
||||||
home.sessionVariables = {
|
home.sessionVariables = {
|
||||||
STEAM_EXTRA_COMPAT_TOOLS_PATHS = "\${HOME}/.steam/root/compatibilitytools.d";
|
STEAM_EXTRA_COMPAT_TOOLS_PATHS = "\${HOME}/.steam/root/compatibilitytools.d";
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,6 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
home.packages = with pkgs; [
|
home.packages = with pkgs; [ teamspeak_client ];
|
||||||
# teamspeak3
|
|
||||||
teamspeak6-client
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,61 +5,35 @@ let
|
||||||
cfg = config.${namespace}.application.zen;
|
cfg = config.${namespace}.application.zen;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
|
||||||
inputs.zen-browser.homeModules.default
|
|
||||||
];
|
|
||||||
|
|
||||||
options.${namespace}.application.zen = {
|
options.${namespace}.application.zen = {
|
||||||
enable = mkEnableOption "enable zen";
|
enable = mkEnableOption "enable zen";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
home.packages = [ inputs.zen-browser.packages.${pkgs.system}.specific ];
|
||||||
|
|
||||||
home.sessionVariables = {
|
home.sessionVariables = {
|
||||||
MOZ_ENABLE_WAYLAND = "1";
|
MOZ_ENABLE_WAYLAND = "1";
|
||||||
};
|
};
|
||||||
|
|
||||||
programs.zen-browser = {
|
programs.zen-browser = {
|
||||||
enable = true;
|
|
||||||
|
|
||||||
policies = {
|
policies = {
|
||||||
AutofillAddressEnabled = true;
|
AutofillAddressEnabled = true;
|
||||||
AutofillCreditCardEnabled = false;
|
AutofillCreditCardEnabled = false;
|
||||||
|
|
||||||
AppAutoUpdate = false;
|
|
||||||
DisableAppUpdate = true;
|
DisableAppUpdate = true;
|
||||||
ManualAppUpdateOnly = true;
|
|
||||||
|
|
||||||
DisableFeedbackCommands = true;
|
DisableFeedbackCommands = true;
|
||||||
DisableFirefoxStudies = true;
|
DisableFirefoxStudies = true;
|
||||||
DisablePocket = true;
|
DisablePocket = true;
|
||||||
DisableTelemetry = true;
|
DisableTelemetry = true;
|
||||||
|
# DontCheckDefaultBrowser = false;
|
||||||
DontCheckDefaultBrowser = false;
|
|
||||||
NoDefaultBookmarks = true;
|
NoDefaultBookmarks = true;
|
||||||
OfferToSaveLogins = false;
|
# OfferToSaveLogins = false;
|
||||||
EnableTrackingProtection = {
|
EnableTrackingProtection = {
|
||||||
Value = true;
|
Value = true;
|
||||||
Locked = true;
|
Locked = true;
|
||||||
Cryptomining = true;
|
Cryptomining = true;
|
||||||
Fingerprinting = 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 = {
|
kwalletrc = {
|
||||||
Wallet.Enabled = true;
|
Wallet.Enabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
plasmarc = {
|
plasmarc = {
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
digitalClock = {
|
digitalClock = {
|
||||||
date = {
|
date = {
|
||||||
enable = true;
|
enable = true;
|
||||||
format.custom = "dd-MM-yyyy";
|
format = "shortDate";
|
||||||
position = "belowTime";
|
position = "belowTime";
|
||||||
};
|
};
|
||||||
time = {
|
time = {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ in {
|
||||||
programs.zed-editor = {
|
programs.zed-editor = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
extensions = [ "nix" "toml" "html" "just-ls" ];
|
extensions = [ "nix" "toml" "html" ];
|
||||||
|
|
||||||
userSettings = {
|
userSettings = {
|
||||||
assistant.enabled = false;
|
assistant.enabled = false;
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ let
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
systemd.user.startServices = "sd-switch";
|
systemd.user.startServices = "sd-switch";
|
||||||
programs.home-manager = {
|
programs.home-manager.enable = true;
|
||||||
enable = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
home.stateVersion = mkDefault (osConfig.system.stateVersion or "25.05");
|
home.stateVersion = mkDefault (osConfig.system.stateVersion or "25.05");
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,6 @@ in
|
||||||
eza.enable = true;
|
eza.enable = true;
|
||||||
fzf.enable = true;
|
fzf.enable = true;
|
||||||
git.enable = true;
|
git.enable = true;
|
||||||
just.enable = true;
|
|
||||||
starship.enable = true;
|
starship.enable = true;
|
||||||
tmux.enable = true;
|
tmux.enable = true;
|
||||||
yazi.enable = true;
|
yazi.enable = true;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
{
|
{ config, lib, pkgs, namespace, ... }:
|
||||||
config,
|
let
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
namespace,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkEnableOption mkIf;
|
inherit (lib) mkEnableOption mkIf;
|
||||||
|
|
||||||
cfg = config.${namespace}.shell.toolset.git;
|
cfg = config.${namespace}.shell.toolset.git;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
options.${namespace}.shell.toolset.git = {
|
options.${namespace}.shell.toolset.git = {
|
||||||
enable = mkEnableOption "version-control system";
|
enable = mkEnableOption "version-control system";
|
||||||
};
|
};
|
||||||
|
|
@ -33,6 +29,12 @@ in {
|
||||||
git = {
|
git = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.gitFull;
|
package = pkgs.gitFull;
|
||||||
|
difftastic = {
|
||||||
|
enable = true;
|
||||||
|
background = "dark";
|
||||||
|
color = "always";
|
||||||
|
display = "inline";
|
||||||
|
};
|
||||||
|
|
||||||
ignores = [
|
ignores = [
|
||||||
# General:
|
# General:
|
||||||
|
|
@ -65,7 +67,7 @@ in {
|
||||||
"*.elc"
|
"*.elc"
|
||||||
];
|
];
|
||||||
|
|
||||||
settings = {
|
extraConfig = {
|
||||||
init.defaultBranch = "main";
|
init.defaultBranch = "main";
|
||||||
core = {
|
core = {
|
||||||
editor = "nvim";
|
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 ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -31,9 +31,7 @@ in {
|
||||||
base16Scheme = "${pkgs.base16-schemes}/share/themes/${cfg.theme}.yaml";
|
base16Scheme = "${pkgs.base16-schemes}/share/themes/${cfg.theme}.yaml";
|
||||||
image = ./${cfg.theme}.jpg;
|
image = ./${cfg.theme}.jpg;
|
||||||
polarity = cfg.polarity;
|
polarity = cfg.polarity;
|
||||||
|
targets.qt.platform = mkDefault "kde6";
|
||||||
targets.qt.platform = mkDefault "kde";
|
|
||||||
targets.zen-browser.profileNames = [ "Chris" ];
|
|
||||||
|
|
||||||
fonts = {
|
fonts = {
|
||||||
serif = {
|
serif = {
|
||||||
|
|
@ -52,7 +50,7 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
emoji = {
|
emoji = {
|
||||||
package = pkgs.noto-fonts-color-emoji;
|
package = pkgs.noto-fonts-emoji;
|
||||||
name = "Noto Color Emoji";
|
name = "Noto Color Emoji";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,37 @@
|
||||||
{
|
{ inputs, config, lib, pkgs, namespace, ... }:
|
||||||
inputs,
|
let
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
namespace,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkIf mkEnableOption;
|
inherit (lib) mkIf mkEnableOption;
|
||||||
|
|
||||||
cfg = config.${namespace}.application.steam;
|
cfg = config.${namespace}.application.steam;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
options.${namespace}.application.steam = {
|
options.${namespace}.application.steam = {
|
||||||
enable = mkEnableOption "enable steam";
|
enable = mkEnableOption "enable steam";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
# environment.systemPackages = with pkgs; [ steam ];
|
|
||||||
|
|
||||||
programs = {
|
programs = {
|
||||||
steam = {
|
steam = {
|
||||||
enable = true;
|
enable = true;
|
||||||
remotePlay.openFirewall = true;
|
package = pkgs.steam-small.override {
|
||||||
dedicatedServer.openFirewall = true;
|
extraEnv = {
|
||||||
localNetworkGameTransfers.openFirewall = true;
|
DXVK_HUD = "compiler";
|
||||||
|
MANGOHUD = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
extraCompatPackages = with pkgs; [
|
gamescopeSession = {
|
||||||
proton-ge-bin
|
enable = true;
|
||||||
];
|
args = ["--immediate-flips"];
|
||||||
|
};
|
||||||
# package = pkgs.steam.override {
|
|
||||||
# extraEnv = {
|
|
||||||
# DXVK_HUD = "compiler";
|
|
||||||
# MANGOHUD = true;
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
|
|
||||||
# gamescopeSession = {
|
|
||||||
# enable = true;
|
|
||||||
# args = ["--immediate-flips"];
|
|
||||||
# };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# https://github.com/FeralInteractive/gamemode
|
# https://github.com/FeralInteractive/gamemode
|
||||||
# gamemode = {
|
gamemode = {
|
||||||
# enable = true;
|
enable = true;
|
||||||
# enableRenice = true;
|
enableRenice = true;
|
||||||
# settings = {};
|
settings = {};
|
||||||
# };
|
};
|
||||||
|
|
||||||
# gamescope = {
|
# gamescope = {
|
||||||
# enable = true;
|
# 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, ... }:
|
||||||
lib,
|
let
|
||||||
config,
|
|
||||||
namespace,
|
|
||||||
inputs,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkIf mkOption mkEnableOption mkMerge;
|
inherit (lib) mkIf mkOption mkEnableOption mkMerge;
|
||||||
inherit (lib.types) nullOr enum;
|
inherit (lib.types) nullOr enum;
|
||||||
|
|
||||||
cfg = config.${namespace}.desktop;
|
cfg = config.${namespace}.desktop;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
inputs.grub2-themes.nixosModules.default
|
inputs.grub2-themes.nixosModules.default
|
||||||
];
|
];
|
||||||
|
|
||||||
options.${namespace}.desktop = {
|
options.${namespace}.desktop = {
|
||||||
use = mkOption {
|
use = mkOption {
|
||||||
type = nullOr (enum ["plasma" "gamescope" "gnome" "cosmic"]);
|
type = nullOr (enum [ "plasma" "gamescope" "gnome" ]);
|
||||||
default = null;
|
default = null;
|
||||||
example = "plasma";
|
example = "plasma";
|
||||||
description = "Which desktop to enable";
|
description = "Which desktop to enable";
|
||||||
|
|
@ -24,11 +20,11 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
{
|
({
|
||||||
services.displayManager = {
|
services.displayManager = {
|
||||||
enable = true;
|
enable = true;
|
||||||
};
|
};
|
||||||
}
|
})
|
||||||
|
|
||||||
# (mkIf (cfg.use != null) {
|
# (mkIf (cfg.use != null) {
|
||||||
# ${namespace}.desktop.${cfg.use}.enable = true;
|
# ${namespace}.desktop.${cfg.use}.enable = true;
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,7 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
environment.plasma6.excludePackages = with pkgs.kdePackages; [
|
environment.plasma6.excludePackages = with pkgs.kdePackages; [ konsole kate ghostwriter oxygen ];
|
||||||
elisa
|
|
||||||
kmahjongg
|
|
||||||
kmines
|
|
||||||
konversation
|
|
||||||
kpat
|
|
||||||
ksudoku
|
|
||||||
konsole
|
|
||||||
kate
|
|
||||||
ghostwriter
|
|
||||||
# oxygen
|
|
||||||
];
|
|
||||||
environment.sessionVariables.NIXOS_OZONE_WL = "1";
|
environment.sessionVariables.NIXOS_OZONE_WL = "1";
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
amdgpu = {
|
amdgpu = {
|
||||||
|
amdvlk = {
|
||||||
|
enable = true;
|
||||||
|
support32Bit.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
initrd.enable = true;
|
initrd.enable = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{ ... }:
|
|
||||||
{
|
|
||||||
config = {
|
|
||||||
home-manager.backupFileExtension = "homeManagerBackup";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
{ pkgs, lib, namespace, config, ... }:
|
{ pkgs, lib, namespace, config, ... }:
|
||||||
let
|
let
|
||||||
|
inherit (lib) mkIf mkEnableOption;
|
||||||
|
|
||||||
cfg = config.${namespace}.nix;
|
cfg = config.${namespace}.nix;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.${namespace}.nix = {};
|
options.${namespace}.nix = {
|
||||||
|
enable = mkEnableOption "Enable nix command";
|
||||||
|
};
|
||||||
|
|
||||||
config = {
|
config = mkIf cfg.enable {
|
||||||
programs.git.enable = true;
|
programs.git.enable = true;
|
||||||
|
|
||||||
nix = {
|
nix = {
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,16 @@
|
||||||
{
|
{ config, lib, pkgs, namespace, ... }:
|
||||||
config,
|
let
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
namespace,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkIf mkEnableOption;
|
inherit (lib) mkIf mkEnableOption;
|
||||||
|
|
||||||
user = "authelia-testing";
|
user = "authelia-testing";
|
||||||
cfg = config.${namespace}.services.authentication.authelia;
|
cfg = config.${namespace}.services.authentication.authelia;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
options.${namespace}.services.authentication.authelia = {
|
options.${namespace}.services.authentication.authelia = {
|
||||||
enable = mkEnableOption "Authelia";
|
enable = mkEnableOption "Authelia";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
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; [
|
environment.systemPackages = with pkgs; [
|
||||||
authelia
|
authelia
|
||||||
];
|
];
|
||||||
|
|
@ -150,23 +130,6 @@ in {
|
||||||
scopes = [ "offline_access" "openid" "email" "picture" "profile" "groups" ];
|
scopes = [ "offline_access" "openid" "email" "picture" "profile" "groups" ];
|
||||||
redirect_uris = [ "http://localhost:3000/api/auth/oauth2/callback/authelia" ];
|
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"];
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -215,8 +178,48 @@ in {
|
||||||
- jellyfin-users
|
- jellyfin-users
|
||||||
- admin
|
- admin
|
||||||
- dev
|
- 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,14 +1,9 @@
|
||||||
{
|
{ inputs, lib, config, namespace, ... }: let
|
||||||
inputs,
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
namespace,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkEnableOption mkIf;
|
inherit (lib) mkEnableOption mkIf;
|
||||||
|
|
||||||
cfg = config.${namespace}.services.authentication.himmelblau;
|
cfg = config.${namespace}.services.authentication.himmelblau;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
imports = [ inputs.himmelblau.nixosModules.himmelblau ];
|
imports = [ inputs.himmelblau.nixosModules.himmelblau ];
|
||||||
|
|
||||||
options.${namespace}.services.authentication.himmelblau = {
|
options.${namespace}.services.authentication.himmelblau = {
|
||||||
|
|
@ -19,7 +14,7 @@ in {
|
||||||
services.himmelblau = {
|
services.himmelblau = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
settings = {
|
||||||
domain = "";
|
domains = [];
|
||||||
pam_allow_groups = [];
|
pam_allow_groups = [];
|
||||||
local_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, ... }:
|
||||||
inputs,
|
let
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
namespace,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkIf mkEnableOption mkOption;
|
inherit (lib) mkIf mkEnableOption mkOption;
|
||||||
inherit (lib.types) str;
|
inherit (lib.types) str;
|
||||||
|
|
||||||
cfg = config.${namespace}.services.games.minecraft;
|
cfg = config.${namespace}.services.games.minecraft;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
inputs.nix-minecraft.nixosModules.minecraft-servers
|
inputs.nix-minecraft.nixosModules.minecraft-servers
|
||||||
];
|
];
|
||||||
|
|
@ -30,7 +25,7 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
users.users.${cfg.user} = {
|
user.users.${cfg.user} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = cfg.group;
|
group = cfg.group;
|
||||||
};
|
};
|
||||||
|
|
@ -108,14 +103,8 @@ in {
|
||||||
inherit (pkgs) linkFarmFromDrvs fetchurl;
|
inherit (pkgs) linkFarmFromDrvs fetchurl;
|
||||||
in {
|
in {
|
||||||
mods = linkFarmFromDrvs "mods" (attrValues {
|
mods = linkFarmFromDrvs "mods" (attrValues {
|
||||||
FabricApi = fetchurl {
|
FabricApi = fetchurl { url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/ZNwYCTsk/fabric-api-0.118.0%2B1.21.4.jar"; sha512 = "1e0d31b6663dc2c7be648f3a5a9cf7b698b9a0fd0f7ae16d1d3f32d943d7c5205ff63a4f81b0c4e94a8997482cce026b7ca486e99d9ce35ac069aeb29b02a30d"; };
|
||||||
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/ZNwYCTsk/fabric-api-0.118.0%2B1.21.4.jar";
|
Terralith = fetchurl { url = "https://cdn.modrinth.com/data/8oi3bsk5/versions/MuJMtPGQ/Terralith_1.21.x_v2.5.8.jar"; sha512 = "f862ed5435ce4c11a97d2ea5c40eee9f817c908f3223b5fd3e3fff0562a55111d7429dc73a2f1ca0b1af7b1ff6fa0470ed6efebb5de13336c40bb70fb357dd60"; };
|
||||||
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"; };
|
# DistantHorizons = fetchurl { url = "https://cdn.modrinth.com/data/uCdwusMi/versions/jptcCdp2/DistantHorizons-2.2.1-a-1.20.4-forge-fabric.jar"; sha512 = "47368d91099d0b5f364339a69f4e425f8fb1e3a7c3250a8b649da76135e68a22f1a76b191c87e15a5cdc0a1d36bc57f2fa825490d96711d09d96807be97d575d"; };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -158,16 +147,13 @@ in {
|
||||||
inherit (lib) concatMapAttrs;
|
inherit (lib) concatMapAttrs;
|
||||||
|
|
||||||
readDirRec = src: dir: fn:
|
readDirRec = src: dir: fn:
|
||||||
concatMapAttrs (
|
concatMapAttrs (name: type: if type == "directory"
|
||||||
name: type:
|
|
||||||
if type == "directory"
|
|
||||||
then (readDirRec src "${dir}/${name}" fn)
|
then (readDirRec src "${dir}/${name}" fn)
|
||||||
else {"${dir}/${name}" = fn "${dir}/${name}";}
|
else { "${dir}/${name}" = (fn "${dir}/${name}"); }
|
||||||
) (readDir "${src}/${dir}");
|
) (readDir "${src}/${dir}");
|
||||||
|
|
||||||
copyDir = dir: readDirRec src dir (x: "${src}/${x}");
|
copyDir = dir: readDirRec src dir (x: "${src}/${x}");
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
"ops.json" = {
|
"ops.json" = {
|
||||||
value = ops;
|
value = ops;
|
||||||
};
|
};
|
||||||
|
|
@ -178,11 +164,7 @@ in {
|
||||||
inherit (builtins) attrNames readDir map;
|
inherit (builtins) attrNames readDir map;
|
||||||
inherit (pkgs) linkFarm;
|
inherit (pkgs) linkFarm;
|
||||||
|
|
||||||
linkFarmFromDir = name: dir:
|
linkFarmFromDir = name: dir: linkFarm name (map (x: { name = x; path = "${src}/${dir}/${x}"; }) (attrNames (readDir "${src}/${dir}")));
|
||||||
linkFarm name (map (x: {
|
|
||||||
name = x;
|
|
||||||
path = "${src}/${dir}/${x}";
|
|
||||||
}) (attrNames (readDir "${src}/${dir}")));
|
|
||||||
in {
|
in {
|
||||||
Deftu = linkFarmFromDir "tekxit-deftu" "Deftu";
|
Deftu = linkFarmFromDir "tekxit-deftu" "Deftu";
|
||||||
TKXAddons = linkFarmFromDir "tekxit-TKXAddons" "TKXAddons";
|
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, ... }:
|
||||||
pkgs,
|
let
|
||||||
lib,
|
|
||||||
namespace,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkIf mkEnableOption mkOption;
|
inherit (lib) mkIf mkEnableOption mkOption;
|
||||||
inherit (lib.types) str;
|
inherit (lib.types) str;
|
||||||
|
|
||||||
cfg = config.${namespace}.services.media;
|
cfg = config.${namespace}.services.media;
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
options.${namespace}.services.media = {
|
options.${namespace}.services.media = {
|
||||||
enable = mkEnableOption "Enable media services";
|
enable = mkEnableOption "Enable media services";
|
||||||
|
|
||||||
|
|
@ -35,6 +31,13 @@ in {
|
||||||
#=========================================================================
|
#=========================================================================
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
podman-tui
|
podman-tui
|
||||||
|
jellyfin
|
||||||
|
jellyfin-web
|
||||||
|
jellyfin-ffmpeg
|
||||||
|
jellyseerr
|
||||||
|
mediainfo
|
||||||
|
id3v2
|
||||||
|
yt-dlp
|
||||||
];
|
];
|
||||||
|
|
||||||
#=========================================================================
|
#=========================================================================
|
||||||
|
|
@ -49,27 +52,105 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d '${cfg.path}/qbittorrent' 0770 ${cfg.user} ${cfg.group} - -"
|
"d '${cfg.path}/series' 0700 ${cfg.user} ${cfg.group} - -"
|
||||||
"d '${cfg.path}/sabnzbd' 0770 ${cfg.user} ${cfg.group} - -"
|
"d '${cfg.path}/movies' 0700 ${cfg.user} ${cfg.group} - -"
|
||||||
"d '${cfg.path}/downloads/incomplete' 0770 ${cfg.user} ${cfg.group} - -"
|
"d '${cfg.path}/music' 0700 ${cfg.user} ${cfg.group} - -"
|
||||||
"d '${cfg.path}/downloads/done' 0770 ${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
|
||||||
#=========================================================================
|
#=========================================================================
|
||||||
services = {
|
services = let
|
||||||
bazarr = {
|
serviceConf = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
user = cfg.user;
|
user = cfg.user;
|
||||||
group = cfg.group;
|
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;
|
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, ... }:
|
||||||
config,
|
let
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
namespace,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (lib) mkIf mkEnableOption mkOption;
|
inherit (lib) mkIf mkEnableOption mkOption;
|
||||||
inherit (lib.types) str;
|
inherit (lib.types) str;
|
||||||
|
|
||||||
cfg = config.${namespace}.services.media.nextcloud;
|
cfg = config.${namespace}.services.media.nextcloud;
|
||||||
in {
|
in
|
||||||
options.${namespace}.services.media.nextcloud = {
|
{
|
||||||
|
options.modules.services.nextcloud = {
|
||||||
enable = mkEnableOption "Nextcloud";
|
enable = mkEnableOption "Nextcloud";
|
||||||
|
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
|
|
@ -25,14 +21,6 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
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 = {
|
||||||
users.${cfg.user} = {
|
users.${cfg.user} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
|
|
@ -52,7 +40,7 @@ in {
|
||||||
|
|
||||||
services.nextcloud = {
|
services.nextcloud = {
|
||||||
enable = true;
|
enable = true;
|
||||||
# webserver = "caddy";
|
webserver = "caddy";
|
||||||
package = pkgs.nextcloud31;
|
package = pkgs.nextcloud31;
|
||||||
hostName = "localhost";
|
hostName = "localhost";
|
||||||
|
|
||||||
|
|
@ -87,5 +75,14 @@ in {
|
||||||
|
|
||||||
# startServices = true;
|
# 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
|
let
|
||||||
inherit (lib) mkIf mkEnableOption;
|
inherit (lib) mkIf mkEnableOption;
|
||||||
|
|
||||||
cfg = config.${namespace}.services.media.nfs;
|
cfg = config.${namespace}.media.nfs;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.${namespace}.services.media.nfs = {
|
options.${namespace}.media.nfs = {
|
||||||
enable = mkEnableOption "Enable 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, ... }:
|
||||||
pkgs,
|
let
|
||||||
config,
|
inherit (lib.modules) mkIf;
|
||||||
lib,
|
inherit (lib.options) mkEnableOption;
|
||||||
namespace,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
inherit (builtins) toString;
|
|
||||||
inherit (lib) mkIf mkEnableOption mkOption types getAttrs toUpper concatMapAttrsStringSep;
|
|
||||||
|
|
||||||
cfg = config.${namespace}.services.security.vaultwarden;
|
cfg = config.${namespace}.services.security.vaultwarden;
|
||||||
|
in
|
||||||
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 {
|
|
||||||
options.${namespace}.services.security.vaultwarden = {
|
options.${namespace}.services.security.vaultwarden = {
|
||||||
enable = mkEnableOption "enable 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 {
|
config = mkIf cfg.enable {
|
||||||
${namespace}.services.networking.caddy.hosts = {
|
environment.systemPackages = with pkgs; [
|
||||||
"vault.kruining.eu" = ''
|
vaultwarden
|
||||||
encode zstd gzip
|
vaultwarden-postgresql
|
||||||
|
|
||||||
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 - -"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
# systemd.services.vaultwarden.wants = [ "zitadelApplyTerraform.service" ];
|
services.vaultwarden = {
|
||||||
|
|
||||||
services = {
|
|
||||||
vaultwarden = {
|
|
||||||
enable = true;
|
enable = true;
|
||||||
dbBackend = "postgresql";
|
dbBackend = "postgresql";
|
||||||
|
|
||||||
package = pkgs.vaultwarden-postgresql;
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
SIGNUPS_ALLOWED = false;
|
SIGNUPS_ALLOWED = false;
|
||||||
DOMAIN = "https://vault.kruining.eu";
|
DOMAIN = "https://passwords.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}
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ in
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
virtualisation = {
|
virtualisation = {
|
||||||
containers.enable = true;
|
containers.enable = true;
|
||||||
oci-containers.backend = "podman";
|
|
||||||
|
|
||||||
podman = {
|
podman = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
{...}: {
|
{ ... }:
|
||||||
}
|
{}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{ pkgs, config, namespace, inputs, system, ... }:
|
{ pkgs, config, namespace, inputs, ... }:
|
||||||
let
|
let
|
||||||
cfg = config.${namespace}.system.security.sops;
|
cfg = config.${namespace}.system.security.sops;
|
||||||
in
|
in
|
||||||
|
|
@ -13,14 +13,10 @@ in
|
||||||
environment.systemPackages = with pkgs; [ sops ];
|
environment.systemPackages = with pkgs; [ sops ];
|
||||||
|
|
||||||
sops = {
|
sops = {
|
||||||
defaultSopsFormat = "yaml";
|
age.keyFile = "/home/.sops-key.age";
|
||||||
defaultSopsFile = inputs.self + "/systems/${system}/${config.networking.hostName}/secrets.yml";
|
|
||||||
|
|
||||||
age = {
|
defaultSopsFile = ../../../../systems/x86_64-linux/${config.networking.hostName}/secrets.yaml;
|
||||||
# keyFile = "~/.config/sops/age/keys.txt";
|
defaultSopsFormat = "yaml";
|
||||||
# sshKeyPaths = [ "~/.ssh/id_ed25519" ];
|
|
||||||
# generateKey = true;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -14,8 +14,9 @@ in
|
||||||
|
|
||||||
sudo-rs = {
|
sudo-rs = {
|
||||||
enable = true;
|
enable = true;
|
||||||
execWheelOnly = true;
|
extraConfig = ''
|
||||||
extraConfig = ''Defaults env_keep += "EDITOR PATH DISPLAY"'';
|
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