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