various improvements
[distro-setup] / brc2
1 #!/bin/bash
2 # I, Ian Kelling, follow the GNU license recommendations at
3 # https://www.gnu.org/licenses/license-recommendations.en.html. They
4 # recommend that small programs, < 300 lines, be licensed under the
5 # Apache License 2.0. This file contains or is part of one or more small
6 # programs. If a small program grows beyond 300 lines, I plan to switch
7 # its license to GPL.
8
9 # Copyright 2024 Ian Kelling
10
11 # Licensed under the Apache License, Version 2.0 (the "License");
12 # you may not use this file except in compliance with the License.
13 # You may obtain a copy of the License at
14
15 # http://www.apache.org/licenses/LICENSE-2.0
16
17 # Unless required by applicable law or agreed to in writing, software
18 # distributed under the License is distributed on an "AS IS" BASIS,
19 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 # See the License for the specific language governing permissions and
21 # limitations under the License.
22
23 # this gets sourced. shebang is just for file mode detection
24
25
26 # * settings
27
28 if [[ $LESSHISTFILE == - ]]; then
29 HISTFILE=
30 c() { cd "$@"; }
31 elif [[ $HISTFILE ]]; then
32 # use an alternate history file when we are streaming.
33 if [[ -e $HOME/.iank-stream-on ]]; then
34 HISTFILE=/a/bin/data/stream_hist
35 else
36 HISTFILE=$HOME/.bh
37 fi
38 fi
39
40 # history personal
41 hip() {
42 history -c
43 HISTFILE=$HOME/.bh
44 history -r
45 }
46
47 # history for streaming
48 his() {
49 history -c
50 HISTFILE=/a/bin/data/stream_hist
51 history -r
52 }
53
54
55 source /a/bin/distro-setup/path-add-function
56 path-add /a/exe
57 # add this with absolute paths as needed for better security
58 #path-add --end /path/to/node_modules/.bin
59 ## for yarn, etc
60 #path-add --end /usr/lib/node_modules/corepack/shims/
61
62 # pip3 --user things go here:
63 path-add --end ~/.local/bin
64 path-add --ifexists --end /a/work/libremanage
65 path-add --ifexists --end /a/opt/adt-bundle*/tools /a/opt/adt-bundle*/platform-tools
66 path-add --ifexists --end /a/opt/scancode-toolkit-3.10.
67 path-add --ifexists --end /p/bin
68
69 case $HOSTNAME in
70 sy|bo)
71 # https://askubuntu.com/questions/1254544/vlc-crashes-when-opening-any-file-ubuntu-20-04
72 if grep -qE '^VERSION_CODENAME="(nabia|focal)"' /etc/os-release &>/dev/null; then
73 export MESA_LOADER_DRIVER_OVERRIDE=i965
74 fi
75 ;;
76 esac
77
78
79 export WCDHOME=/a
80
81
82 case $EUID in
83 0)
84 # shellcheck disable=SC2034 # used in brc
85 SL_SSH_ARGS="-F $HOME/.ssh/confighome"
86 ;;
87 esac
88
89
90 # * include files
91
92 # generated instead of dynamic for the benefit of shellcheck
93 #for x in /a/bin/distro-functions/src/* /a/bin/!(githtml)/*-function?(s); do echo source $x ; done
94 source /a/bin/distro-functions/src/identify-distros
95 source /a/bin/log-quiet/logq-function
96 # for x in /a/bin/bash_unpublished/source-!(.#*); do echo source $x; done
97 source /a/bin/bash_unpublished/source-semi-priv
98 source /a/bin/bash_unpublished/source-state
99
100 if [[ $HOSTNAME == "$MAIL_HOST" ]]; then
101 export MAIL_HOST_P=t
102 else
103 export NOT_MAIL_HOST_P=t
104 fi
105
106 source /a/bin/ds/beet-data
107
108
109 # * functions
110
111
112
113 multimic() {
114 local i
115 local -a sources
116
117 m pactl unload-module module-loopback
118 m pactl unload-module module-null-sink
119 m pactl unload-module module-remap-source
120
121 IFS=" " read -r -a sources <<<"$(pacmd list-sources | sed -rn 's/.*name: <([^>]+).*/\1/p')"
122
123 if (( ! $# )); then
124 i=0
125 for s in ${sources[@]}; do
126 e $i $s
127 i=$(( i+1 ))
128 done
129 read -r l
130 set -- $l
131 fi
132 m pactl load-module module-null-sink sink_name=ianinput sink_properties=device.description=ianinputs
133 for i; do
134 m pactl load-module module-loopback source=${sources[i]} sink_dont_move=true sink=ianinput
135 done
136 pactl load-module module-remap-source source_name=iancombine master=ianinput.monitor source_properties=device.description=iancombine
137 }
138
139 # h ssh test
140 # For testing restrictive ssh.
141 hstest() {
142 install-my-scripts
143 d=$(mktemp -d)
144 sed '/^ *IdentityFile/d' ~/.ssh/config >$d/config
145 s command ssh -F $d/config -i /q/root/h "$@"
146 }
147
148 # h rsync test
149 # For testing restrictive rsync
150 hrtest() { #
151 install-my-scripts
152 d=$(mktemp -d)
153 sed '/^ *IdentityFile/d' ~/.ssh/config >$d/config
154 s rsync -e "ssh -F $d/config -i /q/root/h" "$@"
155 }
156
157 # rsync as root and avoid the default restrictive h key & config.
158 rootrsync() {
159 s rsync -e "ssh -F /root/.ssh/confighome" "$@"
160 }
161
162 zcheck() {
163 ssh bow DISPLAY=:0 scrot /tmp/oegu.jpg
164 scp bow:/tmp/oegu.jpg /t
165 ssh bow rm /tmp/oegu.jpg
166 feh /t/oegu.jpg
167 }
168 zmon() {
169 while true; do
170 ziva-screen
171 sleep 15
172 done
173 }
174
175 slemacs() {
176 local arg rtime v
177 arg="$1"
178 remote="$2"
179 if [[ $arg == [89]0Etiona* ]]; then
180 v=${arg::1}
181 rtime=${arg#*Etiona} # remote time
182 if [[ ! $rtime ]]; then
183 rtime=0
184 fi
185 dir=/a/opt/emacs-trisquel${v}-nox/.iank
186 ltime=$(stat -c%Y $dir/e/e/.emacs.d/init.el)
187 if (( ltime > rtime )); then
188 m rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" $dir "$remote":/home/iank
189 fi
190 fi
191 }
192
193 sle() { # sl emacs
194 local f=/home/iank/.emacs.d/init.el
195 sl --sl-test-cmd "sed -rn '/^VERSION=/{s/^.*=//;s/[^[:alnum:]]//gp}' /etc/os-release; test -e $f && stat -c%Y $f" --sl-test-hook slemacs "$@"
196 }
197 ccomp ssh sle
198
199 # Run this manually after .emacs.d changes. Otherwise, to check if
200 # files changed with find takes 90ms. sl normally only adds 25ms. We
201 # could cut it down to 10ms if we put things on a btrfs filesystem and
202 # looked for changes there, or used some inotify thing, but that seems
203 # like too much work.
204 egh() { # emacs gnuhope
205 RSYNC_RSH=ssh m rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" /a/opt/emacs-trisquel9-nox/.iank lists2d.fsf.org:.ianktrisquel_9
206 RSYNC_RSH=ssh m rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" /a/opt/emacs-trisquel8-nox/.iank lists2d.fsf.org:/home/iank
207 }
208 ekw() {
209 local shell="bash -s"
210 if [[ $HOSTNAME != kw ]]; then
211 shell="ssh kw.office.fsf.org"
212 bbk -m /a -t kw
213 fi
214 $shell <<'EOF'
215 sudo mkdir /root/.ianktrisquel_9
216 sudo rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" /a/opt/emacs-trisquel9-nox/.iank /root/.ianktrisquel_9
217 rsync -rptL --delete --filter=". /b/ds/sl/rsync-filter" /a/opt/emacs-trisquel8-nox/.iank /home/iank
218 EOF
219 }
220
221 rm-docker-iptables() {
222 s iptables -S | gr docker | gr -- -A | sed 's/-A/-D/'| while read -r l; do sudo iptables $l; done
223 s iptables -S -t nat | gr docker | gr -- -A | sed 's/-A/-D/'| while read -r l; do sudo iptables -t nat $l; done
224 s iptables -S | gr docker | gr -- -N | sed 's/-N/-X/'| while read -r l; do sudo iptables $l; done
225 s iptables -S -t nat | gr docker | gr -- -N | sed 's/-N/-X/'| while read -r l; do sudo iptables -t nat $l; done
226 }
227
228 # usage mkschroot [-] distro codename packages
229 # - means no piping in of sources.list
230 #
231 # note some useful post mkschroot i've used in the past
232 # tu /nocow/schroot/flidas/etc/sudoers <<EOF
233 # $USER ALL=(ALL) NOPASSWD: ALL
234 # Defaults env_keep += SUDOD
235 # Defaults always_set_home
236 # Defaults !umask
237 # EOF
238 # sd /nocow/schroot/flidas//etc/locale.gen <<'EOF'
239 # en_US.UTF-8 UTF-8
240 # EOF
241 # s schroot -c flidas locale-gen
242 # s schroot -c flidas update-locale LANG=en_US.UTF-8
243
244 mkschroot() {
245 local sources force repo n distro
246 force=false
247 while [[ $1 == -* ]]; do
248 case $1 in
249 -f) force=true; shift ;;
250 -s)
251 sources="$2"
252 if [[ ! -s $sources ]]; then
253 echo mkschroot: error: sources file $sources does not exist or is empty
254 return 1
255 fi
256 shift 2
257 ;;
258 esac
259 done
260 distro=$1
261 shift
262 case $distro in
263 trisquel)
264 repo=http://mirror.fsf.org/trisquel/
265 ;;
266 ubuntu)
267 repo=http://archive.ubuntu.com/ubuntu/
268 ;;
269 debian)
270 repo=http://deb.debian.org/debian/
271 ;;
272 esac
273 n=$1
274
275 shift
276 if ! $force && schroot -l | grep -xFq chroot:$n; then
277 echo "$0: $n schroot already installed, skipping"
278 return 0
279 fi
280 apps=($@)
281 d=/nocow/schroot/$n
282 sd /etc/schroot/chroot.d/$n.conf <<EOF
283 [$n]
284 description=$n
285 type=directory
286 directory=$d
287 profile=desktop
288 preserve-environment=true
289 users=$USER,user2
290 EOF
291 cd
292 if [[ ! -e $d/bin ]]; then
293 sudo mkdir -p $d
294 # resolvconf otherwise schroot fails with
295 # cp: not writing through dangling symlink '/var/run/schroot/mount/flidas-7a2362e0-81b3-4848-92c1-610203ef5976/etc/resolv.conf'
296 sudo debootstrap --exclude=resolvconf $n $d $repo
297 fi
298 if [[ $sources ]]; then
299 sudo install -m 644 $sources $d/etc/apt/sources.list
300 fi
301 sudo chroot $d apt-get update
302 sudo DEBIAN_FRONTEND=noninteractive chroot $d apt-get -y dist-upgrade --purge --auto-remove
303 sudo cp -P {,$d}/etc/localtime
304 if (( ${#apps[@]} )); then
305 sudo DEBIAN_FRONTEND=noninteractive schroot -c $n -- apt-get install --allow-unauthenticated -y ${apps[@]}
306 fi
307 }
308
309
310 # note: this is incomplete and untested.
311 # https://wiki.archlinux.org/index.php/Install_Arch_Linux_from_existing_Linux#Creating_a_chroot
312 mkarchchroot() {
313 local tarball mirror
314 mirror=https://mirrors.edge.kernel.org/archlinux/iso/latest/
315 tarball=$(curl -s $mirror | sed -nr 's/.*"(archlinux-bootstrap-.*-x86_64.tar.gz)".*/\1/p')
316 wget -O /tmp/arch.tar.gz https://mirrors.edge.kernel.org/archlinux/iso/latest/$tarball
317 s mkdir -p /nocow/schroot/arch
318 cd _/nocow/schroot/arch
319 s sed -i '/## United States/,/^$/s,^#,,' etc/pacman.d/mirrorlist
320 # error: could not determine cachedir mount point /var/cache/pacman/pkg
321 s sed -i /^CheckSpace/d etc/pacman.conf
322 chroot . /bin/bash -s <<'EOF'
323 pacman-key --init
324 pacman-key --populate archlinux
325 pacman -Syyu
326 EOF
327 # example of building an aur package:
328 # pacman -Sy base-devel wget
329 # useradd -m iank
330 # f=$target/etc/sudoers
331 # line='iank ALL=(ALL) NOPASSWD: ALL'
332 # if [[ ! -e $f ]] || ! grep -xF "$line" $f; then
333 # echo "$line" >> $f
334 # fi
335 # su iank
336 # wget https://aur.archlinux.org/cgit/aur.git/snapshot/anbox-image-gapps.tar.gz
337 # tar xzf anbox-image-gapps.tar.gz
338 # cd anbox-image-gapps
339 # makepkg -s
340 }
341
342
343 # clock back in to timetrack from last entry
344 tback() {
345 sqlite3 /p/.timetrap.db "update entries set end = NULL where id = (select max(id) from entries);"
346 }
347
348 # sshfs example:
349 # s sshfs bu@$host:/bu/home/md /bu/mnt -o reconnect,ServerAliveInterval=20,ServerAliveCountMax=30 -o allow_other
350
351 edelayoff() {
352 echo all >/var/spool/exim4/gw/.no-delay-eximids
353 if [[ $EUID == 0 ]]; then
354 chown iank:iank /var/spool/exim4/gw/.no-delay-eximids
355 fi
356 }
357 edelayon() {
358 echo >/var/spool/exim4/gw/.no-delay-eximids
359 if [[ $EUID == 0 ]]; then
360 chown iank:iank /var/spool/exim4/gw/.no-delay-eximids
361 fi
362 }
363
364 eqgo() {
365 local -a array tmpstr delayon
366 delayon=true
367 if grep -qFx all /var/spool/exim4/gw/.no-delay-eximids; then
368 delayon=false
369 fi
370 if $delayon; then
371 echo all >/var/spool/exim4/gw/.no-delay-eximids
372 fi
373 tmpstr=$(exiqgrep -i -r.\*)
374 mapfile -t array <<<"$tmpstr"
375 enn -M "${array[@]}"
376 if $delayon; then
377 echo >/var/spool/exim4/gw/.no-delay-eximids
378 fi
379 }
380 eqgo1() {
381 local eid
382 eid="$(exipick -i -r.\*|h1)"
383 sed -n "/^all$/p;\$a $eid" /var/spool/exim4/gw/.no-delay-eximids
384 enn -M "$eid"
385 }
386 # exim -M (in namespace and without delay)
387 ennm() {
388 local eid
389 for eid; do
390 printf "%s\n" "$eid" >>/var/spool/exim4/gw/.no-delay-eximids
391 done
392 enn -M "$@"
393 }
394
395
396 gnupload(){
397 /a/f/gnulib/build-aux/gnupload "$@"
398 }
399
400 abrowserrmcompat() {
401 local f
402 ngset
403 f=(/p/c/firefox*/compatibility.ini)
404 if (( ${#f[@]} )); then
405 rm ${f[@]}
406 fi
407 ngreset
408 }
409
410 checkre() {
411 s checkrestart -b /a/bin/ds/checkrestart-blacklist -pv
412 }
413
414 cp-blocked-domains-to-brains() {
415 cp /a/f/ans/roles/exim/files/mx/simple/etc/exim4/bad-sender_domains /a/f/brains/sysadmin/kb/blocked_email_domains.mdwn
416 }
417 cp-blocked-domains-to-ansible() {
418 cp /a/f/brains/sysadmin/kb/blocked_email_domains.mdwn /a/f/ans/roles/exim/files/mx/simple/etc/exim4/bad-sender_domains
419 }
420
421
422 daycat() {
423 ngset
424 hrcat /m/md/daylert/{cur,new}/*
425 ngreset
426 }
427 dayclear() {
428 ngset
429 rm -f /m/md/daylert/{cur,new}/*
430 }
431
432
433 acat() {
434 ngset
435 hrcat /m/md/alerts/{cur,new}/*
436 ngreset
437 hr; echo bk; hr
438 ssh bk.b8.nz "shopt -s nullglob; hrcat /m/md/INBOX/new/* /m/md/INBOX/cur/*"
439 }
440 aclear() {
441 ngset
442 rm -f /m/md/alerts/{cur,new}/*
443 ngreset
444 ssh bk.b8.nz "shopt -s nullglob; rm -f /m/md/INBOX/new/* /m/md/INBOX/cur/*"
445 system-status _
446 }
447
448 alerts() {
449 find /var/local/cron-errors /home/iank/cron-errors /sysd-mail-once-state -type f
450 }
451 ralerts() { # remote alerts
452 local ret shell
453 # this list is duplicated in check-remote-mailqs
454 for h in bk je li frodo x3wg kdwg sywg; do
455 echo $h:
456 shell="ssh $h"
457 if [[ $HOSTNAME == "${h%wg}" ]]; then
458 shell=
459 fi
460 ret=0
461 $shell find /var/local/cron-errors /home/iank/cron-errors /sysd-mail-once-state -type f || ret=$?
462 if (( ret )); then
463 echo ret:$ret
464 fi
465 done
466 }
467
468 ap() {
469 # pushd in case current directory has an ansible.cfg file
470 pushd /a/xans >/dev/null
471 ansible-playbook -v -i ${1:- $(hostname -f)}, site.yml
472 popd >/dev/null
473 }
474 aw() {
475 pushd /a/work/ans >/dev/null
476 time ansible-playbook -i inventory adhoc.yml "$@"
477 popd >/dev/null
478 }
479 ad() {
480 pushd /a/bin/distro-setup/a >/dev/null
481 ansible-playbook site.yml "$@"
482 popd >/dev/null
483 }
484
485 astudio() {
486 # googling android emulator libGL error: failed to load driver: r600
487 # lead to http://stackoverflow.com/a/36625175/14456
488 export ANDROID_EMULATOR_USE_SYSTEM_LIBS=1
489 /a/opt/android-studio/bin/studio.sh "$@" & r
490 }
491
492 # Convert brains file path to url and vice versa
493 # usage: brains [URL_OR_PATH]
494 brains() {
495 _iki-convert brains.fsf.org/wiki "$@"
496 }
497 glue() {
498 _iki-convert gluestick.office.fsf.org "$@"
499 }
500
501 # usage: see above
502 _iki-convert() {
503 local url url_prefix path input repo_dir dir url_dir url name
504 url_prefix="$1"
505 name="${url_prefix%%.*}"
506 repo_dir="/f/$name"
507 shift
508 if [[ $1 ]]; then
509 input="$*"
510 else
511 read -r -p "enter path or url"$'\n' input
512 fi
513 case $input in
514 http*)
515 path="$repo_dir/${input##http*://"$url_prefix"/}"
516 # for files like x.jpg, we dont need to convert the extension.
517 if [[ $path == */ ]]; then
518 path=${path%/}.mdwn
519 # brains adds trailing slash, but without trailing is still
520 # valid. We can't be totally sure whether to add mdwn, but we
521 # can guess based on the existence of the file. We can't be sure
522 # because it could be a file like x.jpg, that we just don't have
523 # in our local repo.
524 elif [[ ! -f $path && -e $path.mdwn ]]; then
525 path=${path}.mdwn
526 fi
527 j printf "%s\n" "$path"
528 ;;
529 *)
530 path=$(fp "$input")
531 url_dir=$(echo "$path" | sed -r "s,^(/a)?$repo_dir/,,")
532 url="https://$url_prefix/$url_dir"
533 if [[ $url == *.mdwn ]]; then
534 url="${url%.mdwn}/"
535 fi
536 j echo "$url"
537 ;;
538 esac
539 }
540
541
542 # Generate beet smartplaylists for navidrome.
543 # for going in the reverse direction, run
544 # /b/ds/navidrome-playlist-export
545 beetsmartplaylists() {
546 install -m 0700 -d /tmp/ianbeetstmp
547 beet splupdate
548 # kill off any playlists we deleted. they will still need manual
549 # killing from a navidrome client.
550 rm -rf /i/converted/beetsmartplaylists
551 mkdir -p /i/converted/beetsmartplaylists
552 for f in /tmp/ianbeetstmp/*; do
553 sed 's,^/i/m,/i/converted,;s,\.flac$,.mp3,' "$f" >"/i/converted/beetsmartplaylists/${f##*/}"
554 rm "$f"
555 done
556 rmdir /tmp/ianbeetstmp
557 }
558
559 # internal function for beetrating, in case we need to ssh
560 beetrating-stdin() {
561 local tmp rating path cpath sqlpath userid
562 # plucked this from the db. im the only user.
563 userid=23cc2eb9-e35e-4811-a0f0-d5f0dd6eb634
564 while read -r rating path; do
565 cpath="/i/converted${path#/i/m}" # converted path
566 case $cpath in
567 *.flac)
568 cpath="${cpath%.*}.mp3"
569 ;;
570 esac
571 if [[ ! -e $cpath ]]; then
572 echo "beetraing: error: this should not happen, path does not exist: $cpath"
573 return 1
574 fi
575 sqlpath="${cpath//\'/\'\'}"
576 old_rating=$(sqlite3 /i/navidrome/navidrome.db "select rating from annotation inner join media_file on item_id = id where path = '$sqlpath' and item_type = 'media_file';")
577 if [[ $old_rating ]]; then
578 if [[ $old_rating != "$rating" ]]; then
579 echo "setting rating $old_rating -> $rating $cpath"
580 # https://stackoverflow.com/a/50317320
581 # we got a timeout error once. arbitrarily chose 15 seconds.
582 sqlite3 /i/navidrome/navidrome.db ".timeout 15000" "
583 update annotation set rating = $rating
584 where item_id in (
585 select media_file.id from annotation inner join media_file on annotation.item_id = media_file.id
586 where media_file.path = '$sqlpath' and annotation.item_type = 'media_file' );"
587 fi
588 else
589 echo "setting rating $rating $cpath"
590 # /a/opt/navidrome/persistence/sql_annotations.go v0.48.0
591 # https://www.sqlite.org/lang_insert.html
592 sqlite3 /i/navidrome/navidrome.db ".timeout 15000" "insert into annotation select '$(uuidgen)', '$userid', id, 'media_file', 0, NULL, $rating, 0, NULL from media_file where path = '$sqlpath';"
593 fi
594 done
595 }
596
597 # Export beets ratings into navidrome
598 beetrating() {
599 local ssh_prefix
600 if [[ $HOSTNAME != kd ]]; then
601 ssh_prefix="ssh b8.nz"
602 fi
603 # shellcheck disable=SC2016 # obvious reason
604 beet ls -f '$rating $path' $nav_convert_query | $ssh_prefix beetrating-stdin
605 }
606
607 # Do transcoding and hardlinking of audio files for navidrome.
608 beetconvert() {
609 local tmpf
610 tmpf="$(mktemp)"
611 # a bunch of effort to ignore output we dont care about...
612 sed 's/^format_item:.*/format_item: ignore_this/' ~/.config/beets/config.yaml >$tmpf
613 beet -c $tmpf convert -y $nav_convert_query > >(grep -vFx 'ignore_this' ||:) 2> >(grep -v '^convert: Skipping' ||:)
614 rm "$tmpf"
615 }
616 # This deletes files in the converted directory which should no longer
617 # be there due to a rename of the unconverted file.
618 beetconvert-rm-extras() {
619 local l tmpf
620 local -A paths
621 tmpf="$(mktemp)"
622 # shellcheck disable=SC2016 # obvious reason
623 beet ls -f '$path' $nav_convert_query >"$tmpf"
624 ## begin removal of files that are leftover from previous conversion,
625 # eg, previously rated > 1, now rated 1.
626 while read -r l; do
627 convertedpath="/i/converted${l#/i/m}"
628 case $convertedpath in
629 *.flac) convertedpath="${convertedpath%.flac}.mp3" ;;
630 esac
631 paths[$convertedpath]=t
632 done <"$tmpf"
633
634 find /i/converted -path /i/converted/beetsmartplaylists -prune -o \( -type f -print \) -name '*.mp3' -o -name '*.m4a' >"$tmpf"
635 while read -r l; do
636 if [[ ! ${paths[$l]} ]]; then
637 rm -v "$l"
638 fi
639 # note: the pruning is duplicative of filtering on name, but whatever.
640 done <"$tmpf"
641 rm "$tmpf"
642 }
643
644 beets-gen-playlists() {
645 local i str
646 local -a query_array query_str
647 for i in "${!bpla[@]}"; do
648 query_str=()
649 eval "query_array=(${bpla[$i]})"
650 for str in "${query_array[@]}"; do
651 query_str+=("\"$str\"")
652 done
653 cat <<EOF
654 - name: $i.m3u
655 query: '${query_str[@]}'
656 EOF
657 done
658 }
659
660 # beet playlist. use beetag with a playlist name
661 bpl() {
662 local playlist playlist_regex
663 case $1 in
664 -h|--help)
665 for playlist in "${!bpla[@]}"; do
666 printf "%s\n" "$playlist"
667 done
668 return 0
669 ;;
670 esac
671
672 playlist="${*: -1}"
673 playlist_regex='[a-z0-9_]'
674 if [[ ! $playlist =~ $playlist_regex ]]; then
675 echo "bpl: error unexpected chars in playlist: $playlist"
676 return 1
677 fi
678 # all but last arg as options
679 eval beetag -r "${*:1:$# - 1}" "${bpla[$playlist]}"
680 }
681 complete -W "${!bpla[*]}" bpl
682
683
684 # beet modify quietly
685 beetmq() {
686 local tmpf
687 tmpf="$(mktemp)"
688 # a bunch of effort to ignore output we dont care about...
689 sed 's/^format_item:.*/format_item: ignore_this/' ~/.config/beets/config.yaml >$tmpf
690 beet -c $tmpf modify -y "$@" > >(grep -vFx -e 'ignore_this' -e 'Modifying 1 items.' ||:)
691 rm "$tmpf"
692 beetag-nostatus 1
693 }
694
695 kill-bg-quiet() {
696 # https://stackoverflow.com/a/5722874
697 kill %% 2>/dev/null ||:; wait %% 2>/dev/null ||:
698 }
699
700 # debug variables
701 dv() {
702 for arg; do
703 printf "%s=%s " "$arg" "${!arg}"
704 done
705 echo
706 }
707
708 # Must be called from beetag for variables to be setup
709 beetag-help() {
710 local -i i j col_total row col button_total row_total remainder_cols remainder_term
711 col_total=4
712 button_total=${#button_map[@]}
713 row_total=$(( button_total / col_total ))
714 remainder_cols=$(( button_total % col_total ))
715 # for debugging
716 #dv button_total row_total remainder_cols
717 beetag-nostatus
718 # - 3 is just a constant that helps things work in practice.
719 if [[ $LINES ]] && (( LINES - 3 < scrolled )); then
720 hr
721 for (( i=0; i<button_total; i++)); do
722 row=$(( i / col_total ))
723 col=$(( i % col_total ))
724 remainder_term=$remainder_cols
725 if (( col < remainder_term )); then
726 remainder_term=$col
727 fi
728 j=$(( col * row_total + row + remainder_term ))
729 # avoid double newline when we have exactly row * col buttons
730 if (( i == button_total - 1 )); then
731 printf "%s %s" ${buttons[j]} ${button_map[j]}
732 elif (( i % col_total == col_total -1 )); then
733 printf "%s %s\n" ${buttons[j]} ${button_map[j]}
734 else
735 printf "%s %-15s" ${buttons[j]} ${button_map[j]}
736 fi
737 done
738 cat <<'EOF'
739
740
741 y other genres z fg player ' = toggle play 1-5 rate ] repeat1
742 ; previous _ = delete up/down skip mpv vol,pause,seek
743 EOF
744 hr
745 scrolled=10
746 fi
747 }
748
749 # Must be called from beetag for variables to be setup
750 beetag-nostatus() {
751 if (( $# )); then
752 scrolled=$(( scrolled + $1 ))
753 fi
754 if $erasable_line; then
755 # https://stackoverflow.com/a/71286261
756 # erase line / delete line in terminal
757 printf '\033[1A\033[K'
758 fi
759 erasable_line=false
760 }
761 # meant to be called from beetag
762 beetag-status() {
763 if $erasable_line; then
764 # https://stackoverflow.com/a/71286261
765 printf '\033[1A\033[K'
766 fi
767 erasable_line=true
768 }
769
770 # meant to be called from beetag
771 mpvrpc() {
772 if jobs -p | grep -q . &>/dev/null; then
773 printf "%s\n" "$*" | socat - /tmp/mpvsock >/dev/null ||:
774 fi
775 }
776 # meant to be called from beetag
777 # o for get output
778 mpvrpco() {
779 # note: testing for background jobs will output nothing if we are in a pipeline
780 printf "%s\n" "$*" | socat - /tmp/mpvsock ||:
781 }
782
783 # meant to be called from beetag
784 mpvrpc-percent-pos() {
785 mpvrpco '{ "command": ["get_property", "percent-pos"] }' | jq .data | sed 's/\..*/%/' 2>/dev/null ||:
786 }
787
788 # run if not running.
789 #
790 # Note: this does not work with shell scripts as they are normally
791 # invoked, because the ps output has the interpreter at the start.
792 # A workaround is to invoke the command in that format, or we could
793 # do various other workarounds.
794 #
795 # background, this relies on how ps converts newlines in arguments to spaces, and
796 # assumes we won't be searching for a command with spaces in its arguments
797 rinr() {
798 # shellcheck disable=SC2009 # pgrep has no fixed string option, plus see above.
799 if ps h -o args -C "${1##*/}" | grep -Fxqv "$*" &>/dev/null || [[ $? == 141 ]]; then
800 "$@"
801 fi
802 }
803 # variation of above: run or wait if running
804 rowir() {
805 local pid
806 pid=$(ps h -o 'pid,args' -C "${1##*/}" | sed -r 's/^[[:space:]]*([0-9]+)[[:space:]](.*)/\1\n\2/' | grep -B1 -Fx "$*" | head -n1 ||: )
807 if [[ $pid ]]; then
808 # https://unix.stackexchange.com/questions/427115/listen-for-exit-of-process-given-pid
809 tail --pid="$pid" -f /dev/null
810 else
811 "$@"
812 fi
813 }
814
815 mpvrpc-loadfile() {
816 local path nextpath cachedir finalpath nextpath count
817 cachedir=$HOME/.iank-music-cache
818 path="$1"
819 nextpath="$2"
820
821 # note: logic duplicated in beetpull
822 local remote_p=true
823 if [[ $HOSTNAME == kd ]]; then
824 remote_p=false
825 fi
826
827 if $remote_p; then
828 finalpath="$cachedir${path#/i/m}"
829 rowir rsync --partial -a --inplace --mkpath "b8.nz:$path" "$finalpath"
830 finalnextpath="$cachedir${nextpath#/i/m}"
831 count=$(pgrep -a -f "^rsync --partial -a --inplace --mkpath $cachedir" || [[ $? == 1 ]] )
832 # allow us to start 2 rsyncs in the background
833 if [[ $count == [01] ]]; then
834 rinr rsync --partial -a --inplace --mkpath "b8.nz:$nextpath" "$finalnextpath" &
835 fi
836 else
837 finalpath="$path"
838 fi
839 mpvrpc '{ "command": ["loadfile", "'"$finalpath"'"] }'
840 }
841
842 # tag with beets.
843 # usage: beetag [-r] [-s] QUERY
844 # it lists the query, reads an input char for tagging one by one.
845 #
846 # note, you may want to change the play command for doing rapid taging
847 # by immediately jumping forward into the song. this is set in the beets
848 # config yaml.
849 #
850 # (available buttons: ` \ ) ] [ and non-printing chars, see
851 # https://stackoverflow.com/questions/10679188/casing-arrow-keys-in-bash
852 #
853 #
854 # note: after foregrounding the player, must quit it to get back. can't ctrl-c.
855 #
856 # keys I dont need help to remember:
857 # 1-5 rate
858 # q quit
859 # ret next
860 #
861 # todo: enter should also unpause
862 beetag() {
863 local last_genre_i fstring tag id char new_item char_i genre tag remove doplay i j random path
864 local do_rare_genres read_wait line lsout tmp ls_line skip_lookback
865 local escape_char escaped_input expected_input skip_input_regex right_pad erasable_line seek_sec
866 local pl_state_path pl_state_dir pl_state_file tmpstr
867 local new_random pl_seed_path seed_num seed_file fmt first_play repeat1
868 local -a buttons button_map ids tags tmp_tags initial_ls ls_lines paths
869 local -A button_i
870 local -i i j volume scrolled id_count line_int skip_start pre_j_count head_count skip_lookback
871 local -i overflow_lines overflow
872
873 first_play=true
874 erasable_line=false
875 escape_char=$(printf "\u1b")
876 scrolled=999 # more than any $LINES
877 ### begin arg processing ###
878 random=false
879 repeat1=false
880 new_random=false
881 case $1 in
882 -r)
883 random=true
884 shift
885 ;;
886 -s)
887 random=false
888 shift
889 ;;
890 -x)
891 new_random=true
892 shift
893 ;;
894 esac
895 if (( ! $# )); then
896 echo beetag: error expected a query arg >&2
897 return 1
898 fi
899 ### end arg processing ###
900
901 # note: I used to do beetpull here, but mpv + ssfs on slowish
902 # connection leads to bad/buggy result.
903
904 do_rare_genres=false
905 volume=70
906 read_wait=2
907 doplay=true
908
909 last_genre_i=$(( ${#common_genres[@]} - 1 ))
910 buttons=( {a..p} {r..w} {6..8} , . / - "=")
911 button_map=(${common_genres[@]} ${pl_tags[@]})
912 fstring=
913 for tag in "${pl_tags[@]}"; do
914 fstring+="%ifdef{$tag,$tag }"
915 done
916
917 for (( i=0; i<${#buttons[@]}; i++ )); do
918 button_i[${buttons[i]}]=$i
919 done
920
921 # note: this structure of files is rather haphazard.
922 seed_num=1 # later we might want a few
923 seed_file=seed$seed_num
924 if $random; then
925 pl_state_file=$seed_num
926 else
927 pl_state_file=sorted
928 fi
929 pl_state_dir=/b/data/pl-state
930 if [[ $playlist ]]; then
931 pl_state_dir=$pl_state_dir/$playlist
932 else
933 pl_state_dir=$pl_state_dir/nopl
934 fi
935 pl_state_path=$pl_state_dir/$pl_state_file
936 pl_seed_path=$pl_state_dir/$seed_file
937
938
939 if $new_random || [[ ! -r $pl_seed_path ]]; then
940 mkdir -p $pl_state_dir
941 { base64 < /dev/urandom | head -c 200 ||:; echo; } > $pl_seed_path
942 fi
943
944 # PijokVipiotOzeph is just a random string for a delimiter
945 # shellcheck disable=SC2016 # false positive
946 fmt='%ifdef{rating,$rating }'"$fstring"'$genre | $title - $artist - $album $length $id PijokVipiotOzeph $path'
947 # shellcheck disable=SC2016 # obvious reason
948 tmpstr=$(beet ls -f "$fmt" "$@" | { if $random; then sort -R --random-source=$pl_seed_path; else cat; fi; } )
949 mapfile -t initial_ls <<<"$tmpstr"
950 if [[ ! ${initial_ls[0]} ]]; then
951 echo "beetag: error: no result from beet ls $*"
952 return 1
953 fi
954 id_count=${#initial_ls[@]}
955 for line in "${initial_ls[@]}"; do
956 path="${line#*PijokVipiotOzeph }"
957 # https://github.com/koalaman/shellcheck/issues/2171
958 # shellcheck disable=SC2190 # bug in shellcheck, looking at paths from an earlier function
959 paths+=("$path")
960 line_no_path="${line% PijokVipiotOzeph*}"
961 id="${line_no_path##* }"
962 ids+=("$id")
963 right_pad="${line_no_path%% |*}"
964 ls_line="$(printf %-11s "$right_pad")${line_no_path#"$right_pad"}"
965 ls_lines+=("$ls_line")
966 i=$(( i+1 ))
967 done
968
969
970
971
972 j=0
973 if [[ $playlist ]]; then
974 if [[ -r $pl_state_path ]]; then
975 j=$(cat $pl_state_path)
976 fi
977 fi
978
979 # i only care to see a smallish portion of the list when starting.
980 head_count=$(( LINES - 20 ))
981 head_start=$(( j - head_count / 2 ))
982 if (( head_start < 0 )); then
983 head_start=0
984 fi
985 for (( i=head_start; i < head_count && i < id_count; i++ )); do
986 ls_line="${ls_lines[$i]}"
987 if (( i == j )); then
988 echo "* $ls_line"
989 else
990 echo "$ls_line"
991 fi
992 done
993 if $doplay; then
994 #{ mpv --profile=a --volume=$volume --idle 2>&1 & } 2>/dev/null
995 mpv --profile=a --volume=$volume --idle &
996 # if we dont sleep, can expect an error like this:
997 # socat[1103381] E connect(5, AF=1 "/tmp/mpvsock", 14): Connection refused
998 sleep .1
999 fi
1000
1001 while true; do
1002 id=${ids[j]}
1003 path="${paths[$j]}"
1004 lsout="${ls_lines[j]}"
1005 tags=( ${lsout%%,*} )
1006 beetag-help
1007 printf "██ %s\n" "$lsout"
1008 beetag-nostatus 1
1009 if $doplay; then
1010 # https://stackoverflow.com/a/7687716
1011 # note: duplicated down below
1012 #
1013 # notes on old method of invoking mpv each time:
1014 # https://superuser.com/questions/305933/preventing-bash-from-displaying-done-when-a-background-command-finishes-execut
1015 # we can't disown or run in a subshell or set +m because all that
1016 # disabled job control from working properly in ways we want.
1017 # todo: figure out some kind of answer to this. I think the solution
1018 # is that we are waiting in 2 second intervals and checking if the
1019 # background job exists. Instead, we should make mpv just idle
1020 # when it is done with a song and then send it a command to play a new track.
1021 #{ mpv --profile=a --volume=$volume "$path" 2>&1 & } 2>/dev/null
1022 # old
1023 #{ beet play "--args=--volume=$volume" "id:$id" 2>&1 & } 2>/dev/null
1024
1025 # on slow systems, we may need to wait like .3 seconds before mpv
1026 # is ready. so impatiently check until it is ready
1027 if $first_play; then
1028 first_play=false
1029 for (( i=0; i<20; i++ )); do
1030 if [[ $(mpvrpco '{ "command": ["get_property", "idle-active"] }' 2>/dev/null | jq .data) == true ]]; then
1031 mpvrpc-loadfile "$path" 2>/dev/null
1032 break
1033 fi
1034 sleep .1
1035 done
1036 else
1037 mpvrpc-loadfile "$path"
1038 fi
1039 erasable_line=false
1040 fi
1041 while true; do
1042 char=
1043 if $doplay; then
1044 ret=0
1045 read -rsN1 -t $read_wait char || ret=$?
1046 read_wait=2
1047 # Automatically skip to the next song if this one ends, unless
1048 # we turn off the autoplay.
1049 if (( ret == 142 )) || [[ ! $char ]]; then
1050 if jobs -p | grep -q . &>/dev/null && \
1051 [[ $(mpvrpco '{ "command": ["get_property", "idle-active"] }' | jq .data) == false ]]; then
1052 continue
1053 else
1054 break
1055 fi
1056 fi
1057 else
1058 read -rsN1 char
1059 fi
1060 beetag-help
1061 if [[ $char == $'\n' ]]; then
1062 break
1063 fi
1064 case $char in
1065 ";")
1066 j=$(( j - 2 ))
1067 break
1068 ;;
1069 "'")
1070 if $doplay; then
1071 echo "play toggled off"
1072 doplay=false
1073 else
1074 doplay=true
1075 mpvrpc-loadfile "$path"
1076 erasable_line=false
1077 fi
1078 beetag-nostatus 1
1079 continue
1080 ;;
1081 _)
1082 m beet rm --delete --force "id:$id"
1083 beetag-nostatus 4 # guessing. dont want to test atm
1084 break
1085 ;;
1086 [1-5])
1087 beetmq "id:$id" rating=$char
1088 continue
1089 ;;
1090 9)
1091 volume=$(( volume - 5 ))
1092 if (( volume < 0 )); then
1093 volume=0
1094 fi
1095 ;;&
1096 0)
1097 volume+=5
1098 if (( volume > 130 )); then
1099 volume=130
1100 fi
1101 ;;&
1102 0|9)
1103 mpvrpc '{ "command": ["set_property", "volume", '$volume'] }'
1104 beetag-status
1105 echo volume=$volume
1106 continue
1107 ;;
1108 ']')
1109 if $repeat1; then
1110 repeat1=false
1111 else
1112 repeat1=true
1113 fi
1114 echo repeat1=$repeat1
1115 continue
1116 ;;
1117 q)
1118 kill-bg-quiet
1119 return
1120 ;;
1121 y)
1122 if $do_rare_genres; then
1123 do_rare_genres=false
1124 button_map=(${common_genres[@]} ${pl_tags[@]})
1125 last_genre_i=$(( ${#rare_genres[@]} - 1 ))
1126 else
1127 do_rare_genres=true
1128 button_map=(${rare_genres[@]} ${pl_tags[@]})
1129 last_genre_i=$(( ${#rare_genres[@]} - 1 ))
1130 fi
1131 local -A button_i
1132 for (( i=0; i<${#buttons[@]}; i++ )); do
1133 button_i[${buttons[i]}]=$i
1134 done
1135 for (( i=0; i<${#button_map[@]}; i++ )); do
1136 echo ${buttons[i]} ${button_map[i]}
1137 done
1138 continue
1139 ;;
1140 z)
1141 beetag-nostatus 3
1142 # if we ctrl-z, it will put the whole function into sleep. so
1143 # basically, we can't return from a foregrounded mpv like we
1144 # would like to without some strange mechanism I can't think
1145 # of. So, instead, detect ctrl-c and wait a while for prompt
1146 # input. One idea would be to use a music player like mpd where
1147 # we can send it messages.
1148 if ! fg; then
1149 read_wait=10
1150 fi
1151 continue
1152 ;;
1153
1154 #
1155 " ")
1156 # output time if we aren't already paused
1157 if [[ $(mpvrpco '{ "command": ["get_property", "pause"] }' | jq .data) == false ]]; then
1158 # minutes/seconds
1159 #date -d @"$(mpvrpco '{ "command": ["get_property", "playback-time"] }' | jq .data)" +%M:%S ||:
1160 beetag-status
1161 mpvrpc-percent-pos
1162 fi
1163 # originally found this solution, which worked fine.
1164 #kill -STOP %% &>/dev/null
1165 #
1166 mpvrpc '{ "command": ["cycle", "pause"] }'
1167 continue
1168 ;;
1169 "$escape_char")
1170 expected_input=true
1171 read -rsn2 escaped_input
1172 skip_input_regex="^[0-9]+$"
1173 case $escaped_input in
1174 # up char: show all the songs, use less
1175 '[A')
1176 skip_start=0
1177 skip_lookback=5
1178 if (( j - skip_lookback > skip_start )); then
1179 skip_start=$(( j - skip_lookback ))
1180 fi
1181 beetag-nostatus $(( id_count - skip_start - 1 ))
1182 {
1183 line_int=0
1184 for (( i=skip_start; i < id_count; i++ )); do
1185 if (( i == j )); then
1186 echo " * ${ls_lines[i]}"
1187 continue
1188 fi
1189 echo "$line_int | ${ls_lines[i]}"
1190 line_int+=1
1191 done
1192 } | less -F
1193 ;;
1194 # down char
1195 '[B')
1196 # skip forward, but show the last few songs anyways.
1197 skip_start=0
1198 skip_lookback=3
1199 if (( j - skip_lookback > skip_start )); then
1200 skip_start=$(( j - skip_lookback ))
1201 fi
1202 beetag-nostatus $(( id_count - skip_start - 1 ))
1203
1204 line_int=0
1205 overflow_lines=$LINES
1206 for (( i=skip_start; i < overflow_lines - 1 && i < id_count; i++ )); do
1207 ls_line="${ls_lines[i]}"
1208 overflow=$(( ${#ls_line} / ( COLUMNS - 1 ) ))
1209 overflow_lines=$(( overflow_lines - overflow ))
1210 if (( i == j )); then
1211 echo " * $ls_line"
1212 continue
1213 fi
1214 echo "$line_int | $ls_line"
1215 line_int+=1
1216 done
1217 ;;
1218 # left key
1219 '[D')
1220 seek_sec=-8
1221 ;;&
1222 # right key
1223 '[C')
1224 seek_sec=8
1225 ;;&
1226 '[C'|'[D')
1227 beetag-status
1228 mpvrpc-percent-pos
1229 erasable_line=true
1230 mpvrpc '{ "command": ["seek", "'$seek_sec'"] }'
1231 continue
1232 ;;
1233 *)
1234 expected_input=false
1235 ;;
1236 esac
1237 if $expected_input; then
1238 read -r skip_input
1239 case $skip_input in
1240 q)
1241 kill-bg-quiet
1242 return
1243 ;;
1244 esac
1245 if [[ $skip_input =~ $skip_input_regex ]]; then
1246 pre_j_count=$(( j - skip_start ))
1247 j=$(( j + skip_input - pre_j_count ))
1248 if (( skip_input < pre_j_count )); then
1249 j=$(( j - 1 ))
1250 fi
1251 fi
1252 break
1253 fi
1254 ;;
1255 esac
1256 char_i=${button_i[$char]}
1257 new_item=${button_map[$char_i]}
1258 if [[ ! $char_i || ! $new_item ]]; then
1259 echo "error: no mapping of input: $char found, try again"
1260 continue
1261 fi
1262 if (( char_i <= last_genre_i )); then
1263 m beetmq "id:$id" genre=$new_item
1264 else
1265 remove=false
1266 tmp_tags=()
1267 for tag in ${tags[@]}; do
1268 if [[ $new_item == "$tag" ]]; then
1269 remove=true
1270 else
1271 tmp_tags+=("$tag")
1272 fi
1273 done
1274 if $remove; then
1275 tags=("${tags[@]}")
1276 m beetmq "id:$id" "$new_item!"
1277 else
1278 tags+=("$new_item")
1279 m beetmq "id:$id" $new_item=t
1280 fi
1281 fi
1282 done
1283 if ! $repeat1; then
1284 if (( j < id_count - 1 )); then
1285 j+=1
1286 else
1287 j=0
1288 fi
1289 fi
1290 if [[ $playlist ]]; then
1291 echo $j >$pl_state_path
1292 fi
1293 done
1294 }
1295
1296 # usage: FILE|ALBUM_DIR [GENRE]
1297 beetadd() {
1298 local import_path genre_arg single_track_arg
1299 import_path="$1"
1300 if [[ ! -e $import_path ]]; then
1301 echo "beetadd error: path does not exist"
1302 fi
1303 if [[ $2 ]]; then
1304 genre_arg="--set genre=$2"
1305 fi
1306 if [[ -f $import_path ]]; then
1307 single_track_arg=-s
1308 fi
1309 beet import --set totag=t $single_track_arg $genre_arg "$import_path"
1310 beetag totag:t
1311 beet modify -y totag:t "totag!"
1312 }
1313
1314 # update navidrome music data after doing beets tagging
1315 beet2nav() {
1316 m beetpull
1317 m beetconvert
1318 m beetrating
1319 # this function would naturally just be part of beetconvert,
1320 # but we want beetrating to happen sooner so that our ssh auth dialog
1321 # happens earlier. Currently 17 seconds for that.
1322 m beetconvert-rm-extras
1323 m beetsmartplaylists
1324 }
1325
1326 # pull in beets library locally
1327 beetpull() {
1328 local sshfs_host sshfs_cmd
1329 sshfs_host=b8.nz
1330 if [[ $HOSTNAME == kd ]]; then
1331 return 0
1332 fi
1333 if [[ ! -e /i ]]; then
1334 s mkdir /i
1335 s chown iank:iank /i
1336 fi
1337 sshfs_cmd="sshfs -o ServerAliveInterval=15,reconnect $sshfs_host:/i /i"
1338 if ! pgrep -f "^$sshfs_cmd$" >/dev/null; then
1339 m $sshfs_cmd
1340 fi
1341 }
1342
1343 # remove all playlists in navidrome, for when I make big
1344 # playlist name changes and just want to scrap everything.
1345 nav-rm-plists() {
1346 local tmpf id
1347 tmpf=$(mktemp)
1348 if [[ $HOSTNAME != kd ]]; then
1349 echo "error: run on kd"
1350 return 1
1351 fi
1352 sqlite3 /i/navidrome/navidrome.db "select id from playlist" >$tmpf
1353 while read -r id; do
1354
1355 curl --http1.1 --user "iank:$navidrome_pw" "https://b8.nz/rest/deletePlaylist.view?u=iank&s=sb219dvv7egnoe4i47k75cli0m&t=1c8f5575cd0fdf03deb971187c9c88b1&v=1.2.0&c=DSub&id=$id"
1356 done <$tmpf
1357 rm $tmpf
1358 }
1359
1360 # escape regex.
1361 #
1362 # This is not perfect but generally good enough. It escapes all
1363 # metachars listed man 3 pcrepattern.
1364 er() {
1365 sed 's/[]\\^$.[|()?*+{}]/[&]/g; s/\^/\\^/g' <<<"$*"
1366 }
1367
1368 # usage beegenre QUERY
1369 #
1370 # beet set genre for QUERY based on existing artist most used genre on
1371 #
1372 # inverse of query for each artist found in QUERY. If query starts with
1373 # "artist:" it is used as the artist instead of each artist in QUERY.
1374 #
1375 beegenre() {
1376 local count artist artregex genre singleartist tmpf tmpf2
1377 local -a artists genres
1378 singleartist=false
1379 case $1 in
1380 artist:*)
1381 singleartist=true
1382 artist="$1"
1383 shift
1384 ;;
1385 esac
1386 tmpf=$(mktemp)
1387 tmpf2=$(mktemp)
1388 if $singleartist; then
1389 # shellcheck disable=SC2016 # obvious reason
1390 beet ls -f '$genre' "$artist" "${@/#/^}" | sort | uniq -c | sort -n | tail -n1 >$tmpf
1391 read -r count genre <$tmpf ||:
1392 beet modify "$artist" "$@" genre=$genre
1393 else
1394 # shellcheck disable=SC2016 # obvious reason
1395 beet ls -f '$artist' "$@" | sort -u >$tmpf
1396 while read -r artist; do
1397 artregex=$(er "$artist")
1398 # shellcheck disable=SC2016 # obvious reason
1399 beet ls -f '$genre' "artist::^$artregex$" "${@/#/^}" | sort | uniq -c | sort -n | tail -n1 >$tmpf2
1400 read -r count genre <$tmpf2 || continue
1401 if [[ $count ]]; then
1402 artists+=("$artregex")
1403 genres+=("$genre")
1404 echo "beet modify -y $* \"artist::^$artist$\" genre=$genre # $count"
1405 fi
1406 done <$tmpf
1407 read -r -N 1 -s -p "Y/n " char
1408 case $char in
1409 [Yy$'\n'])
1410 for (( i=0; i<${#artists[@]}; i++ )); do
1411 beet modify -y "$@" "artist::^${artists[i]}$" genre=${genre[i]}
1412 done
1413 ;;
1414 esac
1415 fi
1416 rm $tmpf
1417 }
1418
1419 # prettify the date
1420 btrbk-date() {
1421 local indate
1422 indate="$1"
1423 shift
1424 date +%F_%T%:::z -d "$(sed -r 's/(.{4})(..)(.{5})(..)(.*)/\1-\2-\3:\4:\5/' <<<"$indate")" "$@"
1425 }
1426 btrbk-undate() {
1427 # fudCaHougfirp is a random string
1428 { if [[ $1 ]]; then
1429 echo "$1"
1430 else
1431 cat
1432 fi
1433 } | sed -r 's/-0([45])( |$)/fudCaHougfirp0\100/;s/_/T/;s/[:-]//g;s/fudCaHougfirp/-/'
1434
1435 }
1436 btrbk-date-sed() {
1437 local line
1438 while read -r line; do
1439 if [[ $line == *20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00* ]]; then
1440 pre="${line%%20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00*}"
1441 post="${line##*20[0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]-0[45]00}"
1442 mid="${line:${#pre}:22}"
1443 echo "$pre$(btrbk-date "$mid")$post"
1444 else
1445 echo "$line"
1446 fi
1447 done
1448 }
1449 jrbtrbk() {
1450 jr -u btrbk-run -u btrbk -u switch-mail-host "$@"
1451 }
1452
1453 # internal function
1454 btrbk-host-debug-show-host() {
1455 for f; do
1456 snaphost=
1457 for host in $remote $alt local; do
1458 if line=$(grep -P "\S*$f" /tmp/b/s/$host.log); then
1459 if [[ $snaphost ]]; then
1460 e error: snaphost=$snaphost, host=$host line="$line"
1461 fi
1462 if [[ $line == ssh* ]]; then
1463 tmp="${line#ssh://}"
1464 snaphost="${tmp%%/*}"
1465 else
1466 snaphost=$host
1467 fi
1468 fi
1469 done
1470 echo $snaphost $f | btrbk-date-sed
1471 done
1472 }
1473
1474 # If we get a btrfs receive error like this:
1475 # ERROR: ... clone: did not find source subvol
1476 # running this command will help track down the problem.
1477 # Alter remote= and alt=. When I used it, remote is
1478 # the host having the error when I push a snapshot.
1479 # Alt is just the other host that takes snapshots
1480 # besides the local host.
1481 btrbk-host-debug() {
1482
1483 remote=b8.nz
1484 alt=sywg.b8.nz
1485
1486 mkdir -p /tmp/b/s
1487 for host in $remote $alt; do
1488 h=$(ssh $host hostname)
1489 rsync -a /var/log/btrbk $host:/var/log/btrbk /var/log/btrbk/$h
1490 grr '\bsnapshot success' /var/log/btrbk/$h >/tmp/b/$h.log
1491
1492 ## this takes a while, we only want to do it on 1st run
1493 # if [[ -s /tmp/b/$host.log ]]; then continue; fi
1494 # ssh $host journalctl -u btrbk-run -u btrbk -u switch-mail-host >/tmp/b/$host.log
1495 done
1496 gr '\bsnapshot success' /var/log/btrbk/*.log >/tmp/b/local.log
1497 cd /tmp/b
1498 for f in *.log; do
1499 gr '\bsnapshot success' $f >s/$f
1500 done
1501 cd /mnt/root/btrbk
1502 localq=(q.*)
1503 declare -A localq_a
1504 for f in "${localq[@]}"; do
1505 localq_a[$f]=t
1506 done
1507
1508 remoteq=()
1509 for f in $(ssh $remote "cd /mnt/root/btrbk; echo q.*"); do
1510 if [[ ! ${localq_a[$f]} ]]; then
1511 remoteq+=($f)
1512 fi
1513 done
1514 btrbk-host-debug-show-host "${localq[@]}"
1515 if (( ${#remoteq[@]} >= 1 )); then
1516 echo "=== $remote only ===="
1517 btrbk-host-debug-show-host ${remoteq[@]}
1518 fi
1519
1520 }
1521
1522 # note, to check for glue records
1523 # First, find some the .org nameservers:
1524 # dig +trace iankelling.org
1525 # then, query one:
1526 # dig ns1.iankelling.org @b0.org.afilias-nst.org.
1527
1528 # Now, compare for a domain that does have glue records setup (note the A
1529 # and AAAA records in ADDITIONAL SECTION, those are glue records like the
1530 # one I'm asking for):
1531
1532 # $ dig ns1.gnu.org @b0.org.afilias-nst.org.
1533
1534 bbk() { # btrbk wrapper
1535 local ret=0
1536 c /
1537 local active=true
1538 systemctl is-active btrbk.timer || active=false
1539 if $active; then
1540 ser stop btrbk.timer
1541 fi
1542 btrbk_is_active=$(systemctl is-active btrbk.service ||:)
1543 case $btrbk_is_active in
1544 inactive|failed) : ;;
1545 *)
1546 echo "bbk: error: systemctl is-active btrbk.service output: $btrbk_is_active"
1547 if $active; then ser start btrbk.timer; fi
1548 return 1
1549 ;;
1550 esac
1551 # todo: consider changing this to srun and having the args come
1552 # from a file like /etc/default/btrbk, like is done in exim
1553 s jdo btrbk-run "$@"
1554 if $active; then
1555 if (( ret )); then
1556 echo bbk: WARNING: btrbk.timer not restarted due to failure
1557 else
1558 ser start btrbk.timer
1559 fi
1560 fi
1561 return $ret
1562 }
1563
1564 faimon() {
1565 fai-monitor | pee cat "fai-monitor-gui -"
1566 }
1567
1568 bfg() { java -jar /a/opt/bfg-1.12.14.jar "$@"; }
1569
1570 bigclock() {
1571 xclock -digital -update 1 -face 'arial black-80:bold'
1572 }
1573
1574 nnn() { /a/opt/nnn -H "$@"; }
1575
1576 locat() { # log-once cat
1577 local files
1578 ngset
1579 files=(/var/local/cron-errors/* /home/iank/cron-errors/* /sysd-mail-once-state/*)
1580 case ${#files[@]} in
1581 0) : ;;
1582 1)
1583 echo ${files[0]}
1584 head ${files[0]}
1585 ;;
1586 *)
1587 head ${files[@]}
1588 ;;
1589 esac
1590 ngreset
1591 }
1592
1593 scr() {
1594 screen -RD "$@"
1595 }
1596
1597 # usage: first get an adb shell on the phone.
1598 #
1599 # just followed instructions in readme at
1600 # https://github.com/Yuubi-san/ceb-tools
1601 # tried to use ceb2txt but it failed because of schema
1602 # slightly different than what it expected.
1603 cheogram-get-logs() {
1604 #adb shell rm -r /storage/emulated/0/Download/Cheogram/Backup
1605 read -r -p "do cheogram backup on phone, do not enable extra cheogram data. press any key when done"
1606 cd /p/cheogram
1607 rm -rf Backup b
1608 adb pull /storage/emulated/0/Download/Cheogram/Backup
1609 sqlite3 b </a/opt/ceb-tools/schema.sql
1610 echo "note: the next step took 39 seconds last time i measured"
1611 # expected failure: Error: near line 1: in prepare, table accounts has no column named pinned_mechanism (1)
1612 # the sql needs an update
1613 /a/opt/ceb-tools/ceb2sqlgz Backup/iank@fsf.org.ceb <pas | gunzip | sqlite3 b ||:
1614 rm -r Backup
1615 }
1616
1617 # usage: cheologs [DAYS_LIMIT]
1618 # default days is 100
1619 cheologs() {
1620 local days q
1621 days=${1:-100}
1622 q="
1623 select
1624 datetime(substr(timeSent,0,11), 'unixepoch'),
1625 replace(replace(counterpart,'@fsf.org',''),
1626 '@conference.fsf.org',''),
1627 body
1628 from messages
1629 where timeSent > $(( (EPOCHSECONDS - days * 60 * 60 * 24) * 1000 ))
1630 order by timeSent;"
1631 sqlite3 /p/cheogram/b ".mode tabs" "$q" | less
1632 }
1633
1634 mycheologs() {
1635 local days q
1636 days=${1:-16}
1637 # timezone compared to utc. note: this takes the current offset, so if daylight savings change
1638 # happened in the looking back period, this won't account for it.
1639 zone_offset=$(( $( date +%z | sed 's/[^1-9-]*//g' ) * 60 * 60))
1640 case $zone_offset in
1641 -*) : ;;
1642 *) zone_offset="+ $zone_offset"
1643 esac
1644 echo zone_offset=$zone_offset
1645 q="
1646 select
1647 datetime(substr(timeSent,0,11) $zone_offset, 'unixepoch'),
1648 body
1649 from messages
1650 where timeSent > $(( (EPOCHSECONDS - days * 60 * 60 * 24) * 1000 ))
1651 and counterpart = 'office@conference.fsf.org/iank'
1652 order by timeSent;"
1653 sqlite3 /p/cheogram/b ".mode tabs" "$q" | sed 's/ /./' | less
1654 }
1655
1656 # version of jdo for my non-root user
1657 jdo() {
1658 # comparison of alternative logging methods:
1659 #
1660 # systemd-run command (what this function does)
1661 #
1662 # If there is a user prompt, the program will detect that it is not
1663 # connected to a terminal and act in a non-interactive way, skipping
1664 # the prompt. This has the benefit that you know exactly how the
1665 # program will act if you want to move it into a service that runs
1666 # automatically.
1667 #
1668 # If run with sudo and command is a shell script which does a sleep,
1669 # it can (sometimes?) output some extra whitespace in front of
1670 # messages, more for each subsequent message. This can be avoided by
1671 # becoming root first.
1672 #
1673 # It logs the command's pid and exit code, which is nice.
1674 #
1675 #
1676 ### command |& ts | tee file.log
1677 #
1678 # If there is a user prompt, like "read -p prompt var", it will hang
1679 # without outputting the prompt.
1680 #
1681 # I've had a few times where ts had an error and I wasn't totally sure
1682 # if it was really the command or ts having the problem.
1683 #
1684 # Sometimes some output will get hidden until you hit enter.
1685 #
1686 #
1687 ### command |& pee cat logger
1688 #
1689 # This seems to work. I need to test more.
1690 #
1691 #
1692 ### command |& logger -s
1693 #
1694 # User prompts get confusingly prefixed to earlier output, and all log
1695 # entries get prefixed with annoying priority level.
1696 #
1697 #
1698 ### systemd-cat
1699 #
1700 # Had a few problems. One major one is that it exited in the middle of
1701 # a command on systemctl daemon-reload
1702 #
1703 # Related commands which can log a whole session: script, sudo, screen
1704 local cmd cmd_name jr_pid ret
1705 ret=0
1706 cmd="$1"
1707 shift
1708 cmd_name=${cmd##*/}
1709 if [[ $cmd != /* ]]; then
1710 cmd=$(type -P "$cmd")
1711 fi
1712 #note date format for since is date '+%F %T'
1713 # -q = quiet
1714 journalctl --since=now -qn2 -f -u "$cmd_name" &
1715 jr_pid=$!
1716 # note, we could have a version that does system --user, but if for example
1717 # it does sudo ssh, that will leave a process around that we can't kill
1718 # and it will leave the unit hanging around in a failed state needing manual
1719 # killing of the process.
1720 s systemd-run --uid "$(id -u)" --gid "$(id -g)" \
1721 -E SSH_AUTH_SOCK=/run/openssh_agent \
1722 --unit "$cmd_name" --wait --collect "$cmd" "$@" || ret=$?
1723 # The sleep lets the journal output its last line
1724 # before the prompt comes up.
1725 sleep .5
1726 kill $jr_pid &>/dev/null ||:
1727 unset jr_pid
1728 fg &>/dev/null ||:
1729 # this avoids any err-catch
1730 (( ret == 0 )) || return $ret
1731 }
1732
1733 # service run, and watch the output
1734 srun() {
1735 local unit
1736 ret=0
1737 unit=$1
1738 journalctl -qn2 -f -u $unit &
1739 systemctl start $unit
1740 sleep 2
1741 kill $jr_pid &>/dev/null ||:
1742 unset jr_pid
1743 fg &>/dev/null ||:
1744 }
1745
1746 sm() { # switch mail host
1747 local tmp keyhash
1748 c /
1749 # run latest
1750 keyhash=$(s ssh-keygen -lf /root/.ssh/home | awk '{print $2}')
1751 tmp=$(s ssh-add -l | awk '$2 == "'$keyhash'"' ||:)
1752 if [[ ! $tmp ]]; then
1753 s ssh-add /root/.ssh/home
1754 fi
1755 s jdo switch-mail-host "$@"
1756 return $ret
1757 }
1758 sh2() { # switch host2
1759 local tmp keyhash
1760 c /
1761 # run latest
1762 keyhash=$(s ssh-keygen -lf /root/.ssh/home | awk '{print $2}')
1763 tmp=$(s ssh-add -l | awk '$2 == "'$keyhash'"')
1764 if [[ ! $tmp ]]; then
1765 s ssh-add /root/.ssh/home
1766 fi
1767 install-my-scripts
1768 s jdo switch-host2 "$@"
1769 return $ret
1770 }
1771
1772 # shellcheck disable=SC2120
1773 lipush() {
1774 # note, i had --delete-excluded, but that deletes all files in --exclude-from on
1775 # the remote site, which doesn't make sense, so not sure why i had it.
1776 local p a
1777 # excluding emacs for now
1778 #p=(/a/opt/{emacs-debian11{,-nox},mu,emacs} /a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts})
1779 p=(
1780 /a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts}
1781 /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter
1782 /a/opt/fpaste
1783 /p/c/user-specific/www-data/icecast-fsf{,-tech}-htpasswd
1784 /p/c/icecast.xml
1785 )
1786 a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
1787 ret=0
1788 for h in li je bk; do
1789 m s rsync "$@" $a ${p[@]} /p/c/machine_specific/$h root@$h.b8.nz:/
1790 ## only li is debian11
1791 #p[0]=/a/opt/emacs-trisuqel10
1792 #p[1]=/a/opt/emacs-trisquel10-nox
1793 done
1794 m s rsync "$@" -ahviSAXPH root@li.b8.nz:/a/h/proposed-comments/ /a/h/proposed-comments || ret=$?
1795 return $ret
1796 }
1797 bkpush() { # no emacs. for running faster.
1798 p=(/a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts} /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter)
1799 a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
1800 ret=0
1801 m rsync "$@" $a ${p[@]} /p/c/machine_specific/bk root@bk.b8.nz:/ || ret=$?
1802 return $ret
1803 }
1804 jepush() { # no emacs. for running faster.
1805 p=(/a/bin /a/exe /a/h /a/c /p/c/machine_specific/vps{,.hosts} /c/roles/prom_export/files/simple/usr/local/bin/fsf-install-node-exporter)
1806 a="-ahviSAXPH --specials --devices --delete --relative --exclude-from=/p/c/li-rsync-excludes"
1807 ret=0
1808 m rsync "$@" $a ${p[@]} /p/c/machine_specific/je root@je.b8.nz:/ || ret=$?
1809 return $ret
1810 }
1811
1812 bindpush() {
1813 dsign iankelling.org expertpathologyreview.com zroe.org amnimal.ninja
1814 lipush
1815 for h in li bk; do
1816 m ssh iank@$h.b8.nz dnsup
1817 done
1818 }
1819 bindpushb8() {
1820 lipush
1821 for h in li bk; do
1822 m ssh $h.b8.nz dnsb8
1823 done
1824 }
1825
1826 dnsup() {
1827 conflink -f
1828 m ser reload named
1829 }
1830 dnsb8() {
1831 local f=/var/lib/bind/db.b8.nz
1832 m ser stop named
1833 # jbk is like a temp file. dunno if removing it helps
1834
1835 i=0
1836 while pgrep '^named$' &>/dev/null; do
1837 sleep .5
1838 i=$(( i + 1 ))
1839 if (( i > 100 )); then
1840 echo "dnsb8: error: timeout waiting for named to exit"
1841 return 1
1842 fi
1843 done
1844 m sudo rm -fv $f.jnl $f.signed.jnl $f.jbk
1845 m sudo install -m 644 -o bind -g bind /p/c/machine_specific/vps/bind-initial/db.b8.nz $f
1846 m ser restart named
1847 }
1848 dnsecgen() {
1849 # keys generated like this
1850 # because of https://ftp.isc.org/isc/dnssec-guide/dnssec-guide.pdf
1851 # https://blog.apnic.net/2019/05/23/how-to-deploying-dnssec-with-bind-and-ubuntu-server/
1852
1853 # key length is longer than that guide because
1854 # we are using those at fsf and when old key lengths
1855 # become insecure, I want some extra time to update.
1856 # dnsecgen (in brc2)
1857
1858 local zone=$1
1859 dnssec-keygen -a RSASHA256 -b 2048 $zone
1860 dnssec-keygen -f KSK -a RSASHA256 -b 4096 $zone
1861 for f in K"$zone".*.key; do
1862 # eg Kb8.nz.+008+47995.key tag=47995
1863 # in dnsimple, you add the long string from this.
1864 # in gandi, you add the long string from the .key file,
1865 # then see that the digest matches the ds.
1866 echo "tag is the number after DS"
1867 dnssec-dsfromkey -a SHA-256 $f
1868 done
1869 # For b8.nz, we let bind read the keys and sign, and
1870 # right now they have root ownership, so let them
1871 # get group read.
1872 chmod g+r ./*.private
1873 }
1874 dsign() {
1875 # create .signed file
1876 # note: full paths probably not needed.
1877 local arg
1878 for arg; do
1879 local zone=${arg#db.}
1880 local dir=/p/c/machine_specific/vps/filesystem/var/lib/bind
1881 dnssec-signzone -S -e +31536000 -o $zone -K $dir -d $dir $dir/db.$zone
1882 done
1883 }
1884
1885 # set day start for use in other programs.
1886 # expected to do be in a format like 830, or 800 or 1300.
1887 ds() {
1888 local regex
1889 regex='[0-9]?[0-9]?[0-9][0-9]'
1890 if [[ $1 ]]; then
1891 if [[ ! $1 =~ $regex ]]; then
1892 echo "ds: error. expected \$1 to match $regex, got \$1: $1"
1893 return 1
1894 fi
1895 echo $1 >/b/data/daystart
1896 else
1897 cat /b/data/daystart
1898 fi
1899 }
1900
1901 #### begin bitcoin related things
1902 btc() {
1903 local f=/etc/bitcoin/bitcoin.conf
1904 # importprivkey will timeout if using the default of 15 mins.
1905 # upped it to 1 hour.
1906 bitcoin-cli -rpcclienttimeout=60000 -"$(s grep rpcuser= $f)" -"$(s grep rpcpassword= $f)" "$@"
1907 }
1908 btcusd() { # $1 btc in usd
1909 local price
1910 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1911 printf "$%s\n" "$price"
1912 if [[ $1 ]]; then
1913 printf "$%.2f\n" "$(echo "scale=4; $price * $1"| bc -l)"
1914 fi
1915 }
1916 usdbtc() { # $1 usd in btc
1917 local price
1918 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1919 printf "$%s\n" "$price"
1920 if [[ $1 ]]; then
1921 # 100 mil satoshi / btc. 8 digits after the 1.
1922 printf "%.8f btc\n" "$(echo "scale=10; $1 / $price "| bc -l)"
1923 fi
1924 }
1925 satoshi() { # $1 satoshi in usd
1926 local price
1927 price="$(curl -s https://api.coinbase.com/v2/prices/BTC-USD/spot | jq -r .data.amount)"
1928 price=$(echo "scale=10; $price * 0.00000001"| bc -l)
1929 printf "$%f\n" "$price"
1930 if [[ $1 ]]; then
1931 printf "$%.2f\n" "$(echo "scale=10; $price * $1"| bc -l)"
1932 fi
1933 }
1934
1935 # Bitcoin holds open the wallet file. this causes problems for a
1936 # secondary computer running bitcoin and receiving a backup (as of
1937 # 2023). However, in 2024-02, I ran a backup where a receiving machine
1938 # had the wallet enabled and there was no error, so I don't know if this
1939 # is still an issue or likely it is an inconsistent behavior.
1940 # Note: a pruned node won't allow for a wallet to be added, super lame
1941 # so i'm just not running a bitcoin node for now.
1942 # Error: Prune: last wallet synchronisation goes beyond pruned data. You
1943 # need to -reindex (download the whole blockchain again in case of
1944 # pruned node)
1945 #
1946 #### end bitcoin related things
1947
1948
1949
1950 cbfstool () { /a/opt/coreboot/build/cbfstool "$@"; }
1951
1952
1953 cgpl()
1954 {
1955 if (($#)); then
1956 cp /a/bin/data/COPYING "$@"
1957 else
1958 cp /a/bin/data/COPYING .
1959 fi
1960 }
1961
1962 capache()
1963 {
1964 if (($#)); then
1965 cp /a/bin/data/LICENSE "$@"
1966 else
1967 cp /a/bin/data/LICENSE .
1968 fi
1969 }
1970
1971
1972
1973 apache-header() {
1974 # First paragraph is to avoid people being confused about why a
1975 # file is apache licensed.
1976 cat <<'EOF'
1977 # I, Ian Kelling, follow the GNU license recommendations at
1978 # https://www.gnu.org/licenses/license-recommendations.en.html. They
1979 # recommend that small programs, < 300 lines, be licensed under the
1980 # Apache License 2.0. This file contains or is part of one or more small
1981 # programs. If a small program grows beyond 300 lines, I plan to change
1982 # to a recommended GPL license.
1983
1984 # Copyright 2024 Ian Kelling
1985
1986 # Licensed under the Apache License, Version 2.0 (the "License");
1987 # you may not use this file except in compliance with the License.
1988 # You may obtain a copy of the License at
1989
1990 # http://www.apache.org/licenses/LICENSE-2.0
1991
1992 # Unless required by applicable law or agreed to in writing, software
1993 # distributed under the License is distributed on an "AS IS" BASIS,
1994 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1995 # See the License for the specific language governing permissions and
1996 # limitations under the License.
1997
1998 EOF
1999
2000 }
2001
2002 # apply apache to git tracked bash files + README, except files with A?GPL3 header.
2003 apache-apply-repo() {
2004 for f in $(git ls-files); do
2005 [[ -L $f || ! -f $f ]] && continue
2006 if [[ $f != README ]]; then
2007 if ! grep -n '^#!/bin/bash' $f | grep ^1: &>/dev/null; then continue; fi
2008 if head -n 10 $f | grep 'it under the terms of the GNU General Public License as published by' &>/dev/null; then continue; fi
2009 fi
2010 apache-apply $f
2011 done
2012 }
2013
2014 apache-apply() {
2015 for file; do
2016 if [[ ! -e $file ]]; then
2017 echo '#!/bin/bash' >$file
2018 chmod +x $file
2019 fi
2020 if head -n1 "$file"| grep -E '^#!/' &>/dev/null; then
2021 {
2022 head -n1 "$file"
2023 apache-header
2024 tail -n+2 "$file"
2025 } | sponge "$file"
2026 else
2027 {
2028 apache-header
2029 cat "$file"
2030 } | sponge "$file"
2031 fi
2032 done
2033 }
2034 # strip out the apache license from a file.
2035 apache-strip() {
2036 # shellcheck disable=SC2044 # meh
2037 for f in $(find . -type f -maxdepth 1); do if head -n1 "$f"| grep -E '^#!/bin/bash\b' &>/dev/null; then { head -n 20 $f | tac | sed '/^# limitations under the License.$/,/^# Copyright.*Ian Kelling$/d' | tac; tail -n+21 $f; } |sponge $f; fi ; done
2038 }
2039
2040 chro() {
2041 if type -p chromium &>/dev/null; then
2042 cmd=chromium
2043 else
2044 cd /
2045 cmd="schroot -c bookworm chromium"
2046 CHROMIUM_FLAGS='--enable-remote-extensions' $cmd & r
2047 fi
2048 }
2049
2050
2051 # do all tee.
2052 # pipe to this, or just type like a shell
2053 # todo: test this
2054 dat() {
2055 tee >(ssh frodo.b8.nz) >(ssh x2) >(ssh tp.b8.nz) >(ssh kw) >(ssh tp.b8.nz)
2056 }
2057 da() { # do all
2058 local host
2059 for host in x2 kw tp.b8.nz x3.b8.nz frodo.b8.nz; do
2060 ssh $host "$@"
2061 done
2062 }
2063
2064
2065 debian_pick_mirror () {
2066 # netselect-apt finds a fast mirror.
2067 # but we need to replace the mirrors ourselves,
2068 # because it doesnt do that. best it can do is
2069 # output a basic sources file
2070 # here we get the server it found, get the main server we use
2071 # then substitute all instances of one for the other in the sources file
2072 # and backup original to /etc/apt/sources.list-original.
2073 # this is idempotent. the only way to identify debian sources is to
2074 # note the original server, so we put it in a comment so we can
2075 # identify it later.
2076 local file
2077 file=$(mktemp -d)/f # safe way to get file name without creating one
2078 sudo netselect-apt -o "$file" || return 1
2079 url=$(grep ^\\w $file | head -n1 | awk '{print $2}')
2080 sudo cp -f /etc/apt/sources.list /etc/apt/sources.list-original
2081 sudo sed -ri "/http.us.debian.org/ s@( *[^ #]+ +)[^ ]+([^#]+).*@\1$url\2# http.us.debian.org@" /etc/apt/sources.list
2082 sudo apt-get update
2083 }
2084 digme() {
2085 digdiff @ns{1,2}.iankelling.org "$@"
2086 }
2087
2088 dup() {
2089 local ran_d
2090 ran_d=false
2091 system-status _
2092 case $PS1 in
2093 *[\ \]]D\ *)
2094 pushd /
2095 /b/ds/distro-begin |& ts || return $?
2096 /b/ds/distro-end |& ts || return $?
2097 popd
2098 ran_d=true
2099 ;;&
2100 *[\ \]]DB\ *)
2101 pushd /
2102 /b/ds/distro-begin |& ts || return $?
2103 popd
2104 ran_d=true
2105 ;;
2106 *[\ \]]DE\ *)
2107 pushd /
2108 /b/ds/distro-end |& ts || return $?
2109 popd
2110 ran_d=true
2111 ;;&
2112 *CONFLINK*)
2113 if ! $ran_d; then
2114 conflink
2115 fi
2116 ;;
2117 esac
2118 system-status _
2119 }
2120
2121 envload() { # load environment from a previous: export > file
2122 local file=${1:-$HOME/.${USER}_env}
2123 eval "$(export | sed 's/^declare -x/export -n/')"
2124 while IFS= read -r line; do
2125 # declare -x makes variables local to a function
2126 eval ${line/#declare -x/export}
2127 done < "$file"
2128 }
2129
2130 failfunc() { asdf a b c; }
2131 failfunc2() { failfunc d e f; }
2132
2133 # one that comes with distros is too old for newer devices
2134 fastboot() {
2135 /a/opt/android-platform-tools/fastboot "$@";
2136 }
2137
2138 kdecd() { /usr/lib/x86_64-linux-gnu/libexec/kdeconnectd; }
2139
2140 batp() {
2141 cat /sys/class/power_supply/BAT0/capacity
2142 }
2143
2144 # List of apps to install/update
2145 # Create from existing manually installed apps by doing
2146 # fdroidcl update
2147 # fdroidcl search -i, then manually removing
2148 # automatically installed/preinstalled apps
2149
2150 #
2151 # # my attempt at recovering from boot loop:
2152 # # in that case, boot to recovery (volume up, home button, power, let go of power after samsun logo)
2153 # # then
2154 # mount /dev/block/mmcblk0p12 /data
2155 # cd /data
2156 # find -iname '*appname*'
2157 # rm -rf FOUND_DIRS
2158 # usually good enough to just rm -rf /data/app/APPNAME
2159 #
2160 # currently broken:
2161 # # causes replicant to crash
2162 # org.quantumbadger.redreader
2163 # org.kde.kdeconnect_tp
2164
2165 # not broke, but wont work without gps
2166 #com.zoffcc.applications.zanavi
2167 # not broke, but not using atm
2168 #com.nutomic.syncthingandroid
2169 # # doesn\'t work on replicant
2170 #net.sourceforge.opencamera
2171 #
2172 fdroid_pkgs=(
2173 net.mullvad.mullvadvpn
2174 org.schabi.newpipe
2175 io.github.subhamtyagi.lastlauncher
2176 io.anuke.mindustry
2177 com.biglybt.android.client
2178 de.marmaro.krt.ffupdater
2179 me.ccrama.redditslide
2180 org.fedorahosted.freeotp
2181 at.bitfire.davdroid
2182 com.alaskalinuxuser.justnotes
2183 com.artifex.mupdf.viewer.app
2184 com.danielkim.soundrecorder
2185 com.fsck.k9
2186 com.ichi2.anki
2187 com.jmstudios.redmoon
2188 com.jmstudios.chibe
2189 org.kde.kdeconnect_tp
2190 com.notecryptpro
2191 com.termux
2192 cz.martykan.forecastie
2193 de.danoeh.antennapod
2194 de.blinkt.openvpn
2195 de.marmaro.krt.ffupdater
2196 eu.siacs.conversations
2197 free.rm.skytube.oss
2198 im.vector.alpha # riot
2199 info.papdt.blackblub
2200 me.tripsit.tripmobile
2201 net.gaast.giggity
2202 net.minetest.minetest
2203 net.osmand.plus
2204 org.isoron.uhabits
2205 org.linphone
2206 org.gnu.icecat
2207 org.smssecure.smssecure
2208 org.yaaic
2209 sh.ftp.rocketninelabs.meditationassistant.opensource
2210 )
2211 # https://forum.xda-developers.com/android/software-hacking/wip-selinux-capable-superuser-t3216394
2212 # for maru,
2213 #me.phh.superuser
2214
2215 fdup() {
2216 local -A installed updated
2217 local p
2218 # tried putting this in go buildscript cronjob,
2219 # but it failed with undefined: os.UserCacheDir. I expect its due to
2220 # an environment variable missing, but its easier just to stick it here.
2221 m go get -u mvdan.cc/fdroidcl || return 1
2222 m fdroidcl update
2223 if fdroidcl search -u | grep ^org.fdroid.fdroid; then
2224 fdroidcl install org.fdroid.fdroid
2225 sleep 5
2226 m fdroidcl update
2227 fi
2228 for p in $(fdroidcl search -i| grep -o "^\S\+"); do
2229 installed[$p]=true
2230 done
2231 for p in $(fdroidcl search -u| grep -o "^\S\+"); do
2232 updated[$p]=false
2233 done
2234 for p in ${fdroid_pkgs[@]}; do
2235 if ! ${installed[$p]:-false}; then
2236 m fdroidcl install $p
2237 # sleeps are just me being paranoid since replicant has a history of crashing when certain apps are installed
2238 sleep 5
2239 fi
2240 done
2241 for p in ${!installed[@]}; do
2242 if ! ${updated[$p]:-true}; then
2243 m fdroidcl install $p
2244 sleep 5
2245 fi
2246 done
2247 }
2248
2249 firefox-default-profile() {
2250 local key value section
2251 key=Default
2252 value=1
2253 section=$1
2254 file=/p/c/subdir_files/.mozilla/firefox/profiles.ini
2255 sed -ri "/^ *$key/d" "$file"
2256 sed -ri "/ *\[$section\]/,/^ *\[[^]]+\]/{/^\s*${key}[[:space:]=]/d};/ *\[$section\]/a $key=$value" "$file"
2257 }
2258 fdhome() { #firefox default home profile
2259 firefox-default-profile Profile0
2260 }
2261
2262 fdwork() {
2263 firefox-default-profile Profile4
2264 }
2265
2266 ff() {
2267 if type -P firefox &>/dev/null; then
2268 firefox "$@"
2269 else
2270 iceweasel "$@"
2271 fi
2272 }
2273
2274 fn() {
2275 firefox -P alt "$@" >/dev/null 2>&1
2276 }
2277
2278
2279 fsdiff () {
2280 local missing=false
2281 local dname="${PWD##*/}"
2282 local m="/a/tmp/$dname-missing"
2283 local d="/a/tmp/$dname-diff"
2284 [[ -e $d ]] && rm "$d"
2285 [[ -e $m ]] && rm "$m"
2286 local msize=0
2287 local fsfile
2288 while read -r line; do
2289 fsfile="$1${line#.}"
2290 if [[ -e "$fsfile" ]]; then
2291 md5diff "$line" "$fsfile" && tee -a "/a/tmp/$dname-diff" <<< "$fsfile $line"
2292 else
2293 missing=true
2294 echo "$line" >> "$m"
2295 msize=$((msize + 1))
2296 fi
2297 done < <(find . -type f )
2298 if $missing; then
2299 echo "$m"
2300 (( msize <= 100 )) && cat $m
2301 fi
2302 }
2303 fsdiff-test() {
2304 local tmpd x
2305 # expected output, with different tmp dirs
2306 # /tmp/tmp.HDPbwMqdC9/c/d ./c/d
2307 # /a/tmp/tmp.qLDkYxBYPM-missing
2308 # ./b
2309 tmpd="$(mktemp -d)"
2310 cd "$tmpd"
2311 echo ok > a
2312 echo nok > b
2313 mkdir c
2314 echo ok > c/d
2315 local x
2316 x=$(mktemp -d)
2317 mkdir $x/c
2318 echo different > $x/c/d
2319 echo ok > $x/a
2320 fsdiff $x
2321 rm -r "$x" "$tmpd"
2322 }
2323 rename-test() {
2324 # test whether missing files were renamed, generally for use with fsdiff
2325 # $1 = fsdiff output file, $2 = directory to compare to. pwd = fsdiff dir
2326 # echos non-renamed files
2327 local x line found renamed
2328 local -a sums
2329 for x in "$2"/*; do
2330 { sums+=( "$(md5sum < "$x")" ) ; } 2>/dev/null
2331 done
2332 while read -r line; do
2333 { missing_sum=$(md5sum < "$line") ; } 2>/dev/null
2334 renamed=false
2335 for x in "${sums[@]}"; do
2336 if [[ $missing_sum == "$x" ]]; then
2337 renamed=true
2338 break
2339 fi
2340 done
2341 $renamed || echo "$line"
2342 done < "$1"
2343 return 0
2344 }
2345
2346 feh() {
2347 # F = fullscren, z = random, Z = auto zoom
2348 command feh --auto-rotate -FzZ "$@"
2349 }
2350
2351
2352
2353 fw() {
2354 firefox -P default "$@" >/dev/null 2>&1
2355 }
2356
2357 gitian() {
2358 git config user.email ian@iankelling.org
2359 }
2360
2361 # at least in flidas, things rely on gpg being gpg1
2362 gpg() {
2363 if type -P gpg2 &>/dev/null; then
2364 command gpg2 "$@"
2365 else
2366 command gpg "$@"
2367 fi
2368 }
2369
2370 gse() {
2371 local email=iank@fsf.org
2372 git send-email --notes "--envelope-sender=<$email>" \
2373 --suppress-cc=self "$@"
2374 }
2375
2376 gup() { /a/f/gnulib/build-aux/gnupload "$@"; }
2377
2378 dejagnu() { /a/opt/dejagnu/dejagnu "$@"; }
2379
2380 # do git status on published repos.
2381 hstatus() {
2382 c /a/bin/githtml
2383 for x in *; do
2384 cd "$(readlink -f $x)"/..
2385 status=$(i status -s) || pwd
2386 if [[ $status ]]; then
2387 hr
2388 echo $x
2389 printf "%s\n" "$status"
2390 fi
2391 cd /a/bin/githtml
2392 done
2393 }
2394
2395 hsk() {
2396 local x
2397 c /a/bin/githtml
2398 for x in *; do
2399 cd "$(readlink -f $x)"/..
2400 skgit
2401 cd /a/bin/githtml
2402 done
2403 }
2404
2405 ## work log
2406 #
2407 # note: database location is specified in ~/.timetrap.yml, currently /p/.timetrap.db
2408 wlog() {
2409 local day i days_back
2410 days_back=${1:-16}
2411 for (( i=days_back; i>=0; i-- )); do
2412 day=$( date +%F -d @$((EPOCHSECONDS - 86400*i )) )
2413 date "+%a %b %d" -d @$((EPOCHSECONDS - 86400*i )) | tr '\n' ' '
2414 /a/opt/timetrap/bin/t d -ftotal -s $day -e $day all -m '^w|lunch$'
2415 done
2416 }
2417 to() { t out -a "$@"; }
2418 ti() { t in -a "$@"; }
2419 tl() {
2420 local in_secs
2421 to "$*"
2422 t s lunch
2423 t in -a "$*"
2424 in_secs="$(date -d "${*//[_.]/ }" +%s)"
2425 m t out -a "$(date +%F.%T -d @$(( in_secs + 60*45 )) )"
2426 t s w
2427 }
2428
2429
2430 # help me focus. opens 2 windows.
2431 focus() {
2432 /p/c/proc/focus/linux-amd64/focus &
2433 watcharb5
2434 kill %%
2435 }
2436
2437
2438 # Display a list of the active window title
2439 # i've been on with 10 second samples going back
2440 # 5 minutes. If I've been on one window for 10 seconds
2441 # or longer, then display the second count.
2442 #
2443 # Press any key to exit.
2444 watcharb5() {
2445 local char ret
2446 killall arbtt-capture &>/dev/null ||:
2447 rm -f ~/.arbtt/capture.log
2448 arbtt-capture --sample-rate=10 &
2449 while true; do
2450 arb5
2451 ret=0
2452 # i first thought to sleep and capture ctrl-c, but it seems we can't
2453 # capture control-c, unless maybe we implement the commands in a
2454 # separate script or maybe add err-cleanup to err. Anyways, this
2455 # method is superior because any single char exits.
2456 read -rsN1 -t 5 char || ret=$?
2457 if (( ret == 142 )) || [[ ! $char ]]; then
2458 # debug
2459 #e ret=$ret char=$char
2460 :
2461 else
2462 killall arbtt-capture ||:
2463 return 0
2464 fi
2465 done
2466
2467 }
2468
2469 arb5() {
2470 local i j l sec blanks line
2471 local -a arbtt_lines
2472 if [[ ! -e ~/.arbtt/capture.log ]]; then
2473 sleep 5
2474 fi
2475 blanks=$(( LINES - 34 ))
2476 for (( i=0; i < blanks; i++ )); do
2477 echo
2478 done
2479
2480 {
2481 i=0
2482 j=0
2483 # https://stackoverflow.com/questions/56486272/how-to-concat-multiple-fields-to-same-line-with-jq
2484 arbtt_lines=$(arbtt-dump -l 30 -t json | \
2485 jq -r '.[] | [ ( .inactive / 1000 | floor ) , ( .windows[] | select (.active == true) |.title) ] | @tsv' | tac)
2486 for line in "${arbtt_lines[@]}"; do
2487 read -r sec l <<<"$line"
2488 if (( j >= LINES )); then
2489 break
2490 fi
2491 if (( i % 6 == 0 && i >= 2 )); then
2492 j=$(( j + 1 ))
2493 echo "## $(( i / 6 + 1 )) ##"
2494 fi
2495 if (( sec > 10 )); then
2496 printf "%3d %s\n" $sec "$l" | sed -r "s/^(.{$COLUMNS}).*/\1/"
2497 else
2498 printf " %s\n" "$l" | sed -r "s/^(.{$COLUMNS}).*/\1/"
2499 fi
2500 i=$(( i + 1 ))
2501 j=$(( j + 1 ))
2502 done
2503 while (( j < 34 && j < LINES )); do
2504 echo
2505 j=$(( j + 1 ))
2506 done
2507 } | tac
2508 }
2509
2510 arbttlog() {
2511 # from the log, show only the currently active window, and the number of
2512 # seconds of input inactivity.
2513 arbtt-dump "$@" | grep -v '( )\|Current Desktop' | sed -rn '/^[^ ]/{N;s/^(.{21})([0-9]*)[0-9]{3}m.*\(\*/\1\2/;s/^(.{21})[0-9]*.*\(\*/\1/;s/\n//;p}' ; }
2514
2515 idea() {
2516 /a/opt/idea-IC-163.7743.44/bin/idea.sh "$@" & r
2517 }
2518
2519 ilogs-local() {
2520 d=/var/lib/znc/moddata/log/iank/
2521 for n in freenode libera; do
2522 cd $d/$n
2523 hr
2524 for x in "#$1/"*; do
2525 base=${x##*/}
2526 files=()
2527 for f in $@; do
2528 tmp=\#$f/$base
2529 if [[ -e $tmp ]]; then
2530 files+=(\#$f/$base)
2531 fi
2532 done
2533 sed \"s/^./${base%log}/\" ${files[@]}|sort -n
2534 hr
2535 done
2536 done
2537 }
2538 ilogs() {
2539 sl root@iankelling.org ilogs-local "$@"
2540 }
2541
2542
2543 ilog-local() {
2544 local d chan
2545 chan="$1"
2546 d=/var/lib/znc/moddata/log/iank/
2547 for n in freenode libera; do
2548 if [[ ! -d $d$n/"$chan" ]]; then
2549 continue
2550 fi
2551 cd $d$n/"$chan"
2552 hr
2553 for x in *; do
2554 # *** are parts and joins and such, and they make reading hard.
2555 # I probably will want to see them sometimes, just have to
2556 # remove that part.
2557 echo $x; sed "s/^./${x%log}/;/\*\*\*/d" $x; hr;
2558 done
2559 done
2560 }
2561 ilog() {
2562 local chan tmpf
2563 tmpf=$(mktemp)
2564 chan="${1:-#fsfsys}"
2565 # use * instead of -r since that does sorted order
2566 sl root@li.b8.nz ilog-local "$chan" > $tmpf
2567 less +G $tmpf
2568 rm -f $tmpf
2569 }
2570
2571 o() {
2572 if type gio &> /dev/null ; then
2573 gio open "$@"
2574 elif type gvfs-open &> /dev/null ; then
2575 gvfs-open "$@"
2576 else
2577 xdg-open "$@"
2578 fi
2579 # another alternative is run-mailcap
2580 }
2581 ccomp xdg-open o
2582
2583 # jfilter() {
2584 # grep -Evi -e "^(\S+\s+){4}(sudo|sshd|cron)\[\S*:" \
2585 # -e "^(\S+\s+){4}systemd\[\S*: (starting|started) (btrfsmaintstop|dynamicipupdate|spamd dns bug fix cronjob|rss2email)\.*$"
2586 # }
2587 # jtail() {
2588 # journalctl -n 10000 -f "$@" | jfilter
2589 # }
2590 # jr() { journalctl "$@" | jfilter | less ; }
2591 # jrf() { journalctl -n 200 -f "$@" | jfilter; }
2592
2593
2594 ## old version for model01. i need to get that firmware working again.
2595 # kff() { # keyboardio firmware flash. you must hold down the tilde key
2596 # pushd /a/opt/Model01-Firmware
2597 # # if we didn't want this yes hack, then remove "shell read" from
2598 # # /a/opt/Kaleidoscope/etc/makefiles/sketch.mk
2599 # yes $'\n' | VERBOSE=1 make flash
2600 # popd
2601 # }
2602
2603
2604 kff() {
2605 pushd /a/opt/Kaleidoscope/examples/Devices/Keyboardio/Model100
2606 make flash
2607 popd
2608 }
2609
2610 wgkey() {
2611 local umask_orig name
2612 if (( $# != 1 )); then
2613 e expected 1 arg >&2
2614 return 1
2615 fi
2616 name=$1
2617 umask_orig=$(umask)
2618 umask 0077
2619 wg genkey | tee $name-priv.key | wg pubkey > $name-pub.key
2620 umask $umask_orig
2621 }
2622
2623 host-info-all() {
2624 host-info-update
2625
2626 bindpushb8
2627 # for wireguard configs
2628 ssh iank@li.b8.nz conflink
2629 wrt-setup
2630 }
2631
2632
2633
2634
2635
2636 ## for updating host info like ip, location, update /p/c/host-info and
2637 ## host_info below. the host_info array should probably be in its own
2638 ## file that gets sourced so that it can be more easily updated.
2639
2640 # todo: this is so long that it becomes confusing,
2641 # try to split it up.
2642 #
2643 # To make some changes take effect, run host-info-all.
2644 host-info-update() {
2645
2646 local -A vpn_ips host_ips host_macs portfw_ips nonvpn_ips all_ips
2647 local -a root_hosts nonroot_hosts
2648
2649 # the hosts with no mac
2650 root_hosts=( bk je li b8.nz )
2651 for h in ${root_hosts[@]}; do
2652 root_hosts+=(${h}ex)
2653 done
2654 root_hosts+=(cmc)
2655
2656 while read -r ip host mac opts; do
2657 if [[ $ip == *#* || ! $host ]]; then continue; fi
2658
2659 # opt parsing
2660 vpn=false
2661 root=false
2662 for opt in $opts; do
2663 case $opt in
2664 user=root)
2665 root=true
2666 ;;
2667 vpn)
2668 vpn=true
2669 ;;
2670 esac
2671 done
2672
2673 all_ips[$host]=$ip
2674 if $vpn; then
2675 portfw_ips[$host]=$ip
2676 vpn_ips[$host]=$ip
2677 else
2678 nonvpn_ips[$host]=$ip
2679 fi
2680 if $root; then
2681 # note: the reason we have b8.nz suffix here but not for non_root
2682 # hosts is that it is for the User part, the IdentityFile part is
2683 # redundant to *.b8.nz. Also note ${host}i, we only setup those for vpn hosts, but there is no harm in overspecifying here.
2684 root_hosts+=($host ${host}i $host.b8.nz ${host}i.b8.nz)
2685 # shellcheck disable=SC2004 # false positive
2686 root_hosts_a[$host]=t # a for associative array
2687 else
2688 nonroot_hosts+=($host ${host}i)
2689 fi
2690 host_ips[$host]=$ip
2691 if [[ $mac ]]; then
2692 host_macs[$host]=$mac
2693 fi
2694
2695 done </p/c/host-info
2696
2697 {
2698 cat <<EOF
2699 Host ${nonroot_hosts[@]}
2700 User iank
2701 IdentityFile ~/.ssh/home
2702
2703 Host ${root_hosts[@]}
2704 IdentityFile ~/.ssh/home
2705
2706 EOF
2707 for host in ${!vpn_ips[@]}; do
2708 ipsuf=${vpn_ips[$host]}
2709 cat <<EOF
2710 Host ${host}i ${host}i.b8.nz
2711 Port $((2200 + ipsuf))
2712 EOF
2713 done
2714
2715 # convenience of one auth key entry
2716 for host in ${!all_ips[@]}; do
2717 cat <<EOF
2718 Host $host ${host}i $host.b8.nz ${host}i.b8.nz
2719 HostKeyAlias $host.b8.nz
2720 EOF
2721 done
2722 } | cedit -e /p/c/subdir_files/.ssh/config-static
2723
2724 {
2725 # hack to please emacs parser
2726 here_begin="cat <<EOF"
2727 echo "$here_begin"
2728 for host in ${!vpn_ips[@]}; do
2729 ipsuf=${vpn_ips[$host]}
2730 i_port=$(( 2200 + ipsuf ))
2731 cat <<EOF
2732 config redirect
2733 option name ssh$host
2734 option src wan
2735 option src_dport $i_port
2736 option dest_port 22
2737 option dest_ip \$l.$ipsuf
2738 option dest lan
2739 config rule
2740 option src wan
2741 option target ACCEPT
2742 option dest_port $i_port
2743 EOF
2744 done
2745 echo "EOF"
2746 } >/p/c/cmc-firewall-data
2747
2748
2749 local host ipsuf f files
2750
2751 # shellcheck disable=SC2016 # shellcheck doesnt know this is sed
2752 sedi '/edits below here are made automatically/,$d' /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf
2753 for host in ${!vpn_ips[@]}; do
2754 if [[ ${root_hosts_a[$host]} ]]; then
2755 # root machines dont actually need vpn, but
2756 # the classification still helps with other
2757 # configurations.
2758 continue
2759 fi
2760 ipsuf=${vpn_ips[$host]}
2761 wghole $host $ipsuf
2762 u /b/ds/machine_specific/li/filesystem/etc/openvpn/client-config-hole/$host <<EOF
2763 ifconfig-push 10.5.5.${vpn_ips[$host]} 255.255.255.0
2764 EOF
2765 u /a/bin/ds/machine_specific/$host/filesystem/etc/systemd/system/openvpn-client-tr@.service <<EOF
2766 [Unit]
2767 Description=OpenVPN tunnel for %I
2768 After=syslog.target network-online.target
2769 Wants=network-online.target
2770 Documentation=man:openvpn(8)
2771 Documentation=https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage
2772 Documentation=https://community.openvpn.net/openvpn/wiki/HOWTO
2773 Requires=iptables.service
2774
2775 [Service]
2776 Type=notify
2777 RuntimeDirectory=openvpn-client
2778 RuntimeDirectoryMode=0710
2779 WorkingDirectory=/etc/openvpn/client
2780 ExecStart=/usr/sbin/openvpn --suppress-timestamps --nobind --config /etc/openvpn/client/%i.conf
2781 # todo, try reenabling this from the default openvpn,
2782 # it was disabled so we could do bind mounts as a command,
2783 # but now systemd handles it
2784 #CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE
2785 LimitNPROC=10
2786 # DeviceAllow=/dev/null rw
2787 # DeviceAllow=/dev/net/tun rw
2788
2789 # we use .1 to make this be on a different network than kd, so that we can
2790 # talk to transmission on kd from remote host, and still use this
2791 # vpn.
2792 ExecStartPre=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns -n 10.174.$ipsuf start %i
2793 ExecStartPre=/sbin/iptables-restore /a/bin/distro-setup/transmission-firewall/netns.rules
2794 # allow wireguard network to connect
2795 ExecStartPre=/usr/sbin/ip r add 10.8.0.0/24 via 10.174.$ipsuf.1 dev veth1-client
2796 ExecStopPost=/usr/bin/flock -w 20 /tmp/newns.flock /a/bin/newns/newns stop %i
2797 PrivateNetwork=true
2798 BindReadOnlyPaths=/etc/tr-resolv:/run/systemd/resolve:norbind /etc/basic-nsswitch:/etc/resolved-nsswitch:norbind
2799
2800 [Install]
2801 WantedBy=multi-user.target
2802 EOF
2803 done
2804
2805 {
2806 echo "cat <<EOF"
2807 for host in ${!host_ips[@]}; do
2808 ipsuf=${host_ips[$host]}
2809 # shellcheck disable=SC2016 # intentional
2810 echo 'local-data-ptr: "$l.'$ipsuf $host.b8.nz'"'
2811 done
2812 echo "EOF"
2813 } | u /p/c/ptr-data
2814
2815 {
2816 echo "cat <<EOF"
2817 for host in ${!host_macs[@]}; do
2818 ipsuf=${host_ips[$host]}
2819 echo "dhcp-host=${host_macs[$host]},set:$host,\$l.$ipsuf,$host"
2820 done
2821 echo "EOF"
2822 } | u /p/c/dnsmasq-data
2823
2824
2825 b8_ip=$(dig +short b8.nz @iankelling.org | tail -1)
2826 # if our dynamic ip updates broke, set manually, eg:
2827 #b8_ip=72.74.193.xxx
2828 if [[ ! $b8_ip ]]; then
2829 echo "$0: error: got empty b8.nz ip. returning 1"
2830 return 1
2831 fi
2832 {
2833 cat <<EOF
2834 @ A $b8_ip
2835 i A $b8_ip
2836 EOF
2837 for host in ${!nonvpn_ips[@]}; do
2838 ipsuf=${nonvpn_ips[$host]}
2839 echo "$host A 10.2.0.$ipsuf"
2840 done
2841 for host in ${!vpn_ips[@]}; do
2842 ipsuf=${vpn_ips[$host]}
2843 cat <<EOF
2844 $host A 10.2.0.$ipsuf
2845 ${host}wg A 10.8.0.$ipsuf
2846 ${host}vp A 10.5.5.$ipsuf
2847 ${host}tr A 10.174.$ipsuf.2
2848 ${host}i CNAME i.b8.nz.
2849 EOF
2850 done
2851 } | cedit -e vpn-ips-update /p/c/machine_specific/vps/bind-initial/db.b8.nz
2852
2853
2854 echo checking for stray files:
2855
2856 initial_dir="$PWD"
2857 while read -r dir path; do
2858 cd $dir
2859 ngset
2860 files=( */$path )
2861 ngreset
2862 cd "$initial_dir"
2863 for f in "${files[@]}"; do
2864 host=${f%%/*}
2865 if [[ ! ${vpn_ips[$host]} ]]; then
2866 e rm $dir/$f
2867 fi
2868 done
2869 done <<'EOF'
2870 /a/bin/ds/machine_specific filesystem/etc/systemd/system/openvpn-client-tr@.service
2871 /p/c/machine_specific filesystem/etc/wireguard/wghole.conf
2872 EOF
2873
2874 files=( /b/ds/machine_specific/li/filesystem/etc/openvpn/client-config-hole/* )
2875 for f in "${files[@]}"; do
2876 host=${f##*/}
2877 if [[ ! ${vpn_ips[$host]} ]]; then
2878 e rm $f
2879 e ssh root@li.b8.nz rm -f $f
2880 fi
2881 done
2882
2883 tmpf=$(mktemp)
2884 {
2885 printf "%s" "Host * "
2886 sed -n '/^Host /h;/^IdentityFile .*\/home/{g;s/^Host//;s/ / !/gp}' /p/c/subdir_files/.ssh/config-static | tr '\n' ' ' \
2887 | sed -r 's/ *$/\n/'
2888 echo "IdentityFile ~/.ssh/work"
2889 } >$tmpf
2890 cedit -e work-identity /p/c/subdir_files/.ssh/config-static <$tmpf
2891 rm -f $tmpf
2892
2893 ### begin focus on hosts file update ###
2894 #
2895 # This started as its own function, but it actually
2896 # needed to alter the ssh config, so combined it.
2897 #
2898 # background: This is finally doing dynamic ip resolution via the hosts
2899 # file. I considered detecting where each host was dynamically or
2900 # something, but ultimately decided to mostly avoid that, other than
2901 # detecting the status of the current machine I'm on. I want to be able
2902 # to move it around without having to manually type much of anything.
2903 local -a host_domain_suffix hosts
2904 local -A ip_to_hosts
2905 local suf ip i host at_home suf_from_here
2906
2907 source /p/c/domain-info
2908
2909 at_home=false
2910 if ip n | grep -q "10.2.0.1 .* b4:75:0e:fd:06:4a"; then
2911 at_home=true
2912 fi
2913
2914 for i in ${host_domain_suffix[@]}; do
2915 if [[ $i == *.* ]]; then
2916 suf=$i
2917 continue
2918 fi
2919 hosts+=($i)
2920 if [[ $i == "$HOSTNAME" ]]; then
2921 unset "portfw_ips[$i]"
2922 continue
2923 fi
2924
2925 suf_from_here=$suf
2926 if ! $at_home && [[ $suf == .b8.nz || $suf == [wc].b8.nz ]]; then
2927 suf_from_here=i.b8.nz
2928 else
2929 unset "portfw_ips[$i]"
2930 fi
2931
2932 # note this might be outdated until we do a dns push
2933 ip=$(dig +short "$i$suf_from_here" @iankelling.org | tail -n1) ||:
2934 if [[ ! $ip ]]; then
2935 if [[ $suf == .office.fsf.org ]]; then
2936 suf_from_here=wg.b8.nz
2937 ip=$(getent ahostsv4 "$i$suf_from_here" | awk '{ print $1 }' | head -n1) ||:
2938 fi
2939 fi
2940 if [[ $ip ]]; then
2941 ip_to_hosts[$ip]+=" $i"
2942 else
2943 echo error: failed to get ip of "$i$suf_from_here"
2944 fi
2945 done
2946
2947 for ip in "${!ip_to_hosts[@]}"; do
2948 echo "$ip${ip_to_hosts[$ip]}"
2949 done | s cedit -e hosts-file-up /etc/hosts
2950 for host in ${hosts[@]}; do
2951 echo $host
2952 done >/p/c/subdir_files/.dsh/group/btrbk
2953 ### end focus on hosts file update ###
2954
2955
2956 # note: note sure if this is a great way to check.
2957 # todo: think about it
2958
2959 if $at_home; then
2960 # possible that in the future we want to create
2961 # a dynamic file here, and then we can move the cat
2962 # command above out of the conditional
2963 rsync -a /p/c/subdir_files/.ssh/config-static ~/.ssh/config
2964 else
2965 for host in ${!portfw_ips[@]}; do
2966 ipsuf=${portfw_ips[$host]}
2967 cat <<EOF
2968 Host ${host}
2969 Port $((2200 + ipsuf))
2970 EOF
2971 done > ~/.ssh/config-dynamic
2972 cat /p/c/subdir_files/.ssh/config-static ~/.ssh/config-dynamic >~/.ssh/config
2973 fi
2974 }
2975
2976 # usage host ipsuf [extrahost]
2977 #
2978 # If the keys already exist and you want new ones, remove them:
2979 # rm /p/c/machine_specific/$host/filesystem/etc/wireguard/hole-{priv,pub}.key
2980 #
2981 # extrahost is a host/cidr that is allowed to go be routed through the
2982 # vpn by this host.
2983 wghole() {
2984 if (( $# < 2 || $# > 3 )); then
2985 e expected 2-3 arg of hostname, ip suffix, and extrahost >&2
2986 return 1
2987 fi
2988 local host ipsuf umask_orig vpn_allowed
2989 host=$1
2990 ipsuf=$2
2991 if [[ $3 ]]; then
2992 extrahost=,$3
2993 fi
2994 for vpn_host in ${!vpn_ips[@]}; do
2995 if [[ $vpn_host == "$host" ]]; then
2996 continue
2997 fi
2998 vpn_allowed+=",10.174.${vpn_ips[$vpn_host]}.2/32"
2999 done
3000 mkdir -p /p/c/machine_specific/$host/filesystem/etc/wireguard
3001 (
3002 cd /p/c/machine_specific/$host/filesystem/etc/wireguard
3003 umask_orig=$(umask)
3004 umask 0077
3005 if [[ ! -s hole-priv.key || ! -s hole-pub.key ]]; then
3006 wg genkey | tee hole-priv.key | wg pubkey > hole-pub.key
3007 fi
3008 cat >wghole.conf <<EOF
3009 [Interface]
3010 # contents hole-priv.key
3011 PrivateKey = $(cat hole-priv.key)
3012 ListenPort = 1194
3013 Address = 10.8.0.$ipsuf/24
3014 # https://dev.to/tangramvision/what-they-don-t-tell-you-about-setting-up-a-wireguard-vpn-1h2g
3015 # ||: makes the systemd service not fail due to the failed command
3016 PostUp = ping -w10 -c1 10.8.0.1 ||:
3017
3018 [Peer]
3019 # li. called wgmail on that server
3020 PublicKey = CTFsje45qLAU44AbX71Vo+xFJ6rt7Cu6+vdMGyWjBjU=
3021 AllowedIPs = 10.8.0.0/24$vpn_allowed$extrahost
3022 Endpoint = 72.14.176.105:1194
3023 PersistentKeepalive = 25
3024 EOF
3025 umask $umask_orig
3026 # old approach. systemd seems to work fine and cleaner.
3027 rm -f ../network/interfaces.d/wghole
3028 cedit -q $host /p/c/machine_specific/li/filesystem/etc/wireguard/wgmail.conf <<EOF || [[ $? == 1 ]]
3029 [Peer]
3030 PublicKey = $(cat hole-pub.key)
3031 AllowedIPs = 10.8.0.$ipsuf/32,10.174.${vpn_ips[$host]}.2/32
3032 EOF
3033 )
3034 }
3035
3036 # sudo maybe
3037 #
3038 # passes on any initial -* args to sudo.
3039 sudm() {
3040 local arg
3041 local -a sudo_opts
3042 for arg; do
3043 if [[ $arg == -* ]]; then
3044 sudo_opts+=("$arg")
3045 shift
3046 else
3047 break
3048 fi
3049 done
3050 if [[ $EUID == 0 ]]; then
3051 "$@"
3052 else
3053 sudo "${sudo_opts[@]}" "$@"
3054 fi
3055 }
3056
3057 mns-setup() {
3058 local ns
3059 ns=$1
3060 sudm mkdir -p /root/mount_namespaces
3061 if ! sudm mountpoint /root/mount_namespaces >/dev/null; then
3062 m sudm mount --bind /root/mount_namespaces /root/mount_namespaces
3063 fi
3064 m sudm mount --make-private /root/mount_namespaces
3065 if ! sudm test -e /root/mount_namespaces/$ns; then
3066 m sudm touch /root/mount_namespaces/$ns
3067 fi
3068 if ! sudm mountpoint /root/mount_namespaces/$ns >/dev/null; then
3069 m sudm unshare --propagation slave --mount=/root/mount_namespaces/$ns /bin/true
3070 fi
3071
3072 }
3073
3074 mns() { # mount namespace
3075 local ns
3076 ns=$1
3077 shift
3078 mns-setup $ns
3079 m sudm -E /usr/bin/nsenter --mount=/root/mount_namespaces/$ns "$@"
3080 }
3081
3082 mnsd() { # mount namespace + systemd namespace
3083 local ns unit
3084 ns=$1
3085 unit=$2
3086 shift 2
3087
3088 mns-setup $ns
3089
3090 pid=$(servicepid $unit)
3091 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
3092 export -p >$tmpf
3093 printf "%s " "${@@Q}" >>$tmpf
3094 echo >>$tmpf
3095
3096 m sudo nsenter -t $pid -n --mount=/root/mount_namespaces/$ns sudo -u $USER -i bash -c ". $tmpf & sleep 1; rm $tmpf"
3097 }
3098
3099
3100 mnsr() { # mns run
3101 local ns=$1
3102 shift
3103 mns $ns sudo -u iank -E env "PATH=$PATH" "$@"
3104 }
3105
3106 mnsnonetr() {
3107 ns=$1
3108 lomh
3109 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
3110 s ip netns add nonet
3111 fi
3112 mns $ns --net=/var/run/netns/nonet /bin/bash
3113 lomh
3114 }
3115
3116 mnsnonet() {
3117 ns=$1
3118 lomh
3119 if ! s ip netns list | grep -Fx nonet &>/dev/null; then
3120 s ip netns add nonet
3121 fi
3122 mns $ns --net=/var/run/netns/nonet sudo -E -u iank /bin/bash
3123 lomh
3124 }
3125
3126
3127 lom() {
3128 # l = the loopback device
3129 local l base
3130 # get sudo pass cached right away
3131 if ! sudo -nv 2>/dev/null; then
3132 sudo -v
3133 fi
3134 if [[ $1 == /* ]]; then
3135 base=${1##*/}
3136 fs_file=$1
3137 if mns $base mountpoint -q /mnt/$base; then
3138 return 0
3139 fi
3140 l=$(losetup -j $fs_file | sed -rn 's/^([^ ]+): .*/\1/p' | head -n1 ||:)
3141 if [[ ! $l ]]; then
3142 l=$(sudo losetup -f)
3143 m sudo losetup $l $fs_file
3144 fi
3145 if ! sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
3146 if ! m sudo cryptsetup luksOpen $l $base; then
3147 m sudo losetup -d $l
3148 return 1
3149 fi
3150 fi
3151 m sudo mkdir -p /mnt/$base
3152 m mns $base mount /dev/mapper/$base /mnt/$base
3153 m mns $base chown $USER:$USER /mnt/$base
3154 lomh
3155 else
3156 base=$1
3157 if mns $base mountpoint /mnt/$base &>/dev/null; then
3158 m mns $base umount /mnt/$base
3159 fi
3160 if sudo cryptsetup status /dev/mapper/$base &>/dev/null; then
3161 if ! m sudo cryptsetup luksClose /dev/mapper/$base; then
3162 echo lom: failed cryptsetup luksClose /dev/mapper/$base
3163 return 1
3164 fi
3165 fi
3166 l=$(losetup -l --noheadings | awk '$6 ~ /\/'$base'$/ {print $1}')
3167 if [[ $l ]]; then
3168 m sudo losetup -d $l
3169 else
3170 echo lom: warning: no loopback device found
3171 fi
3172 fi
3173 }
3174
3175 # mu personality. for original, just run mp. for 2, run mp 2.
3176 # this is partly duplicated in mail-setup
3177 mp() {
3178 local dead=false
3179 for s in {1..5}; do
3180 if ! killall mu; then
3181 dead=true
3182 break
3183 fi
3184 sleep 1
3185 done
3186 if ! $dead; then
3187 echo error: mu not dead
3188 m psg mu
3189 return 1
3190 fi
3191 suf=$1
3192 set -- /m/mucache ~/.cache/mu /m/.mu ~/.config/mu
3193 while (($#)); do
3194 target=$1$suf
3195 f=$2
3196 shift 2
3197 if [[ -e $f && ! -L $f ]]; then
3198 m rm -rf $f
3199 fi
3200 m ln -sf -T $target $f
3201 done
3202 }
3203
3204 # maildir enable
3205 mdenable() {
3206 local md dst ln_path src two
3207
3208 two=false
3209 case $1 in
3210 -2) two=true; shift ;;
3211 esac
3212
3213 for md; do
3214 src=
3215 if $two; then
3216 dst=/m/4e2/$md
3217 else
3218 dst=/m/4e/$md
3219 fi
3220
3221 ln_path=/m/md/$md
3222 for d in /m/md/$md /m/4e2/$md; do
3223 if [[ -d $d && ! -L $d ]]; then
3224 src=$d
3225 break
3226 fi
3227 done
3228 if [[ ! $src ]]; then
3229 echo "error: could not find $md" >&2
3230 return 1
3231 fi
3232 m mv -T $src $dst
3233 m ln -sf -T $dst $ln_path
3234 done
3235 }
3236 md2enable() {
3237 mdenable -2 "$@"
3238 }
3239 mddisable() {
3240 local md=$1
3241 dst=/m/md/$md
3242
3243 ### begin copied from mdenable, but different d ###
3244 for d in /m/4e/$md /m/4e2/$md; do
3245 if [[ -d $d && ! -L $d ]]; then
3246 src=$d
3247 break
3248 fi
3249 done
3250 if [[ ! $src ]]; then
3251 echo "error: could not find $md" >&2
3252 return 1
3253 fi
3254 ### end copy from mdenable ###
3255
3256 if [[ -L $dst ]]; then m rm $dst; fi
3257 m mv -T $src $dst
3258 }
3259
3260
3261 mdt() {
3262 markdown "$1" >/tmp/mdtest.html
3263 firefox /tmp/mdtest.html
3264 }
3265
3266 mo() { xset dpms force off; } # monitor off
3267
3268 mpvgpu() {
3269 # seems to be the best gpu decoding on my nvidia 670.
3270 # vlc gets similar or better framerate, but is much darker output on my test movie at least.
3271
3272
3273 case $HOSTNAME in
3274 kd)
3275 echo 0f | sudo tee -a /sys/kernel/debug/dri/0/pstate
3276 ;;
3277 esac
3278 # going back to the default slow clock, and slower fan:
3279 # echo 07 | sudo tee -a /sys/kernel/debug/dri/0/pstate
3280 if [[ $DISPLAY ]]; then
3281 mpv --vo=vdpau --hwdec=auto "$@"
3282 else
3283 # waylandvk seems to work the same
3284 mpv --gpu-context=wayland --hwdec=auto
3285 fi
3286 }
3287
3288 mpvd() {
3289 mpv --profile=d "$@";
3290 }
3291 mpva() {
3292 mpv --profile=a "$@";
3293 }
3294 # mpv all media files in . or $1
3295 mpvm() {
3296 local -a extensions arg
3297 # get page source of https://en.wikipedia.org/w/index.php?title=Video_file_format&action=edit
3298 # into /a/x.log, then
3299 # grep '^| *\.' /a/x.log | sed 's/| *//;s/,//g'
3300
3301 # note: to join them together for a regex, do:
3302 # old=; for e in ${extensions[@]/./}; do if [[ ! $old ]]; then old=$e; continue; fi; echo -n "$old|"; old=$e; done; echo $e
3303 extensions=(
3304 .webm
3305 .mkv
3306 .flv
3307 .flv
3308 .vob
3309 .ogv .ogg
3310 .drc
3311 .gif
3312 .gifv
3313 .mng
3314 .avi
3315 .MTS .M2TS .TS
3316 .mov .qt
3317 .wmv
3318 .yuv
3319 .rm
3320 .rmvb
3321 .viv
3322 .asf
3323 .amv
3324 .mp4 .m4p .m4v
3325 .mpg .mp2 .mpeg .mpe .mpv
3326 .mpg .mpeg .m2v
3327 .m4v
3328 .svi
3329 .3gp
3330 .3g2
3331 .mxf
3332 .roq
3333 .nsv
3334 )
3335 arg=("(" -iname "*${extensions[0]}")
3336 for (( i=1 ; i < ${#extensions[@]}; i++ )); do
3337 arg+=(-o -iname "*${extensions[i]}")
3338 done
3339 arg+=(")")
3340 dir=${1:-.}
3341 # debug:
3342 #find $dir "${arg[@]}" -size +200k
3343 find $dir "${arg[@]}" -size +200k -exec mpv --profile=d '{}' +
3344 }
3345 mpvs() {
3346 mpv --profile=s "$@";
3347 }
3348
3349 myirc() {
3350 if [[ ! $1 ]]; then
3351 set -- fsfsys
3352 fi
3353 local -a d
3354 d=( /var/lib/znc/moddata/log/iank/{freenode,libera} )
3355 # use * instead of -r since that does sorted order
3356 ssh root@iankelling.org "for f in ${d[*]}; do cd \$f/#$1; grep '\<iank.*' *; done" | cut --complement -c12-16
3357 }
3358
3359
3360 allmyirc() {
3361 local d
3362 d=/var/lib/znc/moddata/log/iank/freenode
3363 ssh root@iankelling.org "cd $d; find . -mtime -60 -type f -exec grep '\<iank.*' {} +" | sed -r 's,^..([^/]*)/(.{11})(.{5})(.{8}).,\2\4 \1,' | sort
3364 }
3365
3366 # The way pidgin logs with xmpp (maybe related to running cheogram too)
3367 # is that there are sometimes duplicates, and sometimes the a log file
3368 # is for a specific day yet logs messages for subsequent days, and the
3369 # only way to realize that is to notice that the timestamps rolled over
3370 # into a new day, you can't see it in isolation. So, basically, pidgin
3371 # logs are really annoying to read a grep of my messages to find the
3372 # date and time I said when I started and stopped working, so I'm trying
3373 # out a new client: profanity.
3374 mypidgin() {
3375 c /p/c/.purple/logs/jabber/iank@fsf.org/office@conference.fsf.org.chat
3376 for x in *.html; do html2text -o ${x%.html}.txt $x; done;
3377 # shellcheck disable=SC2016 # false positive on ${
3378 grep -A1 ') iank:' ./*.txt \
3379 | sed -r 's/^(.{10})[^ ]*\.txt:\(?([^ ]*)[[:space:]](..). iank:/\1_\2_\3/
3380 s/^[^ ]*\.txt-//
3381 /^--$/d
3382 s/^[^ ]*\.txt:\((.{2}).(.{2}).(.{4}) (.{8}) (.{2})\)?/\3-\1-\2_\4_\5/' \
3383 | sed -n 'x;1d;0~2{G;s/\n/ /;p};${x;p}'
3384 }
3385
3386 # my profanity
3387 #
3388 myprof() {
3389 pushd /home/iank/.local/share/profanity/chatlogs/iank_at_fsf.org/rooms/office_at_conference.fsf.org
3390 logs=(*)
3391 logcount=${#logs[@]}
3392 if (( logcount > 16 )); then
3393 i=$(( logcount - 16 ))
3394 else
3395 i=0
3396 fi
3397 # usually do this on monday, sometimes later
3398 if [[ $(date +%A) == Monday ]]; then
3399 min_date=$(date -d 'monday 2 weeks ago' +%s)
3400 else
3401 min_date=$(date -d 'monday 3 weeks ago' +%s)
3402 fi
3403 for (( ; i < logcount; i++ )); do
3404 log=${logs[$i]}
3405 d=$(date -d "$(head -n1 $log|awk '{print $1}')" +%s)
3406 if (( d < min_date )); then
3407 continue
3408 fi
3409 if awk '$3 == "iank:"' $log | sed -r 's/^(.{10}).(.{8})[^ ]+(.*)/\1_\2\3/' | grep .; then
3410 hr
3411 fi
3412 done
3413 popd
3414 }
3415
3416
3417 # Tail all recent prof logs. Copying from profanity has unwanted line breaks
3418 # especially for links.
3419 profr() {
3420 case $HOSTNAME in
3421 kd)
3422 profr-local
3423 ;;
3424 *)
3425 ssh b8.nz profr-local
3426 ;;
3427 esac
3428 }
3429
3430 profr-local() {
3431 local d0 d1
3432 local -a files
3433 d0="$(date +%Y_%m_%d).log"
3434 d1="$(date -d '1 day ago' +%Y_%m_%d).log"
3435 ngset
3436 files=(/d/p/profanity/chatlogs/iank_at_fsf.org/{*,rooms/*}/{$d0,$d1})
3437 ngreset
3438 if (( ${#files[@]} > 0 )); then
3439 cat "${files[@]}" | sort | tail -n 40
3440 fi
3441 }
3442
3443
3444 # Tail pms in the last day, for the case where we restart profanity and
3445 # didn't check for pms beforehand. Assume the most recent logs are on kd.
3446 # If that isn't the case, use prof-recent-local
3447 prof-recent() {
3448 case $HOSTNAME in
3449 kd)
3450 prof-recent-local
3451 ;;
3452 *)
3453 ssh b8.nz prof-recent-local
3454 ;;
3455 esac
3456 }
3457 prof-recent-local() {
3458 local d dates date files f
3459 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3460 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3461 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3462 files=()
3463 for date in ${dates[@]}; do
3464 f=$d/$date.log
3465 if [[ -e $f ]]; then
3466 files+=($f)
3467 fi
3468 done
3469 if (( ${#files[@]} >= 1 )); then
3470 cat ${files[@]} | tail
3471 hr
3472 fi
3473 done
3474 }
3475
3476 prof-sort() {
3477 case $HOSTNAME in
3478 kd)
3479 prof-recent-sort
3480 ;;
3481 *)
3482 ssh b8.nz prof-recent-sort
3483 ;;
3484 esac
3485 }
3486
3487 prof-recent-sort() {
3488 local d dates date files f
3489 # consider making the day count passed by parameter. note: this works: $(date -d '2 day ago' +%Y_%m_%d)
3490 dates=("$(date +%Y_%m_%d)" "$(date -d '1 day ago' +%Y_%m_%d)" )
3491 files=()
3492 for d in /d/p/profanity/chatlogs/iank_at_fsf.org/!(rooms); do
3493 for date in ${dates[@]}; do
3494 f=$d/$date.log
3495 if [[ -e $f ]]; then
3496 files+=($f)
3497 fi
3498 done
3499 done
3500 for f in "${files[@]}"; do
3501 sed "s/\$/ $f/" $f
3502 done | sort
3503 }
3504
3505
3506 # usage: debvm DEBIAN_VERSION RAM_MB
3507 debvm() {
3508 local ver ram fname src
3509 ver=$1
3510 ram=${2:-2024}
3511 # * is because it might have -backports in the name. we only expect 1 expansion
3512 fnames=( debian-$ver-*nocloud-"$(dpkg --print-architecture)".qcow2 )
3513 if (( ${#fnames[@]} >= 2 )); then
3514 echo "error: iank: unexpected multiple files"
3515 return 1
3516 fi
3517 fname="${fnames[0]}"
3518 src=/a/opt/roms/$fname
3519 if [[ ! -f $src ]]; then
3520 echo debvm: not found $src, download from eg: https://cloud.debian.org/images/cloud/buster/latest/
3521 return 1
3522 fi
3523 cp -a $src /t
3524 # note, in fai-revm we do this: not sure why, maybe because of br device
3525 # --graphics spice,listen=0.0.0.0
3526 m s virt-install --osinfo debian11 --rng /dev/urandom -n deb${ver}tmp --import -r $ram --vcpus 2 --disk /t/$fname --graphics spice
3527 # note: to ssh into this machine will require host key generation: ssh-keygen -A
3528
3529 # random: for cvs2git on gnu www, use debian 10. I could use trisquel,
3530 # but happen to want to try out the debian cloud images. the upstream
3531 # requires python2 and hasn't really changed since the version in d10.
3532 #
3533 # apt install cvs2git cvs
3534 # # 7G was not enough
3535 # mount -o mode=1777,nosuid,nodev,size=34G -t tmpfs tmpfs /tmp
3536 # cvs2git --encoding utf_8 --fallback-encoding ascii --dumpfile=dump www-rsync/www |& tee /tmp/l
3537 ## www-rsync is an rsynced copy of the cvsfrom savannah
3538 }
3539
3540 mygajim() {
3541 local time time_sec time_pretty days
3542 days=${1:-16}
3543 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank' and jid_id = 17;" | while read -r time l; do
3544 case $time in
3545 16*) : ;;
3546 *) continue ;;
3547 esac
3548 if ! time_pretty=$(date +%F.%R -d @$time); then
3549 echo bad time: $time
3550 return 1
3551 fi
3552 echo $time_pretty "$l"
3553 time_sec=${time%%.*}
3554 # only look at the last 18 days. generally just use this for timesheet.
3555 if (( time_sec < EPOCHSECONDS - 60 * 60 * 24 * days )); then break; fi
3556 done
3557 }
3558
3559 allmygajim() {
3560 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs where contact_name = 'iank'" | less
3561 }
3562
3563 gajlogs() {
3564 sqlite3 -separator ' ' /p/c/subdir_files/.local/share/gajim/logs.db "select time, message from logs" | less
3565 }
3566
3567
3568 net-dev-info() {
3569 e "lspci -nnk|gr -iA2 net"
3570 lspci -nnk|gr -iA2 net
3571 hr
3572 e "s lshw -C network"
3573 hr
3574 sudo lshw -C network
3575 }
3576
3577 nk() {
3578 ser stop NetworkManager
3579 ser disable NetworkManager
3580 ser stop NetworkManager-wait-online.service
3581 ser disable NetworkManager-wait-online.service
3582 ser stop dnsmasq
3583 sudo resolvconf -d NetworkManager
3584 # ser start dnsmasq
3585 sudo ifup br0
3586 }
3587 ngo() {
3588 sudo ifdown br0
3589 ser start NetworkManager
3590 sleep 4
3591 sudo nmtui-connect
3592 }
3593
3594 otp() {
3595 oathtool --totp -b "$*" | xclip -selection clipboard
3596 }
3597 # run cmd and copy output
3598 j() {
3599 "$@" |& pee "xclip -r -selection clipboard" cat
3600 }
3601
3602 # xorg copy. copy text piped into command
3603 xc() {
3604 xclip -r -selection clipboard
3605 }
3606 # echo copy
3607 ec() {
3608 pee "xclip -r -selection clipboard" cat
3609 }
3610
3611 pakaraoke() {
3612 # from http://askubuntu.com/questions/456021/remove-vocals-from-mp3-and-get-only-instrumentals
3613 pactl load-module module-ladspa-sink sink_name=Karaoke master=alsa_output.usb-Audioengine_Audioengine_D1-00.analog-stereo plugin=karaoke_1409 label=karaoke control=-30
3614 }
3615
3616 pfind() { #find *$1* in $PATH
3617 [[ $# != 1 ]] && { echo requires 1 argument; return 1; }
3618 local pathArray
3619 IFS=: pathArray=($PATH); unset IFS
3620 find "${pathArray[@]}" -iname "*$1*"
3621 }
3622
3623 pick-trash() {
3624 # trash-restore lists everything that has been trashed at or below CWD
3625 # This picks out files just in CWD, not subdirectories,
3626 # which also match grep $1, usually use $1 for a time string
3627 # which you get from running restore-trash once first
3628 local name x ask
3629 local nth=1
3630 # last condition is to not ask again for ones we skipped
3631 while name="$( echo | restore-trash | gr "$PWD/[^/]\+$" | gr "$1" )" \
3632 && [[ $name ]] && (( $(wc -l <<<"$name") >= nth )); do
3633 name="$(echo "$name" | head -n $nth | tail -n 1 )"
3634 read -r -p "$name [Y/n] " ask
3635 if [[ ! $ask || $ask == [Yy] ]]; then
3636 x=$( echo "$name" | gr -o "^\s*[0-9]*" )
3637 echo $x | restore-trash > /dev/null
3638 elif [[ $ask == [Nn] ]]; then
3639 nth=$((nth+1))
3640 else
3641 return
3642 fi
3643 done
3644 }
3645
3646
3647 pub() {
3648 rld /a/h/_site/ li:/var/www/iankelling.org/html
3649 }
3650
3651
3652 pumpa() {
3653 # fixes the menu bar in xmonad. this won\'t be needed when xmonad
3654 # packages catches up on some changes in future (this is written in
3655 # 4/2017)
3656 #
3657 # geekosaur: so youll want to upgrade to xmonad 0.13 or else use a
3658 # locally modified XMonad.Hooks.ManageDocks that doesnt set the
3659 # work area; turns out it\'s impossible to set correctly if you are
3660 # not a fully EWMH compliant desktop environment
3661 #
3662 # geekosaur: chrome shows one failure mode, qt/kde another, other
3663 # gtk apps a third, ... I came up with a setting that works for me
3664 # locally but apparently doesnt work for others, so we joined the
3665 # other tiling window managers in giving up on setting it at all
3666 #
3667 xprop -root -remove _NET_WORKAREA
3668 command pumpa & r
3669 }
3670
3671 # reviewboard, used at my old job
3672 #rbpipe() { rbt post -o --diff-filename=- "$@"; }
3673 #rbp() { rbt post -o "$@"; }
3674
3675 rebr() {
3676 sudo ifdown br0
3677 sudo ifup br0
3678 }
3679
3680
3681 r2e() { command r2e -d /p/c/rss2email.json -c /p/c/rss2email.cfg "$@"; }
3682 # only run on MAIL_HOST. simpler to keep this on one system.
3683 r2eadd() { # usage: name url
3684 # initial setup of rss2email:
3685 # r2e new r2e@iankelling.org
3686 # that initializes files, and sets default email.
3687 # symlink to the config doesnt work, so I copied it to /p/c
3688 # and then use cli option to specify explicit path.
3689 # Only option changed from default config is to set
3690 # force-from = True
3691 #
3692 # or else for a few feeds, the from address is set by the feed, and
3693 # if I fail delivery, then I send a bounce message to that from
3694 # address, which makes me be a spammer.
3695
3696 r2e add $1 "$2" $1@r2e.iankelling.org
3697 # get up to date and dont send old entries now:
3698 r2e run --no-send $1
3699 }
3700
3701 rspicy() { # usage: HOST DOMAIN
3702 # connect to spice vm remote host. use vspicy for local host
3703 local port
3704 # shellcheck disable=SC2087
3705 port=$(ssh $1<<EOF
3706 sudo virsh dumpxml $2|grep "<graphics.*type='spice'" | \
3707 sed -rn "s/.*port='([0-9]+).*/\1/p"
3708 EOF
3709 )
3710 if [[ $port ]]; then
3711 spicy -h $1 -p $port
3712 else
3713 echo "error: no port found. check that the domain is running."
3714 fi
3715 }
3716
3717
3718 scssl() {
3719 # s gem install scss-lint
3720 pushd /a/opt/thoughtbot-guides
3721 git pull --stat
3722 popd
3723 scss-lint -c /a/opt/thoughtbot-guides/style/sass/.scss-lint.yml "$@"
3724 }
3725
3726 skbrc() {
3727 sk -e 2120,245 /b/ds/brc /b/ds/brc2
3728 }
3729
3730 skaraoke() {
3731 local tmp out
3732 out=${2:-${1%.*}.sh}
3733 tmp=$(mktemp -d)
3734 script -t -c "mpv --no-config --no-resume-playback --no-terminal --no-audio-display '$1'" $tmp/typescript 2>$tmp/timing
3735 # todo, the current sleep seems pretty good, but it
3736 # would be nice to have an empirical measurement, or
3737 # some better wait to sync up.
3738 #
3739 # note: --loop-file=no prevents it from hanging if you have that
3740 # set to inf the mpv config.
3741 # --loop=no prevents it from exit code 3 due to stdin if you
3742 # had it set to inf in mpv config.
3743 #
3744 # args go to mpv, for example --volume=80, 50%
3745 cat >$out <<EOFOUTER
3746 #!/bin/bash
3747 trap "trap - TERM && kill 0" INT TERM ERR; set -e
3748 ( sleep .2; scriptreplay <( cat <<'EOF'
3749 $(cat $tmp/timing)
3750 EOF
3751 ) <( cat <<'EOF'
3752 $(cat $tmp/typescript)
3753 EOF
3754 ))&
3755 base64 -d - <<'EOF'| mpv --loop=no --loop-file=no --no-terminal --no-audio-display "\$@" -
3756 $(base64 "$1")
3757 EOF
3758 kill 0
3759 EOFOUTER
3760 rm -r $tmp
3761 chmod +x $out
3762 }
3763
3764 # ssh meld. usage: host1 host2 file
3765 smeld() {
3766 meld <(ssh $1 cat $3) <(ssh $2 cat $3)
3767 }
3768
3769 # remote file meld
3770 # usage: host file1 file2
3771 rmeld() {
3772 local tmpdir
3773 tmpdir=$(mktemp -d)
3774 scp "$1:$2" "$1:$3" $tmpdir
3775 meld "$tmpdir/${2##*/}" "$tmpdir/${3##*/}"
3776 }
3777
3778
3779 spd() {
3780 PATH=/usr/local/spdhackfix:$PATH command spd "$@"
3781 }
3782
3783 spamf() { # spamtest on FILE
3784 if (( $# != 1 )); then
3785 e spamtest error: expected 1 arg, filename >&2
3786 return 1
3787 fi
3788 sdncmdroot spamassassin sudo -u Debian-exim spamassassin -t --cf='score PYZOR_CHECK 0' <"$1"
3789 }
3790
3791
3792 # mail related
3793 testmail() {
3794 declare -gi _seq; _seq+=1
3795 echo "test body" | m mail -s "test mail from $HOSTNAME, $_seq" "${@:-root@localhost}"
3796 # for testing to send from an external address, you can do for example
3797 # -fian@iank.bid -aFrom:ian@iank.bid web-6fnbs@mail-tester.com
3798 # note in exim, you can retry a deferred message
3799 # s exim -M MSG_ID
3800 # MSG_ID is in /var/log/exim4/mainlog, looks like 1ccdnD-0001nh-EN
3801 }
3802
3803 # to test sieve, use below command. for fsf mail, see offlineimap-sync script
3804 # make modifications, then copy to live file, use -eW to actually modify mailbox
3805 #
3806 # Another option is to use sieve-test SCRIPT MAIL_FILE. note,
3807 # sieve-test doesnt know about envelopes, Im not sure if sieve-filter does.
3808
3809 # sieve with output filter. arg is mailbox, like INBOX.
3810 # This depends on dovecot conf, notably mail_location in /etc/dovecot/conf.d/10-mail.conf
3811
3812 # always run this first, edit the test files, then run the following
3813 testsieve() {
3814 sieve-filter ~/sieve/maintest.sieve ${1:-INBOX} delete 2> >(head; tail) >/tmp/testsieve.log && sed -rn '/^Performed actions:/,/^[^ ]/{/^ /p}' /tmp/testsieve.log | sort | uniq -c
3815 }
3816 runsieve() {
3817 c ~/sieve; cp personal{test,}.sieve; cp lists{test,}.sieve; cp personalend{test,}.sieve
3818 sieve-filter -eWv ~/sieve/maintest.sieve ${1:-INBOX} delete &> /tmp/testsieve.log
3819 sed -r '/^info: filtering:/{h;d};/^info: msgid=$/N;/^info: msgid=.*left message in mailbox [^ ]+$/d;/^info: msgid=/{H;g};/^info: message kept in source mailbox.$/d' /tmp/testsieve.log
3820 }
3821
3822 # usage:
3823 # alertme SUBJECT
3824 # printf "subject\nbody\n" | alertme
3825 alertme() {
3826 if [[ -t 0 ]]; then
3827 exim -t <<EOF
3828 From: alertme@b8.nz
3829 To: alerts@iankelling.org
3830 Subject: $*
3831 EOF
3832 else
3833 read -r sub
3834 { cat <<EOF
3835 From: alertme@b8.nz
3836 To: alerts@iankelling.org
3837 Subject: $sub
3838
3839 EOF
3840 cat
3841 } | exim -t
3842 fi
3843 }
3844 daylertme() {
3845 if [[ -t 0 ]]; then
3846 exim -t <<EOF
3847 From: alertme@b8.nz
3848 To: daylert@iankelling.org
3849 Subject: $*
3850 EOF
3851 else
3852 read -r sub
3853 { cat <<EOF
3854 From: alertme@b8.nz
3855 To: daylert@iankelling.org
3856 Subject: $sub
3857
3858 EOF
3859 cat
3860 } | exim -t
3861 fi
3862 }
3863
3864 # alert when a page goes live.
3865 alert200() {
3866 local quiet url tmpdir
3867 quiet=false
3868 case $1 in
3869 # dont send a diff of the html. some html is not very readable
3870 -q) quiet=true
3871 shift
3872 ;;
3873 esac
3874 url="$1"
3875 tmpdir="$(mktemp -d)"
3876 cd $tmpdir
3877 while true; do
3878 if wget -q "$url"; then
3879 if $quiet; then
3880 echo | daylert 200
3881 else
3882 alertme $tmpdir
3883 fi
3884 fi
3885 sleep $(( 120 + RANDOM % 300 ))
3886 done
3887 }
3888
3889 # alert on changes to a webpage (just the base page that curl gets)
3890 # usage: weblert URL [SUBJECT...]
3891 weblert() {
3892 local u old new quiet
3893 quiet=false
3894 case $1 in
3895 # dont send a diff of the html. some html is not very readable
3896 -q) quiet=true
3897 shift
3898 ;;
3899 esac
3900 u="$1"
3901 shift
3902 subject="${*:-weblert}"
3903 old=$(curl -s "$u") ||:
3904 while true; do
3905 new=$(curl -s "$u") ||:
3906 if [[ $old && $new ]]; then
3907 if [[ $new != "$old" ]]; then
3908 if $quiet; then
3909 echo | daylertme "$subject"
3910 else
3911 diff <(printf "%s\n" "$old") <(printf "%s\n" "$new") | daylertme "$subject" ||:
3912 fi
3913 fi
3914 old="$new"
3915 fi
3916 sleep $(( 60 + RANDOM % 120 ))
3917 done
3918 }
3919
3920 torshell() {
3921 # per man torsocks
3922 # shellcheck disable=SC1090 # expected
3923 source "$(type -p torsocks)" on
3924 }
3925
3926 eless2() {
3927 less /var/log/exim4/nondmain
3928 }
3929
3930
3931 # mail related
3932 testexim() {
3933 # testmail above calls sendmail, which is a link to exim/postfix.
3934 # its docs dont say a way of adding an argument
3935 # to sendmail to turn on debug output. We could make a wrapper, but
3936 # that is a pain. Exim debug args are documented here:
3937 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
3938 #
3939 # http://www.exim.org/exim-html-current/doc/html/spec_html/ch-building_and_installing_exim.html
3940 # note, for exim daemon, you can turn on debug options by
3941 # adding -d, etc to COMMONOPTIONS in
3942 # /etc/default/exim4
3943 #
3944 # to specify recipients other than those in to, cc, bcc, you can use the cli args, eg:
3945 # exim -t 'test@zroe.org, t2@zroe.org' <<'EOF'
3946 #
3947 # -t = get recipient from header
3948 exim -d -t <<EOF
3949 From: ian@iankelling.org
3950 To: submit@b.b8.nz
3951 Subject: testbug1
3952
3953 Package: test
3954 Version:1
3955
3956 This is a test message.
3957 EOF
3958 }
3959
3960 # test bounce exim
3961 testbexim() {
3962 to=$1
3963 exim -d -f '<>' $to <<EOF
3964 From: Mail Delivery System <Mailer-Daemon@gnu.org>
3965 To: $to
3966 Subject: Mail delivery failed: returning message to sender
3967
3968 This message was created automatically by mail delivery software.
3969 EOF
3970
3971 }
3972
3973
3974 # toggle keyboard
3975 tk() {
3976 # based on
3977 # https://askubuntu.com/questions/160945/is-there-a-way-to-disable-a-laptops-internal-keyboard
3978 id=$(xinput --list --id-only 'AT Translated Set 2 keyboard')
3979 if xinput list | grep -F '∼ AT Translated Set 2 keyboard' &>/dev/null; then
3980 echo enabling keyboard
3981 # find the first slave keyboard number, they are all the same in my output.
3982 # if they werent, worst case we would need to save the slave number somewhere
3983 # when it got disabled.
3984 slave=$(xinput list | sed -n 's/.*slave \+keyboard (\([0-9]*\)).*/\1/p' | head -n1)
3985 xinput reattach $id $slave
3986 else
3987 xinput float $id
3988 fi
3989 }
3990
3991 tm() {
3992 # timer in minutes
3993 # --no-config
3994 (sleep "$(calc "$* * 60")" && mpv --no-config --volume 50 /a/bin/data/alarm.mp3) > /dev/null 2>&1 &
3995 }
3996
3997 ## usage: to connect to my main transmission daemon from a different host, run this
3998 trans-remote-route() {
3999 :
4000 }
4001 trg() { transmission-remote-gtk & r; }
4002 # TODO: this wont work transmission.lan doesnt exist
4003 trc() {
4004 # example, set global upload limit to 100 kilobytes:
4005 # trc -u 100
4006 TR_AUTH=":$(jq -r .profiles[0].password ~/.config/transmission-remote-gtk/config.json)" transmission-remote transmission.lan -ne "$@"
4007 }
4008
4009 trysleep() {
4010 retries="$1"
4011 sleepsecs="$2"
4012 shift 2
4013 for (( i=0; i < retries - 1; i++ )); do
4014 if "$@"; then
4015 return 0
4016 fi
4017 sleep $sleepsecs
4018 done
4019 "$@"
4020 }
4021
4022
4023 tu() {
4024 local s
4025 if [[ -e $1 && ! -w $1 || ! -w $(dirname "$1") ]]; then
4026 s=s;
4027 fi
4028 # full path for using in some initial setup steps
4029 $s /a/exe/teeu "$@"
4030 }
4031
4032 # execute exim in its namespace. Useful args like -Mrm
4033 enn() {
4034 local ecmd pid
4035
4036 ecmd="/usr/sbin/exim4 -C /etc/exim4/nn-mainlog.conf"
4037 if ip a show veth1-mail &>/dev/null; then
4038 s $ecmd "$@"
4039 else
4040 sdncmdroot exim4 $ecmd "$@"
4041 fi
4042 }
4043
4044 # get pid of systemd service
4045 servicepid() {
4046 local pid unit dir
4047 unit="$1"
4048 pid=$(systemctl show --property MainPID --value "$unit")
4049 case $pid in
4050 [1-9]*) : ;;
4051 *)
4052
4053 dir=/sys/fs/cgroup/system.slice
4054 if [[ ! -d $dir ]]; then
4055 # t10 and older directory.
4056 dir=/sys/fs/cgroup/systemd/system.slice
4057 fi
4058
4059 # 0 or empty. This file includes the MainPid, so I expect we
4060 # could just get this in the first place, but i don't know if that
4061 # is always the case.
4062 pid=$(head -n1 $dir/${unit%.service}.service/cgroup.procs)
4063 ;;
4064 esac
4065 if [[ $pid ]]; then
4066 printf "%s\n" "$pid"
4067 else
4068 return 1
4069 fi
4070 }
4071
4072 sdnbash() { # systemd namespace bash
4073 local unit pid
4074 if (( $# != 1 )); then
4075 echo $0: error wrong number of args >&2
4076 return 1
4077 fi
4078 unit=$1
4079 pid=$(servicepid $unit)
4080 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash
4081 }
4082
4083 sdnbashroot() { # systemd namespace bash as root
4084 local unit pid
4085 if (( $# != 1 )); then
4086 echo $0: error wrong number of args >&2
4087 return 1
4088 fi
4089 unit=$1
4090 pid=$(servicepid $unit)
4091 m sudo nsenter -t $pid -n -m bash
4092 }
4093
4094
4095 # systemd namespace cmd
4096 # usage: UNIT CMD...
4097 sdncmd() {
4098 local unit pid tmpf
4099 if (( $# <= 1 )); then
4100 echo $0: error wrong number of args >&2
4101 return 1
4102 fi
4103 unit=$1
4104 shift
4105 pid=$(servicepid $unit)
4106 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
4107 export -p >$tmpf
4108 printf "%s " "${@@Q}" >>$tmpf
4109 echo >>$tmpf
4110 m sudo nsenter -t $pid -n -m sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
4111 }
4112
4113 sdncmdroot() { # systemd namespace root command
4114 local unit pid
4115 if (( $# < 2 )); then
4116 echo $0: error wrong number of args >&2
4117 return 1
4118 fi
4119 unit=$1
4120 shift
4121 pid=$(servicepid $unit)
4122 m sudo nsenter -t $pid -n -m "$@"
4123 }
4124
4125
4126 # systemd network namespace (not mount) cmd
4127 # usage: UNIT CMD...
4128 sdnncmd() {
4129 local unit pid tmpf
4130 if (( $# <= 1 )); then
4131 echo $0: error wrong number of args >&2
4132 return 1
4133 fi
4134 unit=$1
4135 shift
4136 pid=$(servicepid $unit)
4137 tmpf=$(mktemp --tmpdir $unit.XXXXXXXXXX)
4138 export -p >$tmpf
4139 printf "%s " "${@@Q}" >>$tmpf
4140 echo >>$tmpf
4141 m sudo nsenter -t $pid -n sudo -u $USER -i bash -c ". $tmpf & rm $tmpf"
4142 }
4143
4144
4145 mailnnbash() {
4146 sdnbash mailnn
4147 }
4148
4149 # we use wireguard now, use mailnnbash.
4150 # mailvpnbash() {
4151 # m sudo nsenter -t $(pgrep -f "/usr/sbin/openvpn .* --config /etc/openvpn/.*mail.conf") -n -m sudo -u $USER -i bash
4152 # }
4153
4154 eximbash() {
4155 sdnbashroot exim4
4156 }
4157 spamnn() {
4158 local spamdpid
4159 spamdpid=$(systemctl show --property MainPID --value spamassassin)
4160 m sudo nsenter -t $spamdpid -n -m sudo -u Debian-exim spamassassin "$@"
4161 }
4162 unboundbash() {
4163 sdnbashroot unbound
4164 }
4165
4166 nmtc() {
4167 s nmtui-connect "$@"
4168 }
4169
4170 # check exim and others network namespace
4171 mailnncheck() {
4172 local unit pid ns mailnn spamd_ser
4173
4174 spamd_ser=spamd
4175 if systemctl cat spamassassin &>/dev/null; then
4176 spamd_ser=spamassassin
4177 fi
4178
4179 # mailvpn would belong on the list if using openvpn
4180 for unit in mailnn unbound dovecot $spamd_ser exim4 radicale; do
4181 pid=$(servicepid $unit)
4182 echo debug: unit=$unit pid=$pid
4183 if [[ ! $pid ]]; then
4184 echo failed to find pid for unit=$unit
4185 continue
4186 fi
4187 if ! ns=$(s readlink /proc/$pid/ns/net); then
4188 echo failed to find ns for unit=$unit pid=$pid
4189 continue
4190 fi
4191 if [[ $mailnn ]]; then
4192 if [[ $ns != "$mailnn" ]]; then
4193 echo "$unit ns $ns != $mailnn"
4194 fi
4195 else
4196 mailnn=$ns
4197 fi
4198 done
4199
4200 }
4201
4202
4203 vpncmd() {
4204 sdncmd openvpn-client-tr@client.service "$@"
4205 }
4206 vpni() {
4207 sdncmd openvpn-client-tr@client.service bash
4208 }
4209 vpnbash() {
4210 sdncmdroot openvpn-client-tr@client.service bash
4211 }
4212
4213
4214 vpn() {
4215 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4216 local vpn_service=openvpn-client
4217 else
4218 local vpn_service=openvpn
4219 fi
4220
4221 [[ $1 ]] || { echo need arg; return 1; }
4222 journalctl --since=now --unit=$vpn_service@$1 -f -n0 &
4223 sudo systemctl start $vpn_service@$1
4224 # sometimes the ask-password agent does not work and needs a delay.
4225 sleep .5
4226 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=779240
4227 # noticed around 8-2017 after update from around stretch release
4228 # on debian testing, even though the bug is much older.
4229 sudo systemd-tty-ask-password-agent
4230 }
4231
4232 fixu() {
4233 local stats
4234 ls -lad /run/user/1000
4235 stats=$(stat -c%a-%g-%u /run/user/1000)
4236 if [[ $stats != 700-1000-1000 ]]; then
4237 m s chmod 700 /run/user/1000; m s chown iank.iank /run/user/1000
4238 fi
4239 }
4240
4241 # unmute desktop output
4242 um() {
4243 local sink card sedcmd
4244 sink=$(pactl get-default-sink)
4245 if [[ $sink == auto_null ]]; then
4246 # guessing there is just one with an off profile. otherwise we will
4247 # need some other solution, like storing the card identifier that we
4248 # muted with nap. Or, we could so some hakery with
4249 # pactl -f json.
4250 sedcmd='/^[[:space:]]*index:/{s/^[[:space:]]*index://;h};/^[[:space:]]*active profile: <off>$/{g;p;q}'
4251 card=$(pacmd list-cards | sed -n "$sedcmd")
4252 m pacmd set-card-profile "$card" output:analog-stereo
4253 fi
4254
4255 m pactl set-sink-mute @DEFAULT_SINK@ false
4256 rm -f /tmp/ianknap
4257 }
4258
4259 nap() {
4260 local sink card
4261 sink=$(pactl get-default-sink)
4262 card="${sink%.*}"
4263 card="${card/output/card}"
4264 m pacmd set-card-profile "$card" off
4265
4266 # clicking on a link in a browser can cause unmute.
4267 # I don't want that. So, use a stronger form of mute
4268 # than this.
4269 #pactl set-sink-mute @DEFAULT_SINK@ true
4270 touch /tmp/ianknap
4271 }
4272
4273
4274 # systemctl is-enabled / status / cat says nothing, instead theres
4275 # some obscure symlink. paths copied from man systemd.unit.
4276 # possibly also usefull, but incomplete, doesnt show units not loaded in memory:
4277 # seru list-dependencies --reverse --all UNIT
4278 sysd-deps() {
4279 local f
4280 local -a dirs search
4281 ngset
4282
4283 case $1 in
4284 u)
4285 search=(
4286 ~/.config/systemd/user.control/*
4287 $XDG_RUNTIME_DIR/systemd/user.control/*
4288 $XDG_RUNTIME_DIR/systemd/transient/*
4289 $XDG_RUNTIME_DIR/systemd/generator.early/*
4290 ~/.config/systemd/user/*
4291 /etc/systemd/user/*
4292 $XDG_RUNTIME_DIR/systemd/user/*
4293 /run/systemd/user/*
4294 $XDG_RUNTIME_DIR/systemd/generator/*
4295 ~/.local/share/systemd/user/*
4296 /usr/lib/systemd/user/*
4297 $XDG_RUNTIME_DIR/systemd/generator.late/*
4298 )
4299 ;;
4300 *)
4301 search=(
4302 /etc/systemd/system.control/*
4303 /run/systemd/system.control/*
4304 /run/systemd/transient/*
4305 /run/systemd/generator.early/*
4306 /etc/systemd/system/*
4307 /etc/systemd/systemd.attached/*
4308 /run/systemd/system/*
4309 /run/systemd/systemd.attached/*
4310 /run/systemd/generator/*
4311 /lib/systemd/system/*
4312 /run/systemd/generator.late/*
4313 )
4314 ;;
4315 esac
4316 for f in "${search[@]}"; do
4317 [[ -d $f ]] || continue
4318 case $f in
4319 *.requires|*.wants)
4320 dirs+=("$f")
4321 ;;
4322 esac
4323 done
4324 # dirs is just so we write out the directory names, ls does it when there is 2 or more dirs.
4325 case ${#dirs[@]} in
4326 1)
4327 echo "${dirs[0]}:"
4328 ll "${dirs[@]}"
4329 ;;
4330 0) : ;;
4331 *)
4332 ll "${dirs[@]}"
4333 ;;
4334 esac
4335 ngreset
4336 }
4337
4338 fixvpndns() {
4339 local link istls
4340 read -r _ link _ istls < <(resolvectl dnsovertls tunfsf)
4341 case $istls in
4342 yes|no) : ;;
4343 *) echo fixvpndns error: unexpected istls value: $istls >&2; return 1 ;;
4344 esac
4345 s busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager SetLinkDNSOverTLS is $link no
4346 }
4347
4348 vpnoff() {
4349 [[ $1 ]] || { echo need arg; return 1; }
4350 if [[ -e /lib/systemd/system/openvpn-client@.service ]]; then
4351 local vpn_service=openvpn-client
4352 else
4353 local vpn_service=openvpn
4354 fi
4355 sudo systemctl stop $vpn_service@$1
4356 }
4357 vpnoffc() { # vpn off client
4358 ser stop openvpn-client-tr@client
4359 }
4360 vpnc() {
4361 local unit
4362 unit=openvpn-client-tr@client
4363 sudo -v
4364 if [[ $(systemctl is-active $unit) != active ]]; then
4365 s systemctl start $unit
4366 sleep 1
4367 fi
4368 }
4369
4370
4371 vspicy() { # usage: VIRSH_DOMAIN
4372 # connect to vms made with virt-install
4373 spicy -p "$(sudo virsh dumpxml "$1"|grep "<graphics.*type='spice'"|\
4374 sed -r "s/.*port='([0-9]+).*/\1/")"
4375 }
4376
4377 wian() {
4378 cat-new-files /m/4e/INBOX/new
4379 }
4380 wakehours() {
4381 local sec
4382 if (( $# != 1 )) ; then
4383 echo wakehours: error: expected 1 arg, got $# >&2
4384 return 1
4385 fi
4386 sec=$(( EPOCHSECONDS - $( date +%s -d $1am ) ))
4387 printf "%d:%02d\n" $(( sec / 60 / 60)) $(( (sec / 60) % 60 ))
4388 }
4389
4390 calvis() { # calendar visualize
4391 install -m 600 /dev/null /tmp/calendar-bytes
4392 while read -r l; do
4393 for char in $l; do
4394 # shellcheck disable=SC2059 # intentional for the hex formatting
4395 printf "\x$(printf "%x" $char)" >>/tmp/calendar-bytes
4396 done
4397 done < <(grep -v '[#-]' /p/calendar-data)
4398 /p/c/proc/calendar/linux-amd64/calendar
4399 }
4400
4401 wtr() { curl wttr.in/boston; }
4402
4403 xevkb() { xev -event keyboard; }
4404
4405 # * misc stuff
4406
4407 vrun() {
4408 printf "running: %s\n" "$*"
4409 "$@"
4410 }
4411
4412 electrum() {
4413 # Running the appimage said fuse was not available, but try
4414 # running the appimage with --appimage-extract, which worked.
4415 # It seems there is no need to backup the wallet, it can be restored
4416 # via the seed onto any computer that needs it.
4417 /a/opt/electrum/squashfs-root/AppRun "$@"
4418
4419
4420 # This was an old way I ran electrum over tor, and seems like I
4421 # imported a bitcoin core wallet.
4422 #
4423 # https://electrum.readthedocs.io/en/latest/tor.html
4424 # https://github.com/spesmilo/electrum-docs/issues/129
4425 # s rsync -ptog --chown bitcoin:bitcoin ~/.Xauthority /var/lib/bitcoind/.Xauthority
4426 # sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/electrum-4.2.1-x86_64.AppImage -p socks5:localhost:9050
4427
4428 }
4429
4430
4431 monero() {
4432 sudo -u bitcoin DISPLAY=$DISPLAY XAUTHORITY=/var/lib/bitcoind/.Xauthority /a/opt/monero-gui-v0.17.3.2/monero-wallet-gui
4433 }
4434
4435
4436 # grep + find
4437 gef() {
4438 faf | grep -E "$@" ||:
4439 rgv "$@"
4440 }
4441
4442 # rg my main files
4443 rgm() {
4444 rg "$@" /p/w.org /a/t.org /a/work.org /b
4445 }
4446
4447 # re all my files more expansively.
4448 # usage [-OPT...] regex space combined
4449 rem() {
4450 local paths
4451 local -a opts
4452 for arg; do
4453 if [[ $arg == -* ]]; then
4454 opts+=("$1")
4455 shift
4456 else
4457 break
4458 fi
4459 done
4460 paths="/p/c /b/"
4461 find $paths -not \( -name .svn -prune -o -name .git -prune \
4462 -o -name .hg -prune -o -name .editor-backups -prune \
4463 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4464 rgv $local_rgv_args -g "!bash_unpublished" "${opts[@]}" -- "$*" $paths /a/work.org ||:
4465 }
4466 reml() { # rem with limit to 5 matches per file
4467 local_rgv_args="-m 5"
4468 rem "$@"
4469 }
4470
4471 rep() {
4472 local paths
4473 paths="/p/c"
4474 find $paths -not \( -name .svn -prune -o -name .git -prune \
4475 -o -name .hg -prune -o -name .editor-backups -prune \
4476 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4477 rgv $local_rgv_args -- "$*" $paths /a/t.org /p/w.org ||:
4478 }
4479 repl() { # rem with limit to 5 matches per file
4480 local local_rgv_args="-m 5"
4481 rem "$@"
4482 }
4483
4484
4485 # re on common fsf files
4486 ref() {
4487 local paths
4488 paths="/f/gluestick /f/brains /f/s /c"
4489 find $paths -not \( -name .svn -prune -o -name .git -prune \
4490 -o -name .hg -prune -o -name .editor-backups -prune \
4491 -o -name .undo-tree-history -prune \) 2>/dev/null | grep -iP --color=auto -- "$*" ||:
4492 rgv -- "$*" $paths /a/work.org ||:
4493 }
4494
4495
4496 # for use in /f/bind
4497 fupzone() {
4498 # shellcheck disable=SC2046 # i want word splitting
4499 ./update-zone $(i s | sed -rn 's/.*db\.(.*)/\1/p')
4500 }
4501
4502 # setup:
4503 # pip3 install linode-cli
4504 # linode-cli
4505 livp9() {
4506 local input ip id tmp
4507 input=$1
4508 if [[ $2 ]]; then
4509 id=$2
4510 ip=$3
4511 else
4512 tmp=$(mktemp)
4513 echo $tmp
4514 linode-cli --json --pretty linodes create --root_pass loxHuceygomGisun | tee $tmp
4515 read -r ip id <<<"$(tail -n+2 $tmp | jq -r '.[0].ipv4[0] , .[0].id')"
4516 for string in $ip $id; do
4517 case $string in
4518 [0-9]*) : ;;
4519 *)
4520 echo "livp9: bad value ip=$ip id=$id input=$input"
4521 return 1
4522 ;;
4523 esac
4524 done
4525 rm $tmp
4526
4527 while true; do
4528 if timeout 4 ssh $ip :; then
4529 break
4530 fi
4531 sleep 3
4532 done
4533 fi
4534 ssh $ip <<EOF
4535 apt-get -qq update
4536 apt-get -qq -y install ffmpeg rsync
4537 mkdir vp9
4538 EOF
4539 m rsync $input $ip:
4540 m ssh $ip ffmpeg -nostdin -hide_banner -loglevel error -i $input -g 192 -vcodec libvpx-vp9 -vf scale=-1:720 -max_muxing_queue_size 9999 -b:v 750K -pass 1 -an -f null /dev/null
4541 m ssh $ip ffmpeg -nostdin -hide_banner -loglevel error -y -i $input -g 192 -vcodec libvpx-vp9 -tile-rows 2 -vf scale=-1:720 -max_muxing_queue_size 9999 -b:v 750K -pass 2 -c:a libvorbis -qscale:a 5 vp9/$input
4542 rsync $ip:vp9/$input vp9
4543 linode-cli linodes delete $id
4544 }
4545
4546 reset-konsole() {
4547 # we also have a file in /a/c/...konsole...
4548 local f=$HOME/.config/konsolerc
4549 setini DefaultProfile profileian.profile "Desktop Entry" $f
4550 setini Favorites profileian.profile "Favorite Profiles" $f
4551 setini ShowMenuBarByDefault false KonsoleWindow $f
4552 setini TabBarPosition Top TabBar $f
4553 }
4554
4555 reset-sakura() {
4556 while read -r k v; do
4557 # shellcheck disable=SC2154
4558 setini $k $v sakura /a/c/subdir_files/.config/sakura/sakura.conf
4559 done <<'EOF'
4560 colorset1_back rgb(33,37,39)
4561 less_questions true
4562 audible_bell No
4563 visible_bell No
4564 disable_numbered_tabswitch true
4565 scroll_lines 10000000
4566 scrollbar true
4567 EOF
4568 }
4569
4570 # make a page of links found in the files $@. redirect output
4571 linkhtml() {
4572 gr -oh 'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)' "$@" | \
4573 rev | sort -u | rev | sed 's,.*,<a href="\0">\0</a><br\>,'
4574 }
4575
4576 reset-xscreensaver() {
4577 # except for spash, i set these by setting gui options in
4578 # xscreensaver-command -demo
4579 # then finding the corresponding option in .xscreensaver
4580 # spash, i happened to notice in .xscreensaver
4581 #
4582 # dpmsOff, monitor doesnt come back on using old free software supported nvidia card
4583 cat > /home/iank/.xscreensaver <<'EOF'
4584 mode: blank
4585 dpmsEnabled: True
4586 dpmsStandby: 0:07:00
4587 dpmsSuspend: 0:08:00
4588 dpmsOff: 0:00:00
4589 timeout: 0:05:00
4590 lock: True
4591 lockTimeout: 0:06:00
4592 splash: False
4593 EOF
4594
4595 }
4596
4597
4598 # very useful, copy directory structure 3 deep. add remove /*/ to change level
4599 # rsync -aivh --exclude '/*/*/*/' -f"+ */" -f"- *" SRC DEST
4600
4601
4602 # * stuff that makes sense to be at the end
4603 if [[ "$SUDOD" ]]; then
4604 # allow failure, for example if we are sudoing into a user with diffferent/lesser permissions.
4605 cd "$SUDOD" ||:
4606 unset SUDOD
4607 elif [[ -d /a ]] && [[ $PWD == "$HOME" ]] && [[ $- == *i* ]]; then
4608 cd /a
4609 OLDPWD=
4610 fi
4611
4612
4613
4614
4615 # for mitmproxy to get a newer python.
4616 # commented until i want to use it because it
4617 # noticably slows bash startup
4618 #
4619
4620 mypyenvinit () {
4621 if [[ $EUID == 0 || ! -e ~/.pyenv/bin ]]; then
4622 echo "error: dont be root. make sure pyenv is installed"
4623 return 1
4624 fi
4625 export PATH="$HOME/.pyenv/bin:$PATH"
4626 eval "$(pyenv init -)"
4627 eval "$(pyenv virtualenv-init -)"
4628 }
4629
4630
4631
4632 # I have the git repo and a release. either one should work.
4633 # I have both because I was trying to solve an issue that
4634 # turned out to be unrelated.
4635 # ARDUINO_PATH=/a/opt/Arduino/build/linux/work
4636
4637 ## i should have documented this...
4638 # based on https://github.com/keyboardio/Kaleidoscope
4639 export KALEIDOSCOPE_DIR=/a/opt/Kaleidoscope
4640
4641 # They want to be added to the start, but i think
4642 # that should be avoided unless we really need it.
4643 path-add --end ~/.npm-global
4644
4645
4646 path-add --end $HOME/.cargo/bin
4647
4648 if type -P rg &>/dev/null; then
4649 # --no-messages because of annoying errors on broken symlinks
4650 # -z = search .gz etc files
4651 # -. = search dotfiles
4652 # -n --no-heading: show files and line numbers together allowing for clicking
4653 rg() {
4654 local path_arg
4655 if [[ ${#@} == 1 ]]; then
4656 path_arg=.
4657 fi
4658
4659 command rg -. -z --no-messages -Lin --no-heading -M 900 --no-ignore-parent --no-ignore-vcs -g '!.git' -g '!auto-save-list' -g '!.savehist' "$@" $path_arg || return $?
4660 }
4661 #fails if not exist. ignore
4662 complete -r rg 2>/dev/null ||:
4663 else
4664 alias rg=grr
4665 fi
4666
4667 # rg with respecting vcs ignore files
4668 rgv() {
4669 local path_arg ret=0
4670 if [[ ${#@} == 1 ]]; then
4671 path_arg=.
4672 fi
4673 # settings that are turned off for pipes, keep them on.
4674 # Found by searching for "terminal" in --help
4675 # --heading
4676 # -n
4677 #
4678 # -. = search dotfiles
4679 # -z = search zipped files
4680 # -i = case insensitive
4681 # -M = max columns
4682 # -n --no-heading: show files and line numbers together allowing for clicking
4683 # --no-messages because of annoying errors on broken symlinks
4684 # --no-ignore-parent because i have /a/.git which ignores almost everything under it.
4685 command rg -n --no-heading -. -z --no-messages -i -M 900 --no-ignore-parent -g '!.git' -g '!auto-save-list' -g '!.savehist' "$@" $path_arg || ret=$?
4686 return $ret
4687 }
4688
4689 amall() {
4690 echo "$(tput setaf 5 2>/dev/null ||:)â–ˆ coresite â–ˆ$(tput sgr0 2>/dev/null||:)"
4691 amfsf "$@"
4692 echo "$(tput setaf 5 2>/dev/null ||:)â–ˆ office â–ˆ$(tput sgr0 2>/dev/null||:)"
4693 amoffice "$@"
4694 }
4695 amallq() { # amall quiet
4696 amfsf "$@"
4697 amoffice "$@"
4698 }
4699 amfsf() {
4700 sedi -r '/alertmanager.url/s/@prom.office/@prom/' ~/.config/amtool/config.yml
4701 amtool "$@"
4702 }
4703 amoffice() {
4704 sedi -r '/alertmanager.url/s/@prom.fsf/@prom.office.fsf/' ~/.config/amtool/config.yml
4705 amtool "$@"
4706 }
4707 amls() {
4708 amall silence query "$@"
4709 }
4710 # amtool silence add
4711 amsa() {
4712 amall silence add "$@"
4713 }
4714 # amtool silence force
4715 amsf() {
4716 amall silence add x!="1"
4717 }
4718 amrmall() {
4719 # note: not sure if quoting of this arg is correct
4720 amfsf silence expire "$(amfsf silence query -q)"
4721 amoffice silence expire "$(amoffice silence query -q)"
4722 }
4723
4724
4725 youtube-dl-update() {
4726 sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
4727 sudo chmod a+rx /usr/local/bin/youtube-dl
4728 }
4729
4730 # https://github.com/yt-dlp/yt-dlp/wiki/Installation
4731 yt-dlp-update() {
4732 sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
4733 sudo chmod a+rx /usr/local/bin/yt-dlp # Make executable
4734 }
4735
4736 mpvyt() {
4737 mpv --ytdl ytdl_path=/usr/local/bin/yt-dlp "$@"
4738 }
4739
4740 # taken from default changes to bashrc and bash_profile
4741 path-add --end --ifexists $HOME/.rvm/bin
4742 # also had ruby bin dir, but moved that to environment.sh
4743 # so its included in overall env
4744
4745
4746 # ya, hacky hardcoded hostnames in 2023. we could do better
4747 hssh-update() {
4748 local -a failed_hosts hosts
4749 case $HOSTNAME in
4750 sy|kd)
4751 hosts=(
4752 kd.b8.nz x3.office.fsf.org syw x2.b8.nz
4753 )
4754 ;;
4755 x3)
4756 hosts=(
4757 b8.nz sywg.b8.nz
4758 )
4759 ;;
4760 esac
4761 for host in ${hosts[@]}; do
4762 e $host
4763 if ! scp /b/fai/fai/config/files/usr/local/bin/hssh/IANK root@$host:/usr/local/bin/hssh; then
4764 failed_hosts+=($host)
4765 fi
4766 done
4767 if (( ${#failed_hosts[@]} >= 1 )); then
4768 echo failed_hosts=${failed_hosts[*]}
4769 return 1
4770 fi
4771 }
4772
4773 noi3bar() {
4774 touch /tmp/noi3bar
4775 }
4776 i3bar() {
4777 rm -fv /tmp/noi3bar
4778 }
4779
4780 # example:
4781 # <#part type="image/jpeg" filename="/home/iank/2023-12-24-ski-trip.jpg" disposition=attachment> <#/part>
4782 #
4783 attach-txt() {
4784 local f
4785 for f; do
4786 if [[ ! -s $f ]]; then
4787 e "error: empty or non-existent file $f"
4788 return 1
4789 fi
4790 done
4791 for f; do
4792 echo '<#part type="image/jpeg" filename="'"$(rl "$f")"'" disposition=attachment> <#/part>'
4793 done | ec
4794 }
4795
4796 ctof() {
4797 units "tempC($1)" tempF
4798 }
4799
4800 ftoc() {
4801 units "tempF($1)" tempC
4802 }
4803
4804 # local icecast
4805 localic() {
4806 local mod=false
4807 cedit live /p/c/machine_specific/vps/filesystem/var/lib/bind/db.iankelling.org <<'EOF' || mod=true
4808 live CNAME b8.nz.
4809 EOF
4810 if $mod; then
4811 ip=$(ip r show default | sed -r 's/.*src ([^ ]*).*/\1/' | head -n1)
4812 if [[ ! $ip ]] && timeout 1 ping -c 1 $ip; then
4813 echo "error: failed to get ip: $ip" >&2
4814 exit 1
4815 fi
4816 cat >/p/c/cmc-firewall-data-http <<EOF
4817 http_ip=$ip
4818 EOF
4819 bindpush
4820 wrt-setup
4821 fi
4822 web-conf -e ian@iankelling.org -f 8000 - apache2 live.iankelling.org <<'EOF'
4823 <Location "/fsf.webm">
4824 AuthType Basic
4825 AuthName "basic_auth"
4826 # created with
4827 # htpasswd -c icecast-fsf-htpasswd USERNAME
4828 AuthUserFile "/etc/icecast-fsf-htpasswd"
4829 Require valid-user
4830 </Location>
4831 <Location "/fsf-tech.webm">
4832 AuthType Basic
4833 AuthName "basic_auth"
4834 AuthUserFile "/etc/icecast-fsf-tech-htpasswd"
4835 Require valid-user
4836 </Location>
4837 EOF
4838 s cat /etc/letsencrypt/live/live.iankelling.org/{fullchain,privkey}.pem | s dd of=/etc/icecast2/fullchainpluskey.pem
4839 ser start icecast2
4840 }
4841 # li icecast
4842 liic() {
4843 cedit live /p/c/machine_specific/vps/filesystem/var/lib/bind/db.iankelling.org <<'EOF' || bindpush
4844 live A 72.14.176.105
4845 AAAA 2600:3c00::f03c:91ff:fe6d:baf8
4846 EOF
4847 }
4848 # icecast rm -r
4849 icrmr() {
4850 find /var/icecast -type f -delete
4851 ssh li.b8.nz find /var/icecast -type f -delete
4852 }
4853
4854
4855 # obs screen switching of
4856 obof() {
4857 ls -l /tmp/no-obs-auto-scene-switch
4858 touch /tmp/no-obs-auto-scene-switch
4859 }
4860 # obs screen switching on
4861 obon() {
4862 ls -l /tmp/no-obs-auto-scene-switch
4863 if [[ -e /tmp/no-obs-auto-scene-switch ]]; then
4864 rm -f /tmp/no-obs-auto-scene-switch
4865 fi
4866 }
4867
4868 obs-gen-profiles() {
4869 local p=/p/c/basic/profiles
4870 sed 's/fsf-sysops/fsf-tech/g' $p/fsfsysops/basic.ini >$p/fsftech/basic.ini
4871 sed 's/fsf-sysops/fsf/g' $p/fsfsysops/basic.ini >$p/fsf/basic.ini
4872 }
4873
4874 # terminal clear. like clear, but put the prompt at the bottom,
4875 # useful for obs streaming the bottom half of a terminal window.
4876 tclear() {
4877 for ((i=0; i<COLUMNS; i++)); do
4878 echo
4879 done
4880 }
4881
4882 opensslcertinfo() {
4883 openssl x509 -txt -in "$@"
4884 }
4885
4886 # dsh on btrbk hosts
4887 dsb() {
4888 :
4889 }
4890
4891 # dsh a file and run it
4892 dsa() {
4893 local ret file
4894 if ! parallel -j 10 scp x {}:/tmp <~/.dsh/group/btrbk; then
4895 echo parallel scp failed. dsa returning $ret
4896 fi
4897 dsh -g btrbk
4898 }
4899
4900 # temporary
4901 zmqsend() {
4902 /nocow/t/ffmpeg-release/ffmpeg-7.0.1/tools/zmqsend "$@"
4903 }
4904
4905 ffg() { /nocow/t/ffmpeg-release/ffmpeg-7.0.1/tools/graph2dot -o /tmp/g.tmp && dot -Tpng /tmp/g.tmp -o /tmp/g.png && feh /tmp/g.png; }
4906
4907 firefox-hide-tabs() {
4908
4909 # without this, make tabs smaller by setting browser.uidensity 1 in about:config
4910
4911 profiledir=$1
4912 [[ $1 ]] || return 1
4913 # Related: the sidebery extension is useful.
4914
4915 # This is from
4916 # https://raw.githubusercontent.com/MrOtherGuy/firefox-csshacks/master/chrome/hide_tabs_toolbar.css
4917
4918 ainsl $profiledir/chrome/userChrome.css '#TabsToolbar{ visibility: collapse !important }'
4919
4920 }
4921
4922 # kill lease on cmc
4923 klease() {
4924 local tmpdir ret out
4925 ret=0
4926 out=$(ssh cmc dnsmasq-end-lease "$1" 2>&1) || ret=1
4927 printf "%s\n" "$out"
4928 if [[ $out == *"try diffing"* ]]; then
4929 tmpdir=$(mktemp -d)
4930 m scp cmc:/tmp/dhcp.leases cmc:/tmp/dhcp.leases.iank $tmpdir
4931 m diff $tmpdir/dhcp.leases $tmpdir/dhcp.leases.iank ||:
4932 rm -rf $tmpdir
4933 fi
4934 return $ret
4935 }
4936
4937 # ffs and switch the bash history on this terminal.
4938 # disabled because I don't really need this and
4939 # the history switching is annoying for debugging.
4940 #
4941 # ffs() {
4942 # local last
4943 # last="${*: -1}"
4944 # if [[ $last && $last != -* && $last != sysops ]]; then
4945 # his
4946 # fi
4947 # command ffs "$@"
4948 # }
4949
4950 i3gen() {
4951 /b/ds/i3-sway/gen
4952 }
4953
4954
4955 # insensitive find plus edit
4956 ife() {
4957 local tmps found_count i char file
4958 local -a found_files
4959 local -A button_file
4960 tmps=$(ifn "$@")
4961 mapfile -t found_files <<<"$tmps"
4962 found_count=${#found_files[@]}
4963 if (( ${#found_files[@]} == 1 )); then
4964 m g ${found_files[0]}
4965 else
4966 i=0
4967 for button in {a..z}; do
4968 button_file[$button]="${found_files[$i]}"
4969 echo $button: ${found_files[$i]}
4970 i=$(( i + 1 ))
4971 if (( i >= found_count )); then
4972 break
4973 fi
4974 done
4975 read -rsN1 -t 5 char ||:
4976 file="${button_file[$char]}"
4977
4978 if [[ $file ]]; then
4979 g "$file"
4980 else
4981 echo "no selection"
4982 fi
4983 fi
4984 }
4985
4986
4987 export BASEFILE_DIR=/a/bin/fai-basefiles
4988
4989 #export ANDROID_HOME=/a/opt/android-home
4990 # https://f-droid.org/en/docs/Installing_the_Server_and_Repo_Tools/
4991 #export USE_SDK_WRAPPER=yes
4992 #PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
4993
4994 # didnt get drush working, if I did, this seems like the
4995 # only good thing to include for it.
4996 # Include Drush completion.
4997 # if [ -f "/home/ian/.drush/drush.complete.sh" ] ; then
4998 # source /home/ian/.drush/drush.complete.sh
4999 # fi
5000
5001
5002 # best practice
5003 unset IFS
5004
5005 # https://wiki.archlinux.org/index.php/Xinitrc#Autostart_X_at_login
5006 # i added an extra condition as gentoo xorg guide says depending on
5007 # $DISPLAY is fragile.
5008 if [[ ! $DISPLAY && $XDG_VTNR == 1 ]] && shopt -q login_shell && isarch; then
5009 exec startx
5010 fi
5011
5012
5013 # ensure no bad programs appending to this file will have an affect
5014 return 0