diff options
-rw-r--r-- | custom/notify-user.hs | 50 | ||||
-rw-r--r-- | custom/notify-user.nix | 69 | ||||
-rw-r--r-- | custom/notify-users.nix | 53 | ||||
-rw-r--r-- | custom/recv-media.sh | 56 | ||||
-rw-r--r-- | custom/uucp-mediaclient.nix | 89 | ||||
-rw-r--r-- | hel.nix | 22 | ||||
-rw-r--r-- | hel/recv-media.nix | 72 |
7 files changed, 259 insertions, 152 deletions
diff --git a/custom/notify-user.hs b/custom/notify-user.hs new file mode 100644 index 00000000..f9cc2369 --- /dev/null +++ b/custom/notify-user.hs | |||
@@ -0,0 +1,50 @@ | |||
1 | {-# LANGUAGE ViewPatterns, StandaloneDeriving #-} | ||
2 | |||
3 | import System.FilePath.Glob (glob) | ||
4 | import System.Environment (setEnv, getArgs) | ||
5 | import System.Process (spawnProcess, waitForProcess) | ||
6 | import System.Exit (exitWith, ExitCode(..)) | ||
7 | |||
8 | import Data.List (isPrefixOf, dropWhile, dropWhileEnd, init) | ||
9 | import Data.Char (isSpace, toLower, toUpper) | ||
10 | |||
11 | import Control.Monad (forM_, void) | ||
12 | |||
13 | import qualified Libnotify as Notify | ||
14 | import Data.Monoid | ||
15 | |||
16 | import System.Console.GetOpt.Simple | ||
17 | |||
18 | import qualified Data.Map as Map | ||
19 | |||
20 | import Data.Maybe | ||
21 | import Text.Read (readMaybe) | ||
22 | |||
23 | deriving instance Read Notify.Urgency | ||
24 | |||
25 | main :: IO () | ||
26 | main = do | ||
27 | envFiles <- glob "@userHome@/.dbus/session-bus/*" | ||
28 | forM_ envFiles $ \envFile -> do | ||
29 | sessionAddr <- unQuote . tail . snd . break (== '=') . head . filter ("DBUS_SESSION_BUS_ADDRESS=" `isPrefixOf`) . lines <$> readFile envFile | ||
30 | setEnv "DBUS_SESSION_BUS_ADDRESS" sessionAddr | ||
31 | lines <- lines <$> getContents | ||
32 | case lines of | ||
33 | ((trim -> summary):(trim . unlines -> contents)) -> do | ||
34 | (opts, _) <- flip getUsingConf [] [ (arg, "urgency", Optional, "") | ||
35 | , (arg, "app-name", Optional, "") | ||
36 | , (arg, "category", Optional, "") | ||
37 | ] | ||
38 | let | ||
39 | urgency = fromMaybe Notify.Normal $ readMaybe . caseForRead =<< Map.lookup "urgency" opts | ||
40 | appName = fromMaybe "notify-@userName@" $ Map.lookup "app-name" opts | ||
41 | category = fromMaybe "" $ Map.lookup "category" opts | ||
42 | Notify.display_ $ Notify.summary summary <> Notify.body contents <> Notify.appName appName <> Notify.urgency urgency <> Notify.category category | ||
43 | _ -> exitWith $ ExitFailure 2 | ||
44 | where | ||
45 | trim = dropWhileEnd isSpace . dropWhile isSpace | ||
46 | unQuote ('\'':xs) = init xs | ||
47 | unQuote ('"':xs) = init xs | ||
48 | unQuote xs = xs | ||
49 | caseForRead [] = [] | ||
50 | caseForRead (x:xs) = toUpper x : map toLower xs | ||
diff --git a/custom/notify-user.nix b/custom/notify-user.nix deleted file mode 100644 index e9e98ddc..00000000 --- a/custom/notify-user.nix +++ /dev/null | |||
@@ -1,69 +0,0 @@ | |||
1 | { stdenv, writeTextFile | ||
2 | , ghcWithPackages | ||
3 | , user ? "gkleen" | ||
4 | # , libnotify | ||
5 | }: | ||
6 | |||
7 | stdenv.mkDerivation { | ||
8 | name = ''notify-${user}''; | ||
9 | src = writeTextFile { name = ''notify-${user}.hs''; text = '' | ||
10 | {-# LANGUAGE ViewPatterns, StandaloneDeriving #-} | ||
11 | |||
12 | import System.FilePath.Glob (glob) | ||
13 | import System.Environment (setEnv, getArgs) | ||
14 | import System.Process (spawnProcess, waitForProcess) | ||
15 | import System.Exit (exitWith, ExitCode(..)) | ||
16 | |||
17 | import Data.List (isPrefixOf, dropWhile, dropWhileEnd, init) | ||
18 | import Data.Char (isSpace, toLower, toUpper) | ||
19 | |||
20 | import Control.Monad (forM_, void) | ||
21 | |||
22 | import qualified Libnotify as Notify | ||
23 | import Data.Monoid | ||
24 | |||
25 | import System.Console.GetOpt.Simple | ||
26 | |||
27 | import qualified Data.Map as Map | ||
28 | |||
29 | import Data.Maybe | ||
30 | import Text.Read (readMaybe) | ||
31 | |||
32 | deriving instance Read Notify.Urgency | ||
33 | |||
34 | main = do | ||
35 | envFiles <- glob "/home/${user}/.dbus/session-bus/*" | ||
36 | forM_ envFiles $ \envFile -> do | ||
37 | sessionAddr <- unQuote . tail . snd . break (== '=') . head . filter ("DBUS_SESSION_BUS_ADDRESS=" `isPrefixOf`) . lines <$> readFile envFile | ||
38 | setEnv "DBUS_SESSION_BUS_ADDRESS" sessionAddr | ||
39 | lines <- lines <$> getContents | ||
40 | case lines of | ||
41 | ((trim -> summary):(trim . unlines -> contents)) -> do | ||
42 | (opts, _) <- flip getUsingConf [] [ (arg, "urgency", Optional, "") | ||
43 | , (arg, "app-name", Optional, "") | ||
44 | , (arg, "category", Optional, "") | ||
45 | ] | ||
46 | let | ||
47 | urgency = fromMaybe Notify.Normal $ readMaybe . caseForRead =<< Map.lookup "urgency" opts | ||
48 | appName = fromMaybe "notify-${user}" $ Map.lookup "app-name" opts | ||
49 | category = fromMaybe "" $ Map.lookup "category" opts | ||
50 | Notify.display_ $ Notify.summary summary <> Notify.body contents <> Notify.appName appName <> Notify.urgency urgency <> Notify.category category | ||
51 | _ -> exitWith $ ExitFailure 2 | ||
52 | where | ||
53 | trim = dropWhileEnd isSpace . dropWhile isSpace | ||
54 | unQuote ('\''':xs) = init xs | ||
55 | unQuote ('"':xs) = init xs | ||
56 | unQuote xs = xs | ||
57 | caseForRead [] = [] | ||
58 | caseForRead (x:xs) = toUpper x : map toLower xs | ||
59 | ''; }; | ||
60 | phases = [ "buildPhase" "installPhase" ]; | ||
61 | buildPhase = '' | ||
62 | ${ghcWithPackages (p: with p; [ Glob process libnotify getopt-simple containers ])}/bin/ghc -odir . -hidir . $src -o notify-${user} | ||
63 | ''; | ||
64 | installPhase = '' | ||
65 | mkdir -p $out/bin | ||
66 | cp notify-${user} $out/bin | ||
67 | chmod +x $out/bin/notify-${user} | ||
68 | ''; | ||
69 | } | ||
diff --git a/custom/notify-users.nix b/custom/notify-users.nix new file mode 100644 index 00000000..e68b0be2 --- /dev/null +++ b/custom/notify-users.nix | |||
@@ -0,0 +1,53 @@ | |||
1 | { config, lib, pkgs, ... }: | ||
2 | |||
3 | with lib; | ||
4 | |||
5 | let | ||
6 | cfg = config.services.notify-users; | ||
7 | |||
8 | notify-user = userName: with pkgs; stdenv.mkDerivation { | ||
9 | name = "notify-${userName}"; | ||
10 | src = ./notify-user.hs; | ||
11 | |||
12 | phases = [ "unpackPhase" "buildPhase" "installPhase" ]; | ||
13 | |||
14 | unpackPhase = '' | ||
15 | cp $src notify-user.hs | ||
16 | ''; | ||
17 | |||
18 | inherit userName; | ||
19 | userHome = config.users.users."${userName}".home; | ||
20 | |||
21 | buildPhase = '' | ||
22 | substituteAllInPlace notify-user.hs | ||
23 | ${ghcWithPackages (p: with p; [ Glob process libnotify getopt-simple containers ])}/bin/ghc -odir . -hidir . $src -o notify-${userName} | ||
24 | ''; | ||
25 | |||
26 | installPhase = '' | ||
27 | mkdir -p $out/bin | ||
28 | |||
29 | install -m 755 -t $out/bin \ | ||
30 | notify-${userName} | ||
31 | ''; | ||
32 | }; | ||
33 | in { | ||
34 | options = { | ||
35 | services.notify-users = mkOption { | ||
36 | type = with types; listOf str; | ||
37 | default = []; | ||
38 | description = '' | ||
39 | Users to install a notify-user script for | ||
40 | ''; | ||
41 | }; | ||
42 | }; | ||
43 | |||
44 | config = mkIf (cfg != []) { | ||
45 | security.wrappers = listToAttrs (map (user: nameValuePair "notify-${user}" { | ||
46 | owner = user; | ||
47 | setuid = true; | ||
48 | setgid = false; | ||
49 | permissions = "u+rx,g+x,o+x"; | ||
50 | source = "${notify-user user}/bin/notify-${user}"; | ||
51 | }) cfg); | ||
52 | }; | ||
53 | } | ||
diff --git a/custom/recv-media.sh b/custom/recv-media.sh new file mode 100644 index 00000000..29e3d158 --- /dev/null +++ b/custom/recv-media.sh | |||
@@ -0,0 +1,56 @@ | |||
1 | #!@zsh@/bin/zsh | ||
2 | |||
3 | pid=$? | ||
4 | |||
5 | exec 1> >(@utillinux@/bin/logger --id=$pid -t recv-media -p user.notice) | ||
6 | exec 2> >(@utillinux@/bin/logger --id=$pid -t recv-media -p user.error) | ||
7 | |||
8 | [[ -z "$1" || -z "$2" ]] && exit 2 | ||
9 | |||
10 | dir=@mediaDir@ | ||
11 | group=$(@coreutils@/bin/stat -c '%G' $dir) | ||
12 | tmpFile="${dir}/.tmp/${1:t}" | ||
13 | target="${dir}/${1:t}" | ||
14 | |||
15 | if [[ -n "${3}" ]]; then | ||
16 | target="${dir}"/$(@coreutils@/bin/base64 -d <<<${3}) | ||
17 | fi | ||
18 | |||
19 | @utillinux@/bin/logger --id=$pid -t recv-media -p user.debug <<EOF | ||
20 | $(id) | ||
21 | $(stat ${dir}) | ||
22 | $(stat ${1}) | ||
23 | $(echo ${2}) | ||
24 | EOF | ||
25 | |||
26 | if [[ $(id -Gn) != *"$group"* ]]; then | ||
27 | printf "Groups are ‘%s’. Trying to switch primary group to ‘%s’..." $(id -Gn) $group | ||
28 | exec -- @wrapperDir@/sg $group "$0 $*" | ||
29 | fi | ||
30 | |||
31 | typeset -a failures | ||
32 | failures=() | ||
33 | |||
34 | ( | ||
35 | if ! cp -lnv --preserve=all "$1" "${target}"; then | ||
36 | @coreutils@/bin/mkdir -pv "${tmpFile:h}" || failures+="mkdir" | ||
37 | |||
38 | @utillinux@/bin/ionice -c 3 -t @coreutils@/bin/cp -vn --preserve=all "$1" "${tmpFile}" && mv -v "${tmpFile}" "${target}" || failures+="cp" | ||
39 | fi | ||
40 | @coreutils@/bin/touch -c -m -t "$2" "${target}" || failures+="touch" | ||
41 | ) | ||
42 | |||
43 | if @doNotify@ && [[ $#failures -gt 0 ]]; then | ||
44 | printf "%s\n%s\n%s" "${target:t}" "Failed to download" ${(j:, :)failures} | @notify@ -a recv-media -u Critical | ||
45 | fi | ||
46 | |||
47 | if @doNotify@; then | ||
48 | ( | ||
49 | if @showTitle@; then | ||
50 | summary=$(@ffmpeg@/bin/ffmpeg -i "${target}" -f ffmetadata pipe:1 2>/dev/null | sed -r '/\[CHAPTER\]/q; /^title=/!d; s/^title=//') | ||
51 | else | ||
52 | summary=${target:t} | ||
53 | fi | ||
54 | printf "%s\n%s\n" "${summary}" "" | @notify@ -a recv-media || true | ||
55 | ) || true | ||
56 | fi | ||
diff --git a/custom/uucp-mediaclient.nix b/custom/uucp-mediaclient.nix new file mode 100644 index 00000000..07afeda4 --- /dev/null +++ b/custom/uucp-mediaclient.nix | |||
@@ -0,0 +1,89 @@ | |||
1 | { config, lib, pkgs, ... }: | ||
2 | |||
3 | with lib; | ||
4 | |||
5 | let | ||
6 | cfg = config.services.uucp.media-client; | ||
7 | |||
8 | recv-media = with pkgs; stdenv.mkDerivation rec { | ||
9 | name = "recv-media"; | ||
10 | src = ./recv-media.sh; | ||
11 | |||
12 | phases = [ "unpackPhase" "buildPhase" "installPhase" ]; | ||
13 | |||
14 | unpackPhase = '' | ||
15 | cp $src recv-media | ||
16 | ''; | ||
17 | |||
18 | inherit zsh coreutils; | ||
19 | inherit (config.security) wrapperDir; | ||
20 | inherit (cfg) mediaDir; | ||
21 | utillinux = util-linux; | ||
22 | doNotify = cfg.notify.users != []; | ||
23 | ffmpeg = if doNotify then ffmpeg-full else "/no-such-path"; | ||
24 | notify = writeScript "notify.sh" '' | ||
25 | #!${zsh}/bin/zsh | ||
26 | |||
27 | ${concatMapStringsSep "\n" ({ name, ... }: "${notify-user name}/bin/notify-${name} $@") notify.users} | ||
28 | ''; | ||
29 | |||
30 | buildPhase = '' | ||
31 | substituteAllInPlace recv-media | ||
32 | ''; | ||
33 | |||
34 | installPhase = '' | ||
35 | mkdir -p $out/bin | ||
36 | |||
37 | install -m 755 -t $out/bin \ | ||
38 | recv-media | ||
39 | ''; | ||
40 | }; | ||
41 | |||
42 | |||
43 | in { | ||
44 | options = { | ||
45 | services.uucp.media-client = { | ||
46 | remoteNodes = mkOption { | ||
47 | type = with types; listOf str; | ||
48 | default = []; | ||
49 | description = '' | ||
50 | Servers to receive media from | ||
51 | ''; | ||
52 | }; | ||
53 | |||
54 | notify = { | ||
55 | users = mkOption { | ||
56 | type = with types; listOf str; | ||
57 | default = []; | ||
58 | description = '' | ||
59 | Users to notify | ||
60 | ''; | ||
61 | }; | ||
62 | |||
63 | extractTitle = mkOption { | ||
64 | type = types.bool; | ||
65 | default = true; | ||
66 | description = '' | ||
67 | Use ffmpeg to include the media title in notifications | ||
68 | ''; | ||
69 | }; | ||
70 | }; | ||
71 | |||
72 | mediaDir = mkOption { | ||
73 | type = types.path; | ||
74 | default = "/var/media"; | ||
75 | description = "Media directory"; | ||
76 | }; | ||
77 | }; | ||
78 | }; | ||
79 | |||
80 | config = mkIf (cfg.remoteNodes != []) { | ||
81 | imports = [ ./custom/notify-users.nix ]; | ||
82 | |||
83 | services.uucp.commandPath = [ "${recv-media}/bin" ]; | ||
84 | services.uucp.remoteNodes = genAttrs cfg.remoteNodes (name: { commands = ["recv-media"]; } ); | ||
85 | |||
86 | services.notify-users = notify.users; | ||
87 | }; | ||
88 | } | ||
89 | |||
@@ -15,6 +15,8 @@ | |||
15 | ./custom/tinc/yggdrasil.nix | 15 | ./custom/tinc/yggdrasil.nix |
16 | ./custom/uucp.nix | 16 | ./custom/uucp.nix |
17 | ./custom/borgbackup.nix | 17 | ./custom/borgbackup.nix |
18 | ./custom/uucp-mediaclient.nix | ||
19 | ./custom/notify-users.nix | ||
18 | ./utils/nix/module.nix | 20 | ./utils/nix/module.nix |
19 | ]; | 21 | ]; |
20 | 22 | ||
@@ -214,7 +216,7 @@ | |||
214 | "odin" = { | 216 | "odin" = { |
215 | publicKeys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKcDj49TqmflGTmtGBqDawxmCBWW1txj61CZ7KT0hTHK uucp@odin"]; | 217 | publicKeys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKcDj49TqmflGTmtGBqDawxmCBWW1txj61CZ7KT0hTHK uucp@odin"]; |
216 | hostnames = ["odin.asgard.yggdrasil"]; | 218 | hostnames = ["odin.asgard.yggdrasil"]; |
217 | commands = lib.mkForce ["recv-media" "notify-gkleen"]; | 219 | commands = ["notify-gkleen"]; |
218 | }; | 220 | }; |
219 | "ymir" = { | 221 | "ymir" = { |
220 | publicKeys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFH1QWdgoC03nzW5GBuCl2pqASHeIXIYtE9IInHdaKcO uucp@ymir"]; | 222 | publicKeys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFH1QWdgoC03nzW5GBuCl2pqASHeIXIYtE9IInHdaKcO uucp@ymir"]; |
@@ -222,9 +224,16 @@ | |||
222 | }; | 224 | }; |
223 | }; | 225 | }; |
224 | 226 | ||
225 | commandPath = [ "${pkgs.callPackage ./hel/recv-media.nix {}}/bin" config.security.wrapperDir ]; | 227 | commandPath = [ "${config.security.wrapperDir}" ]; |
226 | defaultCommands = lib.mkForce []; | 228 | defaultCommands = lib.mkForce []; |
229 | |||
230 | media-client = { | ||
231 | remoteNodes = [ "odin" ]; | ||
232 | notify.users = [ "gkleen" ]; | ||
233 | }; | ||
227 | }; | 234 | }; |
235 | |||
236 | notify-users = [ "gkleen" ]; | ||
228 | 237 | ||
229 | postfix = { | 238 | postfix = { |
230 | enable = true; | 239 | enable = true; |
@@ -357,15 +366,6 @@ | |||
357 | "mount.cifs".source = "${pkgs.cifs-utils}/bin/mount.cifs"; | 366 | "mount.cifs".source = "${pkgs.cifs-utils}/bin/mount.cifs"; |
358 | "thinklight".source = | 367 | "thinklight".source = |
359 | "${(pkgs.callPackage ./custom/thinklight.nix { thinklight = "kbd_backlight"; })}/bin/thinklight"; | 368 | "${(pkgs.callPackage ./custom/thinklight.nix { thinklight = "kbd_backlight"; })}/bin/thinklight"; |
360 | "notify-gkleen" = { | ||
361 | group = "users"; | ||
362 | owner = "gkleen"; | ||
363 | setgid = true; | ||
364 | setuid = true; | ||
365 | permissions = "u+rx,g+x,o+x"; | ||
366 | source = let notify-user = pkgs.callPackage ./custom/notify-user.nix { inherit (pkgs.haskellPackages) ghcWithPackages; }; | ||
367 | in "${notify-user}/bin/notify-gkleen"; | ||
368 | }; | ||
369 | }; | 369 | }; |
370 | 370 | ||
371 | polkit = { | 371 | polkit = { |
diff --git a/hel/recv-media.nix b/hel/recv-media.nix deleted file mode 100644 index 91801593..00000000 --- a/hel/recv-media.nix +++ /dev/null | |||
@@ -1,72 +0,0 @@ | |||
1 | { stdenv | ||
2 | , coreutils | ||
3 | , writeScriptBin | ||
4 | , eject | ||
5 | , notifyUser ? "gkleen" | ||
6 | , showTitle ? true | ||
7 | , ffmpeg ? null | ||
8 | , gnused ? null | ||
9 | , wrapperDir ? "/run/wrappers/bin" | ||
10 | }: | ||
11 | |||
12 | assert showTitle -> ffmpeg != null && gnused != null && notifyUser != null; | ||
13 | |||
14 | writeScriptBin "recv-media" '' | ||
15 | #!${stdenv.shell} | ||
16 | |||
17 | pid=$? | ||
18 | notify=${if notifyUser == null then "false" else "true"} | ||
19 | notifyUser=${if notifyUser == null then "" else notifyUser} | ||
20 | |||
21 | PATH=${wrapperDir}:${eject}/bin:${coreutils}/bin${if showTitle then '':${ffmpeg}/bin:${gnused}/bin'' else ""} | ||
22 | |||
23 | exec 1> >(logger --id=$pid -t recv-media -p user.notice) | ||
24 | exec 2> >(logger --id=$pid -t recv-media -p user.error) | ||
25 | |||
26 | [[ -z "$1" || -z "$2" ]] && exit 2 | ||
27 | |||
28 | dir=/var/media | ||
29 | group=$(stat -c '%G' $dir) | ||
30 | tmpFile="''${dir}/.tmp/''${1:t}" | ||
31 | target="''${dir}/''${1:t}" | ||
32 | |||
33 | if [[ -n "''${3}" ]]; then | ||
34 | target="''${dir}"/$(base64 -d <<<''${3}) | ||
35 | fi | ||
36 | |||
37 | logger --id=$pid -t recv-media -p user.debug <<EOF | ||
38 | $(id) | ||
39 | $(stat ''${dir}) | ||
40 | $(stat ''${1}) | ||
41 | $(echo ''${2}) | ||
42 | EOF | ||
43 | |||
44 | if [[ $(id -Gn) != *"$group"* ]]; then | ||
45 | printf "Groups are ‘%s’. Trying to switch primary group to ‘%s’..." $(id -Gn) $group | ||
46 | exec -- sg $group "$0 $*" | ||
47 | fi | ||
48 | |||
49 | typeset -a failures | ||
50 | failures=() | ||
51 | |||
52 | ( | ||
53 | if ! cp -lnv --preserve=all "$1" "''${target}"; then | ||
54 | mkdir -pv "''${tmpFile:h}" || failures+="mkdir" | ||
55 | |||
56 | ionice -c 3 -t cp -vn --preserve=all "$1" "''${tmpFile}" && mv -v "''${tmpFile}" "''${target}" || failures+="cp" | ||
57 | fi | ||
58 | touch -c -m -t "$2" "''${target}" || failures+="touch" | ||
59 | ) | ||
60 | |||
61 | if $notify && [[ $#failures -gt 0 ]]; then | ||
62 | printf "%s\n%s\n%s" $(basename "$1") "Failed to download" ''${(j:, :)failures} | notify-''${notifyUser} -a recv-media -u Critical | ||
63 | fi | ||
64 | |||
65 | if $notify; then | ||
66 | ( | ||
67 | summary=${if showTitle then ''$(ffmpeg -i "''${target}" -f ffmetadata pipe:1 2>/dev/null | sed -r '/^\[CHAPTER\]$/q; /^title=/!d; s/^title=//')'' else ''""''} | ||
68 | [[ -z "''${summary}" ]] && summary=$(basename "$1") | ||
69 | printf "%s\n%s\n" "''${summary}" "" | notify-''${notifyUser} -a recv-media || true | ||
70 | ) || true | ||
71 | fi | ||
72 | '' | ||